Fastapi refactor update
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
Dockerfile
|
||||
README.md
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
postman
|
||||
Dockerfile
|
||||
README.md
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
postman
|
||||
|
||||
38
.env
38
.env
@@ -1,8 +1,30 @@
|
||||
ENV=local
|
||||
OPENAI_API_KEY=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
|
||||
JWT_SECRET_KEY=6e9c124ba92e8814719dcb0f21200c8aa4d0f119a994ac5e06eb90a366c83ab2
|
||||
JWT_TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0In0.Emrs2D3BmMP4b3zMjw0fJTPeyMwWEBDbxx2vvaWguO0
|
||||
GOOGLE_APPLICATION_CREDENTIALS=firebase-configs/encoach-staging.json
|
||||
HEY_GEN_TOKEN=MjY4MDE0MjdjZmNhNDFmYTlhZGRkNmI3MGFlMzYwZDItMTY5NTExNzY3MA==
|
||||
|
||||
GPT_ZERO_API_KEY=0195b9bb24c5439899f71230809c74af
|
||||
OPENAI_API_KEY=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
|
||||
JWT_SECRET_KEY=6e9c124ba92e8814719dcb0f21200c8aa4d0f119a994ac5e06eb90a366c83ab2
|
||||
JWT_TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0In0.Emrs2D3BmMP4b3zMjw0fJTPeyMwWEBDbxx2vvaWguO0
|
||||
HEY_GEN_TOKEN=MjY4MDE0MjdjZmNhNDFmYTlhZGRkNmI3MGFlMzYwZDItMTY5NTExNzY3MA==
|
||||
GPT_ZERO_API_KEY=0195b9bb24c5439899f71230809c74af
|
||||
MONGODB_URI=mongodb+srv://user:JKpFBymv0WLv3STj@encoach.lz18a.mongodb.net/?retryWrites=true&w=majority&appName=EnCoach
|
||||
GOOGLE_APPLICATION_CREDENTIALS=firebase-configs/encoach-staging.json
|
||||
|
||||
# Staging
|
||||
ENV=staging
|
||||
|
||||
#
|
||||
#FIREBASE_SCRYPT_B64_SIGNER_KEY="qjo/b5U5oNxA8o+PHFMZx/ZfG8ZQ7688zYmwMOcfZvVjOM6aHe4Jf270xgyrVArqLIQwFi7VkFnbysBjueMbVw=="
|
||||
#FIREBASE_SCRYPT_B64_SALT_SEPARATOR="Bw=="
|
||||
#FIREBASE_SCRYPT_ROUNDS=8
|
||||
#FIREBASE_SCRYPT_MEM_COST=14
|
||||
#FIREBASE_PROJECT_ID=encoach-staging
|
||||
#MONGODB_DB=staging
|
||||
|
||||
# Prod
|
||||
#ENV=production
|
||||
|
||||
#GOOGLE_APPLICATION_CREDENTIALS=firebase-configs/storied-phalanx-349916.json
|
||||
#FIREBASE_SCRYPT_B64_SIGNER_KEY="vbO3Xii2lajSeSkCstq3s/dCwpXP7J2YN9rP/KRreU2vGOT1fg+wzSuy1kIhBECqJHG82tmwAilSxLFFtNKVMA=="
|
||||
#FIREBASE_SCRYPT_B64_SALT_SEPARATOR="Bw=="
|
||||
#FIREBASE_SCRYPT_ROUNDS=8
|
||||
#FIREBASE_SCRYPT_MEM_COST=14
|
||||
#FIREBASE_PROJECT_ID=storied-phalanx-349916
|
||||
MONGODB_DB=staging
|
||||
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
__pycache__
|
||||
.idea
|
||||
.env
|
||||
.DS_Store
|
||||
.venv
|
||||
scripts
|
||||
__pycache__
|
||||
.idea
|
||||
.env
|
||||
.DS_Store
|
||||
.venv
|
||||
_scripts
|
||||
|
||||
16
.idea/.gitignore
generated
vendored
16
.idea/.gitignore
generated
vendored
@@ -1,8 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
||||
49
.idea/ielts-be.iml
generated
49
.idea/ielts-be.iml
generated
@@ -1,25 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PackageRequirementsSettings">
|
||||
<option name="versionSpecifier" value="Don't specify version" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/../flaskProject\templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/_scripts" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.11 (ielts-be)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PackageRequirementsSettings">
|
||||
<option name="versionSpecifier" value="Don't specify version" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/../flaskProject\templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
18
.idea/misc.xml
generated
18
.idea/misc.xml
generated
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (ielts-be)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (ielts-be)" project-jdk-type="Python SDK" />
|
||||
<component name="PyCharmProfessionalAdvertiser">
|
||||
<option name="shown" value="true" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (ielts-be)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (ielts-be)" project-jdk-type="Python SDK" />
|
||||
<component name="PyCharmProfessionalAdvertiser">
|
||||
<option name="shown" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
14
.idea/modules.xml
generated
14
.idea/modules.xml
generated
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ielts-be.iml" filepath="$PROJECT_DIR$/.idea/ielts-be.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ielts-be.iml" filepath="$PROJECT_DIR$/.idea/ielts-be.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/vcs.xml
generated
10
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
82
Dockerfile
82
Dockerfile
@@ -1,41 +1,41 @@
|
||||
FROM python:3.11-slim as requirements-stage
|
||||
WORKDIR /tmp
|
||||
RUN pip install poetry
|
||||
COPY pyproject.toml ./poetry.lock* /tmp/
|
||||
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||||
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Allow statements and log messages to immediately appear in the logs
|
||||
ENV PYTHONUNBUFFERED True
|
||||
|
||||
# Copy local code to the container image.
|
||||
ENV APP_HOME /app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
COPY . ./
|
||||
|
||||
COPY --from=requirements-stage /tmp/requirements.txt /app/requirements.txt
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
ffmpeg \
|
||||
poppler-utils \
|
||||
texlive-latex-base \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive-xetex \
|
||||
pandoc \
|
||||
librsvg2-bin \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the web service on container startup. Here we use the gunicorn
|
||||
# webserver, with one worker process and 8 threads.
|
||||
# For environments with multiple CPU cores, increase the number of workers
|
||||
# to be equal to the cores available.
|
||||
# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
|
||||
CMD exec uvicorn --bind 0.0.0.0:8000 --workers 1 --threads 8 --timeout 0 app.server:app
|
||||
FROM python:3.11-slim as requirements-stage
|
||||
WORKDIR /tmp
|
||||
RUN pip install poetry
|
||||
COPY pyproject.toml ./poetry.lock* /tmp/
|
||||
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||||
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Allow statements and log messages to immediately appear in the logs
|
||||
ENV PYTHONUNBUFFERED True
|
||||
|
||||
# Copy local code to the container image.
|
||||
ENV APP_HOME /app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
COPY . ./
|
||||
|
||||
COPY --from=requirements-stage /tmp/requirements.txt /app/requirements.txt
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
ffmpeg \
|
||||
poppler-utils \
|
||||
texlive-latex-base \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive-xetex \
|
||||
pandoc \
|
||||
librsvg2-bin \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the web service on container startup. Here we use the gunicorn
|
||||
# webserver, with one worker process and 8 threads.
|
||||
# For environments with multiple CPU cores, increase the number of workers
|
||||
# to be equal to the cores available.
|
||||
# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
|
||||
CMD exec uvicorn --bind 0.0.0.0:8000 --workers 1 --threads 8 --timeout 0 app.server:app
|
||||
|
||||
101
README.md
101
README.md
@@ -1,52 +1,49 @@
|
||||
Latest refactor from develop's branch commit 5d5cd21 2024-08-28
|
||||
|
||||
|
||||
# Endpoints
|
||||
|
||||
In ielts-ui I've added a wrapper to every backend request in '/src/utils/translate.backend.endpoints.ts' to use the
|
||||
new endpoints if the "BACKEND_TYPE" environment variable is set to "async", if the env variable is not present or
|
||||
with another value, the wrapper will return the old endpoint.
|
||||
|
||||
| Method | ielts-be | This one |
|
||||
|--------|--------------------------------------|---------------------------------------------|
|
||||
| GET | /healthcheck | /api/healthcheck |
|
||||
| GET | /listening_section_1 | /api/listening/section/1 |
|
||||
| GET | /listening_section_2 | /api/listening/section/2 |
|
||||
| GET | /listening_section_3 | /api/listening/section/3 |
|
||||
| GET | /listening_section_4 | /api/listening/section/4 |
|
||||
| POST | /listening | /api/listening |
|
||||
| POST | /writing_task1 | /api/grade/writing/1 |
|
||||
| POST | /writing_task2 | /api/grade/writing/2 |
|
||||
| GET | /writing_task1_general | /api/writing/1 |
|
||||
| GET | /writing_task2_general | /api/writing/2 |
|
||||
| POST | /speaking_task_1 | /api/grade/speaking/1 |
|
||||
| POST | /speaking_task_2 | /api/grade/speaking/2 |
|
||||
| POST | /speaking_task_3 | /api/grade/speaking/3 |
|
||||
| GET | /speaking_task_1 | /api/speaking/1 |
|
||||
| GET | /speaking_task_2 | /api/speaking/2 |
|
||||
| GET | /speaking_task_3 | /api/speaking/3 |
|
||||
| POST | /speaking | /api/speaking |
|
||||
| POST | /speaking/generate_speaking_video | /api/speaking/generate_speaking_video |
|
||||
| POST | /speaking/generate_interactive_video | /api/speaking/generate_interactive_video |
|
||||
| GET | /reading_passage_1 | /api/reading/passage/1 |
|
||||
| GET | /reading_passage_2 | /api/reading/passage/2 |
|
||||
| GET | /reading_passage_3 | /api/reading/passage/3 |
|
||||
| GET | /level | /api/level |
|
||||
| GET | /level_utas | /api/level/utas |
|
||||
| POST | /fetch_tips | /api/training/tips |
|
||||
| POST | /grading_summary | /api/grade/summary |
|
||||
| POST | /grade_short_answers | /api/grade/short_answers |
|
||||
| POST | /upload_level | /api/level/upload |
|
||||
| POST | /training_content | /api/training/ |
|
||||
| POST | /custom_level | /api/level/custom |
|
||||
|
||||
# Run the app
|
||||
|
||||
This is for Windows, creating venv and activating it may differ based on your OS
|
||||
|
||||
1. python -m venv env
|
||||
2. env\Scripts\activate
|
||||
3. pip install poetry
|
||||
4. poetry install
|
||||
5. python app.py
|
||||
|
||||
Latest refactor from develop's branch commit 5d5cd21 2024-08-28
|
||||
|
||||
|
||||
# Endpoints
|
||||
|
||||
|
||||
| Method | ielts-be | This one |
|
||||
|--------|--------------------------------------|---------------------------------------------|
|
||||
| GET | /healthcheck | /api/healthcheck |
|
||||
| GET | /listening_section_1 | /api/listening/section/1 |
|
||||
| GET | /listening_section_2 | /api/listening/section/2 |
|
||||
| GET | /listening_section_3 | /api/listening/section/3 |
|
||||
| GET | /listening_section_4 | /api/listening/section/4 |
|
||||
| POST | /listening | /api/listening |
|
||||
| POST | /writing_task1 | /api/grade/writing/1 |
|
||||
| POST | /writing_task2 | /api/grade/writing/2 |
|
||||
| GET | /writing_task1_general | /api/writing/1 |
|
||||
| GET | /writing_task2_general | /api/writing/2 |
|
||||
| POST | /speaking_task_1 | /api/grade/speaking/1 |
|
||||
| POST | /speaking_task_2 | /api/grade/speaking/2 |
|
||||
| POST | /speaking_task_3 | /api/grade/speaking/3 |
|
||||
| GET | /speaking_task_1 | /api/speaking/1 |
|
||||
| GET | /speaking_task_2 | /api/speaking/2 |
|
||||
| GET | /speaking_task_3 | /api/speaking/3 |
|
||||
| POST | /speaking | /api/speaking |
|
||||
| POST | /speaking/generate_speaking_video | /api/speaking/generate_speaking_video |
|
||||
| POST | /speaking/generate_interactive_video | /api/speaking/generate_interactive_video |
|
||||
| GET | /reading_passage_1 | /api/reading/passage/1 |
|
||||
| GET | /reading_passage_2 | /api/reading/passage/2 |
|
||||
| GET | /reading_passage_3 | /api/reading/passage/3 |
|
||||
| GET | /level | /api/level |
|
||||
| GET | /level_utas | /api/level/utas |
|
||||
| POST | /fetch_tips | /api/training/tips |
|
||||
| POST | /grading_summary | /api/grade/summary |
|
||||
| POST | /grade_short_answers | /api/grade/short_answers |
|
||||
| POST | /upload_level | /api/level/upload |
|
||||
| POST | /training_content | /api/training/ |
|
||||
| POST | /custom_level | /api/level/custom |
|
||||
|
||||
# Run the app
|
||||
|
||||
This is for Windows, creating venv and activating it may differ based on your OS
|
||||
|
||||
1. python -m venv env
|
||||
2. env\Scripts\activate
|
||||
3. pip install poetry
|
||||
4. poetry install
|
||||
5. python app.py
|
||||
|
||||
|
||||
55
app.py
55
app.py
@@ -1,30 +1,25 @@
|
||||
import os
|
||||
|
||||
import click
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--env",
|
||||
type=click.Choice(["local", "dev", "prod"], case_sensitive=False),
|
||||
default="local",
|
||||
)
|
||||
def main(env: str):
|
||||
load_dotenv()
|
||||
os.environ["ENV"] = env
|
||||
if env == "prod":
|
||||
raise Exception("Production environment not supported yet!")
|
||||
|
||||
uvicorn.run(
|
||||
app="app.server:app",
|
||||
host="localhost",
|
||||
port=8000,
|
||||
reload=True if env != "prod" else False,
|
||||
workers=1,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
import click
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--env",
|
||||
type=click.Choice(["local", "staging", "production"], case_sensitive=False),
|
||||
default="staging",
|
||||
)
|
||||
def main(env: str):
|
||||
uvicorn.run(
|
||||
app="app.server:app",
|
||||
host="localhost",
|
||||
port=8000,
|
||||
reload=True if env != "production" else False,
|
||||
workers=1,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .home import home_router
|
||||
from .listening import listening_router
|
||||
from .reading import reading_router
|
||||
from .speaking import speaking_router
|
||||
from .training import training_router
|
||||
from .writing import writing_router
|
||||
from .grade import grade_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(home_router, prefix="/api", tags=["Home"])
|
||||
router.include_router(listening_router, prefix="/api/listening", tags=["Listening"])
|
||||
router.include_router(reading_router, prefix="/api/reading", tags=["Reading"])
|
||||
router.include_router(speaking_router, prefix="/api/speaking", tags=["Speaking"])
|
||||
router.include_router(writing_router, prefix="/api/writing", tags=["Writing"])
|
||||
router.include_router(grade_router, prefix="/api/grade", tags=["Grade"])
|
||||
router.include_router(training_router, prefix="/api/training", tags=["Training"])
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .home import home_router
|
||||
from .listening import listening_router
|
||||
from .reading import reading_router
|
||||
from .speaking import speaking_router
|
||||
from .training import training_router
|
||||
from .writing import writing_router
|
||||
from .grade import grade_router
|
||||
from .user import user_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(home_router, prefix="/api", tags=["Home"])
|
||||
router.include_router(listening_router, prefix="/api/listening", tags=["Listening"])
|
||||
router.include_router(reading_router, prefix="/api/reading", tags=["Reading"])
|
||||
router.include_router(speaking_router, prefix="/api/speaking", tags=["Speaking"])
|
||||
router.include_router(writing_router, prefix="/api/writing", tags=["Writing"])
|
||||
router.include_router(grade_router, prefix="/api/grade", tags=["Grade"])
|
||||
router.include_router(training_router, prefix="/api/training", tags=["Training"])
|
||||
router.include_router(user_router, prefix="/api/user", tags=["Users"])
|
||||
|
||||
148
app/api/grade.py
148
app/api/grade.py
@@ -1,74 +1,74 @@
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Depends, Path, Request
|
||||
|
||||
from app.controllers.abc import IGradeController
|
||||
from app.dtos.writing import WritingGradeTaskDTO
|
||||
from app.dtos.speaking import GradeSpeakingAnswersDTO, GradeSpeakingDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
|
||||
controller = "grade_controller"
|
||||
grade_router = APIRouter()
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/writing/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_writing_task(
|
||||
data: WritingGradeTaskDTO,
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_writing_task(task, data)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/speaking/2',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_speaking_task_2(
|
||||
data: GradeSpeakingDTO,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_speaking_task(2, [data.dict()])
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/speaking/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_speaking_task_1_and_3(
|
||||
data: GradeSpeakingAnswersDTO,
|
||||
task: int = Path(..., ge=1, le=3),
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_speaking_task(task, data.answers)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/summary',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grading_summary(
|
||||
request: Request,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await grade_controller.grading_summary(data)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/short_answers',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_short_answers(
|
||||
request: Request,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await grade_controller.grade_short_answers(data)
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Depends, Path, Request
|
||||
|
||||
from app.controllers.abc import IGradeController
|
||||
from app.dtos.writing import WritingGradeTaskDTO
|
||||
from app.dtos.speaking import GradeSpeakingAnswersDTO, GradeSpeakingDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
|
||||
controller = "grade_controller"
|
||||
grade_router = APIRouter()
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/writing/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_writing_task(
|
||||
data: WritingGradeTaskDTO,
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_writing_task(task, data)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/speaking/2',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_speaking_task_2(
|
||||
data: GradeSpeakingDTO,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_speaking_task(2, [data.dict()])
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/speaking/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_speaking_task_1_and_3(
|
||||
data: GradeSpeakingAnswersDTO,
|
||||
task: int = Path(..., ge=1, le=3),
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
return await grade_controller.grade_speaking_task(task, data.answers)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/summary',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grading_summary(
|
||||
request: Request,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await grade_controller.grading_summary(data)
|
||||
|
||||
|
||||
@grade_router.post(
|
||||
'/short_answers',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def grade_short_answers(
|
||||
request: Request,
|
||||
grade_controller: IGradeController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await grade_controller.grade_short_answers(data)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
home_router = APIRouter()
|
||||
|
||||
|
||||
@home_router.get(
|
||||
'/healthcheck'
|
||||
)
|
||||
async def healthcheck():
|
||||
return {"healthy": True}
|
||||
from fastapi import APIRouter
|
||||
home_router = APIRouter()
|
||||
|
||||
|
||||
@home_router.get(
|
||||
'/healthcheck'
|
||||
)
|
||||
async def healthcheck():
|
||||
return {"healthy": True}
|
||||
|
||||
110
app/api/level.py
110
app/api/level.py
@@ -1,55 +1,55 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, UploadFile, Request
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import ILevelController
|
||||
|
||||
controller = "level_controller"
|
||||
level_router = APIRouter()
|
||||
|
||||
|
||||
@level_router.get(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_level_exam(
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.get_level_exam()
|
||||
|
||||
|
||||
@level_router.get(
|
||||
'/utas',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_level_utas(
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.get_level_utas()
|
||||
|
||||
|
||||
@level_router.post(
|
||||
'/upload',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def upload(
|
||||
file: UploadFile,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.upload_level(file)
|
||||
|
||||
|
||||
@level_router.post(
|
||||
'/custom',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def custom_level(
|
||||
request: Request,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await level_controller.get_custom_level(data)
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, UploadFile, Request
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import ILevelController
|
||||
|
||||
controller = "level_controller"
|
||||
level_router = APIRouter()
|
||||
|
||||
|
||||
@level_router.get(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_level_exam(
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.get_level_exam()
|
||||
|
||||
|
||||
@level_router.get(
|
||||
'/utas',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_level_utas(
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.get_level_utas()
|
||||
|
||||
|
||||
@level_router.post(
|
||||
'/upload',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def upload(
|
||||
file: UploadFile,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
return await level_controller.upload_level(file)
|
||||
|
||||
|
||||
@level_router.post(
|
||||
'/custom',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def custom_level(
|
||||
request: Request,
|
||||
level_controller: ILevelController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await level_controller.get_custom_level(data)
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import IListeningController
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.dtos.listening import SaveListeningDTO
|
||||
|
||||
|
||||
controller = "listening_controller"
|
||||
listening_router = APIRouter()
|
||||
|
||||
|
||||
@listening_router.get(
|
||||
'/section/{section}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_listening_question(
|
||||
exercises: list[str],
|
||||
section: int = Path(..., ge=1, le=4),
|
||||
topic: str | None = None,
|
||||
difficulty: str = random.choice(EducationalContent.DIFFICULTIES),
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.get_listening_question(section, topic, exercises, difficulty)
|
||||
|
||||
|
||||
@listening_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def save_listening(
|
||||
data: SaveListeningDTO,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.save_listening(data)
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import IListeningController
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.dtos.listening import SaveListeningDTO
|
||||
|
||||
|
||||
controller = "listening_controller"
|
||||
listening_router = APIRouter()
|
||||
|
||||
|
||||
@listening_router.get(
|
||||
'/section/{section}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_listening_question(
|
||||
exercises: list[str],
|
||||
section: int = Path(..., ge=1, le=4),
|
||||
topic: str | None = None,
|
||||
difficulty: str = random.choice(EducationalContent.DIFFICULTIES),
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.get_listening_question(section, topic, exercises, difficulty)
|
||||
|
||||
|
||||
@listening_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def save_listening(
|
||||
data: SaveListeningDTO,
|
||||
listening_controller: IListeningController = Depends(Provide[controller])
|
||||
):
|
||||
return await listening_controller.save_listening(data)
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import IReadingController
|
||||
|
||||
controller = "reading_controller"
|
||||
reading_router = APIRouter()
|
||||
|
||||
|
||||
@reading_router.get(
|
||||
'/passage/{passage}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_reading_passage(
|
||||
passage: int = Path(..., ge=1, le=3),
|
||||
topic: str = Query(default=random.choice(EducationalContent.TOPICS)),
|
||||
exercises: list[str] = Query(default=[]),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
reading_controller: IReadingController = Depends(Provide[controller])
|
||||
):
|
||||
return await reading_controller.get_reading_passage(passage, topic, exercises, difficulty)
|
||||
|
||||
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import IReadingController
|
||||
|
||||
controller = "reading_controller"
|
||||
reading_router = APIRouter()
|
||||
|
||||
|
||||
@reading_router.get(
|
||||
'/passage/{passage}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_reading_passage(
|
||||
passage: int = Path(..., ge=1, le=3),
|
||||
topic: str = Query(default=random.choice(EducationalContent.TOPICS)),
|
||||
exercises: list[str] = Query(default=[]),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
reading_controller: IReadingController = Depends(Provide[controller])
|
||||
):
|
||||
return await reading_controller.get_reading_passage(passage, topic, exercises, difficulty)
|
||||
|
||||
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends, BackgroundTasks
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import ISpeakingController
|
||||
from app.dtos.speaking import (
|
||||
SaveSpeakingDTO, GenerateVideo1DTO, GenerateVideo2DTO, GenerateVideo3DTO
|
||||
)
|
||||
|
||||
controller = "speaking_controller"
|
||||
speaking_router = APIRouter()
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/1',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_speaking_task(
|
||||
first_topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
second_topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.get_speaking_part(1, first_topic, difficulty, second_topic)
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_speaking_task(
|
||||
task: int = Path(..., ge=2, le=3),
|
||||
topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.get_speaking_part(task, topic, difficulty)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def save_speaking(
|
||||
data: SaveSpeakingDTO,
|
||||
background_tasks: BackgroundTasks,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.save_speaking(data, background_tasks)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/generate_video/1',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video_1(
|
||||
data: GenerateVideo1DTO,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(
|
||||
1, data.avatar, data.first_topic, data.questions, second_topic=data.second_topic
|
||||
)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/generate_video/2',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video_2(
|
||||
data: GenerateVideo2DTO,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(
|
||||
2, data.avatar, data.topic, [data.question], prompts=data.prompts, suffix=data.suffix
|
||||
)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/generate_video/3',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video_3(
|
||||
data: GenerateVideo3DTO,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(
|
||||
3, data.avatar, data.topic, data.questions
|
||||
)
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends, BackgroundTasks
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import ISpeakingController
|
||||
from app.dtos.speaking import (
|
||||
SaveSpeakingDTO, GenerateVideo1DTO, GenerateVideo2DTO, GenerateVideo3DTO
|
||||
)
|
||||
|
||||
controller = "speaking_controller"
|
||||
speaking_router = APIRouter()
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/1',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_speaking_task(
|
||||
first_topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
second_topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.get_speaking_part(1, first_topic, difficulty, second_topic)
|
||||
|
||||
|
||||
@speaking_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_speaking_task(
|
||||
task: int = Path(..., ge=2, le=3),
|
||||
topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.get_speaking_part(task, topic, difficulty)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def save_speaking(
|
||||
data: SaveSpeakingDTO,
|
||||
background_tasks: BackgroundTasks,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.save_speaking(data, background_tasks)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/generate_video/1',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video_1(
|
||||
data: GenerateVideo1DTO,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(
|
||||
1, data.avatar, data.first_topic, data.questions, second_topic=data.second_topic
|
||||
)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/generate_video/2',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video_2(
|
||||
data: GenerateVideo2DTO,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(
|
||||
2, data.avatar, data.topic, [data.question], prompts=data.prompts, suffix=data.suffix
|
||||
)
|
||||
|
||||
|
||||
@speaking_router.post(
|
||||
'/generate_video/3',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_video_3(
|
||||
data: GenerateVideo3DTO,
|
||||
speaking_controller: ISpeakingController = Depends(Provide[controller])
|
||||
):
|
||||
return await speaking_controller.generate_video(
|
||||
3, data.avatar, data.topic, data.questions
|
||||
)
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from app.dtos.training import FetchTipsDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import ITrainingController
|
||||
|
||||
controller = "training_controller"
|
||||
training_router = APIRouter()
|
||||
|
||||
|
||||
@training_router.post(
|
||||
'/tips',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_reading_passage(
|
||||
data: FetchTipsDTO,
|
||||
training_controller: ITrainingController = Depends(Provide[controller])
|
||||
):
|
||||
return await training_controller.fetch_tips(data)
|
||||
|
||||
|
||||
@training_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def training_content(
|
||||
request: Request,
|
||||
training_controller: ITrainingController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await training_controller.get_training_content(data)
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from app.dtos.training import FetchTipsDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import ITrainingController
|
||||
|
||||
controller = "training_controller"
|
||||
training_router = APIRouter()
|
||||
|
||||
|
||||
@training_router.post(
|
||||
'/tips',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_reading_passage(
|
||||
data: FetchTipsDTO,
|
||||
training_controller: ITrainingController = Depends(Provide[controller])
|
||||
):
|
||||
return await training_controller.fetch_tips(data)
|
||||
|
||||
|
||||
@training_router.post(
|
||||
'/',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def training_content(
|
||||
request: Request,
|
||||
training_controller: ITrainingController = Depends(Provide[controller])
|
||||
):
|
||||
data = await request.json()
|
||||
return await training_controller.get_training_content(data)
|
||||
|
||||
21
app/api/user.py
Normal file
21
app/api/user.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.dtos.user_batch import BatchUsersDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import IUserController
|
||||
|
||||
controller = "user_controller"
|
||||
user_router = APIRouter()
|
||||
|
||||
|
||||
@user_router.post(
|
||||
'/import',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def batch_import(
|
||||
batch: BatchUsersDTO,
|
||||
user_controller: IUserController = Depends(Provide[controller])
|
||||
):
|
||||
return await user_controller.batch_import(batch)
|
||||
@@ -1,25 +1,25 @@
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import IWritingController
|
||||
|
||||
controller = "writing_controller"
|
||||
writing_router = APIRouter()
|
||||
|
||||
|
||||
@writing_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_writing_task_general_question(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
return await writing_controller.get_writing_task_general_question(task, topic, difficulty)
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import IWritingController
|
||||
|
||||
controller = "writing_controller"
|
||||
writing_router = APIRouter()
|
||||
|
||||
|
||||
@writing_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def get_writing_task_general_question(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
|
||||
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
return await writing_controller.get_writing_task_general_question(task, topic, difficulty)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .dependency_injection import config_di
|
||||
|
||||
__all__ = [
|
||||
"config_di"
|
||||
]
|
||||
from .dependency_injection import DependencyInjector
|
||||
|
||||
__all__ = [
|
||||
"DependencyInjector"
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,120 +1,140 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from dependency_injector import providers, containers
|
||||
from firebase_admin import credentials
|
||||
from openai import AsyncOpenAI
|
||||
from httpx import AsyncClient as HTTPClient
|
||||
from google.cloud.firestore_v1 import AsyncClient as FirestoreClient
|
||||
from dotenv import load_dotenv
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
from app.repositories.impl import *
|
||||
from app.services.impl import *
|
||||
from app.controllers.impl import *
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def config_di(
|
||||
*, polly_client: any, http_client: HTTPClient, whisper_model: any
|
||||
) -> None:
|
||||
"""
|
||||
Loads up all the common configs of all the environments
|
||||
and then calls the specific env configs
|
||||
"""
|
||||
# Firebase token
|
||||
cred = credentials.Certificate(os.getenv("GOOGLE_APPLICATION_CREDENTIALS"))
|
||||
firebase_token = cred.get_access_token().access_token
|
||||
|
||||
container = containers.DynamicContainer()
|
||||
|
||||
openai_client = providers.Singleton(AsyncOpenAI)
|
||||
polly_client = providers.Object(polly_client)
|
||||
http_client = providers.Object(http_client)
|
||||
firestore_client = providers.Singleton(FirestoreClient)
|
||||
whisper_model = providers.Object(whisper_model)
|
||||
|
||||
llm = providers.Factory(OpenAI, client=openai_client)
|
||||
stt = providers.Factory(OpenAIWhisper, model=whisper_model)
|
||||
tts = providers.Factory(AWSPolly, client=polly_client)
|
||||
vid_gen = providers.Factory(Heygen, client=http_client, heygen_token=os.getenv("HEY_GEN_TOKEN"))
|
||||
ai_detector = providers.Factory(GPTZero, client=http_client, gpt_zero_key=os.getenv("GPT_ZERO_API_KEY"))
|
||||
|
||||
firebase_instance = providers.Factory(
|
||||
FirebaseStorage, client=http_client, token=firebase_token, bucket=os.getenv("FIREBASE_BUCKET")
|
||||
)
|
||||
|
||||
firestore = providers.Factory(Firestore, client=firestore_client)
|
||||
|
||||
# Services
|
||||
|
||||
listening_service = providers.Factory(
|
||||
ListeningService, llm=llm, tts=tts, file_storage=firebase_instance, document_store=firestore
|
||||
)
|
||||
reading_service = providers.Factory(ReadingService, llm=llm)
|
||||
|
||||
speaking_service = providers.Factory(
|
||||
SpeakingService, llm=llm, vid_gen=vid_gen,
|
||||
file_storage=firebase_instance, document_store=firestore,
|
||||
stt=stt
|
||||
)
|
||||
|
||||
writing_service = providers.Factory(WritingService, llm=llm, ai_detector=ai_detector)
|
||||
|
||||
with open('app/services/impl/level/mc_variants.json', 'r') as file:
|
||||
mc_variants = json.load(file)
|
||||
|
||||
level_service = providers.Factory(
|
||||
LevelService, llm=llm, document_store=firestore, mc_variants=mc_variants, reading_service=reading_service,
|
||||
writing_service=writing_service, speaking_service=speaking_service, listening_service=listening_service
|
||||
)
|
||||
|
||||
grade_service = providers.Factory(
|
||||
GradeService, llm=llm
|
||||
)
|
||||
|
||||
embeddings = SentenceTransformer('all-MiniLM-L6-v2')
|
||||
|
||||
training_kb = providers.Factory(
|
||||
TrainingContentKnowledgeBase, embeddings=embeddings
|
||||
)
|
||||
|
||||
training_service = providers.Factory(
|
||||
TrainingService, llm=llm, firestore=firestore, training_kb=training_kb
|
||||
)
|
||||
|
||||
# Controllers
|
||||
|
||||
container.grade_controller = providers.Factory(
|
||||
GradeController, grade_service=grade_service, speaking_service=speaking_service, writing_service=writing_service
|
||||
)
|
||||
|
||||
container.training_controller = providers.Factory(
|
||||
TrainingController, training_service=training_service
|
||||
)
|
||||
|
||||
container.level_controller = providers.Factory(
|
||||
LevelController, level_service=level_service
|
||||
)
|
||||
container.listening_controller = providers.Factory(
|
||||
ListeningController, listening_service=listening_service
|
||||
)
|
||||
|
||||
container.reading_controller = providers.Factory(
|
||||
ReadingController, reading_service=reading_service
|
||||
)
|
||||
|
||||
container.speaking_controller = providers.Factory(
|
||||
SpeakingController, speaking_service=speaking_service
|
||||
)
|
||||
|
||||
container.writing_controller = providers.Factory(
|
||||
WritingController, writing_service=writing_service
|
||||
)
|
||||
|
||||
container.llm = llm
|
||||
|
||||
container.wire(
|
||||
packages=["app"]
|
||||
)
|
||||
import json
|
||||
import os
|
||||
|
||||
from dependency_injector import providers, containers
|
||||
from firebase_admin import credentials
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from openai import AsyncOpenAI
|
||||
from httpx import AsyncClient as HTTPClient
|
||||
from dotenv import load_dotenv
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
from app.repositories.impl import *
|
||||
from app.services.impl import *
|
||||
from app.controllers.impl import *
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class DependencyInjector:
|
||||
|
||||
def __init__(self, polly_client: any, http_client: HTTPClient, whisper_model: any):
|
||||
self._container = containers.DynamicContainer()
|
||||
self._polly_client = polly_client
|
||||
self._http_client = http_client
|
||||
self._whisper_model = whisper_model
|
||||
|
||||
def inject(self):
|
||||
self._setup_clients()
|
||||
self._setup_third_parties()
|
||||
self._setup_repositories()
|
||||
self._setup_services()
|
||||
self._setup_controllers()
|
||||
self._container.wire(
|
||||
packages=["app"]
|
||||
)
|
||||
|
||||
def _setup_clients(self):
|
||||
self._container.openai_client = providers.Singleton(AsyncOpenAI)
|
||||
self._container.polly_client = providers.Object(self._polly_client)
|
||||
self._container.http_client = providers.Object(self._http_client)
|
||||
self._container.whisper_model = providers.Object(self._whisper_model)
|
||||
|
||||
def _setup_third_parties(self):
|
||||
self._container.llm = providers.Factory(OpenAI, client=self._container.openai_client)
|
||||
self._container.stt = providers.Factory(OpenAIWhisper, model=self._container.whisper_model)
|
||||
self._container.tts = providers.Factory(AWSPolly, client=self._container.polly_client)
|
||||
self._container.vid_gen = providers.Factory(
|
||||
Heygen, client=self._container.http_client, heygen_token=os.getenv("HEY_GEN_TOKEN")
|
||||
)
|
||||
self._container.ai_detector = providers.Factory(
|
||||
GPTZero, client=self._container.http_client, gpt_zero_key=os.getenv("GPT_ZERO_API_KEY")
|
||||
)
|
||||
|
||||
def _setup_repositories(self):
|
||||
cred = credentials.Certificate(os.getenv("GOOGLE_APPLICATION_CREDENTIALS"))
|
||||
firebase_token = cred.get_access_token().access_token
|
||||
|
||||
self._container.document_store = providers.Object(
|
||||
AsyncIOMotorClient(os.getenv("MONGODB_URI"))[os.getenv("MONGODB_DB")]
|
||||
)
|
||||
|
||||
self._container.firebase_instance = providers.Factory(
|
||||
FirebaseStorage,
|
||||
client=self._container.http_client, token=firebase_token, bucket=os.getenv("FIREBASE_BUCKET")
|
||||
)
|
||||
|
||||
def _setup_services(self):
|
||||
self._container.listening_service = providers.Factory(
|
||||
ListeningService,
|
||||
llm=self._container.llm,
|
||||
tts=self._container.tts,
|
||||
file_storage=self._container.firebase_instance,
|
||||
document_store=self._container.document_store
|
||||
)
|
||||
self._container.reading_service = providers.Factory(ReadingService, llm=self._container.llm)
|
||||
|
||||
self._container.speaking_service = providers.Factory(
|
||||
SpeakingService, llm=self._container.llm, vid_gen=self._container.vid_gen,
|
||||
file_storage=self._container.firebase_instance, document_store=self._container.document_store,
|
||||
stt=self._container.stt
|
||||
)
|
||||
|
||||
self._container.writing_service = providers.Factory(
|
||||
WritingService, llm=self._container.llm, ai_detector=self._container.ai_detector
|
||||
)
|
||||
|
||||
with open('app/services/impl/exam/level/mc_variants.json', 'r') as file:
|
||||
mc_variants = json.load(file)
|
||||
|
||||
self._container.level_service = providers.Factory(
|
||||
LevelService, llm=self._container.llm, document_store=self._container.document_store,
|
||||
mc_variants=mc_variants, reading_service=self._container.reading_service,
|
||||
writing_service=self._container.writing_service, speaking_service=self._container.speaking_service,
|
||||
listening_service=self._container.listening_service
|
||||
)
|
||||
|
||||
self._container.grade_service = providers.Factory(
|
||||
GradeService, llm=self._container.llm
|
||||
)
|
||||
|
||||
embeddings = SentenceTransformer('all-MiniLM-L6-v2')
|
||||
|
||||
self._container.training_kb = providers.Factory(
|
||||
TrainingContentKnowledgeBase, embeddings=embeddings
|
||||
)
|
||||
|
||||
self._container.training_service = providers.Factory(
|
||||
TrainingService, llm=self._container.llm,
|
||||
firestore=self._container.document_store, training_kb=self._container.training_kb
|
||||
)
|
||||
|
||||
def _setup_controllers(self):
|
||||
self._container.grade_controller = providers.Factory(
|
||||
GradeController, grade_service=self._container.grade_service,
|
||||
speaking_service=self._container.speaking_service,
|
||||
writing_service=self._container.writing_service
|
||||
)
|
||||
|
||||
self._container.training_controller = providers.Factory(
|
||||
TrainingController, training_service=self._container.training_service
|
||||
)
|
||||
|
||||
self._container.level_controller = providers.Factory(
|
||||
LevelController, level_service=self._container.level_service
|
||||
)
|
||||
self._container.listening_controller = providers.Factory(
|
||||
ListeningController, listening_service=self._container.listening_service
|
||||
)
|
||||
|
||||
self._container.reading_controller = providers.Factory(
|
||||
ReadingController, reading_service=self._container.reading_service
|
||||
)
|
||||
|
||||
self._container.speaking_controller = providers.Factory(
|
||||
SpeakingController, speaking_service=self._container.speaking_service
|
||||
)
|
||||
|
||||
self._container.writing_controller = providers.Factory(
|
||||
WritingController, writing_service=self._container.writing_service
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .filters import ErrorAndAboveFilter
|
||||
from .queue_handler import QueueListenerHandler
|
||||
|
||||
__all__ = [
|
||||
"ErrorAndAboveFilter",
|
||||
"QueueListenerHandler"
|
||||
]
|
||||
from .filters import ErrorAndAboveFilter
|
||||
from .queue_handler import QueueListenerHandler
|
||||
|
||||
__all__ = [
|
||||
"ErrorAndAboveFilter",
|
||||
"QueueListenerHandler"
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
|
||||
class ErrorAndAboveFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord:
|
||||
return record.levelno < logging.ERROR
|
||||
import logging
|
||||
|
||||
|
||||
class ErrorAndAboveFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord:
|
||||
return record.levelno < logging.ERROR
|
||||
|
||||
@@ -1,105 +1,105 @@
|
||||
import datetime as dt
|
||||
import json
|
||||
import logging
|
||||
|
||||
LOG_RECORD_BUILTIN_ATTRS = {
|
||||
"args",
|
||||
"asctime",
|
||||
"created",
|
||||
"exc_info",
|
||||
"exc_text",
|
||||
"filename",
|
||||
"funcName",
|
||||
"levelname",
|
||||
"levelno",
|
||||
"lineno",
|
||||
"module",
|
||||
"msecs",
|
||||
"message",
|
||||
"msg",
|
||||
"name",
|
||||
"pathname",
|
||||
"process",
|
||||
"processName",
|
||||
"relativeCreated",
|
||||
"stack_info",
|
||||
"thread",
|
||||
"threadName",
|
||||
"taskName",
|
||||
}
|
||||
|
||||
"""
|
||||
This isn't being used since the app will be run on gcloud run but this can be used for future apps.
|
||||
If you want to test it:
|
||||
|
||||
formatters:
|
||||
|
||||
"json": {
|
||||
"()": "json_formatter.JSONFormatter",
|
||||
"fmt_keys": {
|
||||
"level": "levelname",
|
||||
"message": "message",
|
||||
"timestamp": "timestamp",
|
||||
"logger": "name",
|
||||
"module": "module",
|
||||
"function": "funcName",
|
||||
"line": "lineno",
|
||||
"thread_name": "threadName"
|
||||
}
|
||||
}
|
||||
|
||||
handlers:
|
||||
|
||||
"file_json": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "json",
|
||||
"filename": "logs/log",
|
||||
"maxBytes": 1000000,
|
||||
"backupCount": 3
|
||||
}
|
||||
|
||||
and add "cfg://handlers.file_json" to queue handler
|
||||
"""
|
||||
|
||||
# From this video https://www.youtube.com/watch?v=9L77QExPmI0
|
||||
# Src here: https://github.com/mCodingLLC/VideosSampleCode/blob/master/videos/135_modern_logging/mylogger.py
|
||||
class JSONFormatter(logging.Formatter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
fmt_keys: dict[str, str] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.fmt_keys = fmt_keys if fmt_keys is not None else {}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
message = self._prepare_log_dict(record)
|
||||
return json.dumps(message, default=str)
|
||||
|
||||
def _prepare_log_dict(self, record: logging.LogRecord):
|
||||
always_fields = {
|
||||
"message": record.getMessage(),
|
||||
"timestamp": dt.datetime.fromtimestamp(
|
||||
record.created, tz=dt.timezone.utc
|
||||
).isoformat(),
|
||||
}
|
||||
if record.exc_info is not None:
|
||||
always_fields["exc_info"] = self.formatException(record.exc_info)
|
||||
|
||||
if record.stack_info is not None:
|
||||
always_fields["stack_info"] = self.formatStack(record.stack_info)
|
||||
|
||||
message = {
|
||||
key: msg_val
|
||||
if (msg_val := always_fields.pop(val, None)) is not None
|
||||
else getattr(record, val)
|
||||
for key, val in self.fmt_keys.items()
|
||||
}
|
||||
message.update(always_fields)
|
||||
|
||||
for key, val in record.__dict__.items():
|
||||
if key not in LOG_RECORD_BUILTIN_ATTRS:
|
||||
message[key] = val
|
||||
|
||||
return message
|
||||
import datetime as dt
|
||||
import json
|
||||
import logging
|
||||
|
||||
LOG_RECORD_BUILTIN_ATTRS = {
|
||||
"args",
|
||||
"asctime",
|
||||
"created",
|
||||
"exc_info",
|
||||
"exc_text",
|
||||
"filename",
|
||||
"funcName",
|
||||
"levelname",
|
||||
"levelno",
|
||||
"lineno",
|
||||
"module",
|
||||
"msecs",
|
||||
"message",
|
||||
"msg",
|
||||
"name",
|
||||
"pathname",
|
||||
"process",
|
||||
"processName",
|
||||
"relativeCreated",
|
||||
"stack_info",
|
||||
"thread",
|
||||
"threadName",
|
||||
"taskName",
|
||||
}
|
||||
|
||||
"""
|
||||
This isn't being used since the app will be run on gcloud run but this can be used for future apps.
|
||||
If you want to test it:
|
||||
|
||||
formatters:
|
||||
|
||||
"json": {
|
||||
"()": "json_formatter.JSONFormatter",
|
||||
"fmt_keys": {
|
||||
"level": "levelname",
|
||||
"message": "message",
|
||||
"timestamp": "timestamp",
|
||||
"logger": "name",
|
||||
"module": "module",
|
||||
"function": "funcName",
|
||||
"line": "lineno",
|
||||
"thread_name": "threadName"
|
||||
}
|
||||
}
|
||||
|
||||
handlers:
|
||||
|
||||
"file_json": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "json",
|
||||
"filename": "logs/log",
|
||||
"maxBytes": 1000000,
|
||||
"backupCount": 3
|
||||
}
|
||||
|
||||
and add "cfg://handlers.file_json" to queue handler
|
||||
"""
|
||||
|
||||
# From this video https://www.youtube.com/watch?v=9L77QExPmI0
|
||||
# Src here: https://github.com/mCodingLLC/VideosSampleCode/blob/master/videos/135_modern_logging/mylogger.py
|
||||
class JSONFormatter(logging.Formatter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
fmt_keys: dict[str, str] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.fmt_keys = fmt_keys if fmt_keys is not None else {}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
message = self._prepare_log_dict(record)
|
||||
return json.dumps(message, default=str)
|
||||
|
||||
def _prepare_log_dict(self, record: logging.LogRecord):
|
||||
always_fields = {
|
||||
"message": record.getMessage(),
|
||||
"timestamp": dt.datetime.fromtimestamp(
|
||||
record.created, tz=dt.timezone.utc
|
||||
).isoformat(),
|
||||
}
|
||||
if record.exc_info is not None:
|
||||
always_fields["exc_info"] = self.formatException(record.exc_info)
|
||||
|
||||
if record.stack_info is not None:
|
||||
always_fields["stack_info"] = self.formatStack(record.stack_info)
|
||||
|
||||
message = {
|
||||
key: msg_val
|
||||
if (msg_val := always_fields.pop(val, None)) is not None
|
||||
else getattr(record, val)
|
||||
for key, val in self.fmt_keys.items()
|
||||
}
|
||||
message.update(always_fields)
|
||||
|
||||
for key, val in record.__dict__.items():
|
||||
if key not in LOG_RECORD_BUILTIN_ATTRS:
|
||||
message[key] = val
|
||||
|
||||
return message
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
{
|
||||
"version": 1,
|
||||
"objects": {
|
||||
"queue": {
|
||||
"class": "queue.Queue",
|
||||
"maxsize": 1000
|
||||
}
|
||||
},
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "[%(levelname)s] (%(module)s|L: %(lineno)d) %(asctime)s: %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"error_and_above": {
|
||||
"()": "app.configs.logging.ErrorAndAboveFilter"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "INFO",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["error_and_above"]
|
||||
},
|
||||
"error": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "ERROR",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"queue_handler": {
|
||||
"class": "app.configs.logging.QueueListenerHandler",
|
||||
"handlers": [
|
||||
"cfg://handlers.console",
|
||||
"cfg://handlers.error"
|
||||
],
|
||||
"queue": "cfg://objects.queue",
|
||||
"respect_handler_level": true
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": [
|
||||
"queue_handler"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"version": 1,
|
||||
"objects": {
|
||||
"queue": {
|
||||
"class": "queue.Queue",
|
||||
"maxsize": 1000
|
||||
}
|
||||
},
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "[%(levelname)s] (%(module)s|L: %(lineno)d) %(asctime)s: %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"error_and_above": {
|
||||
"()": "app.configs.logging.ErrorAndAboveFilter"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "INFO",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["error_and_above"]
|
||||
},
|
||||
"error": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "ERROR",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"queue_handler": {
|
||||
"class": "app.configs.logging.QueueListenerHandler",
|
||||
"handlers": [
|
||||
"cfg://handlers.console",
|
||||
"cfg://handlers.error"
|
||||
],
|
||||
"queue": "cfg://objects.queue",
|
||||
"respect_handler_level": true
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": [
|
||||
"queue_handler"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
from logging.config import ConvertingList, ConvertingDict, valid_ident
|
||||
from logging.handlers import QueueHandler, QueueListener
|
||||
from queue import Queue
|
||||
import atexit
|
||||
|
||||
|
||||
class QueueHnadlerHelper:
|
||||
|
||||
@staticmethod
|
||||
def resolve_handlers(l):
|
||||
if not isinstance(l, ConvertingList):
|
||||
return l
|
||||
|
||||
# Indexing the list performs the evaluation.
|
||||
return [l[i] for i in range(len(l))]
|
||||
|
||||
@staticmethod
|
||||
def resolve_queue(q):
|
||||
if not isinstance(q, ConvertingDict):
|
||||
return q
|
||||
if '__resolved_value__' in q:
|
||||
return q['__resolved_value__']
|
||||
|
||||
cname = q.pop('class')
|
||||
klass = q.configurator.resolve(cname)
|
||||
props = q.pop('.', None)
|
||||
kwargs = {k: q[k] for k in q if valid_ident(k)}
|
||||
result = klass(**kwargs)
|
||||
if props:
|
||||
for name, value in props.items():
|
||||
setattr(result, name, value)
|
||||
|
||||
q['__resolved_value__'] = result
|
||||
return result
|
||||
|
||||
|
||||
# The guy from this video https://www.youtube.com/watch?v=9L77QExPmI0 is using logging features only available in 3.12
|
||||
# This article had the class required to build the queue handler in 3.11
|
||||
# https://rob-blackbourn.medium.com/how-to-use-python-logging-queuehandler-with-dictconfig-1e8b1284e27a
|
||||
class QueueListenerHandler(QueueHandler):
|
||||
|
||||
def __init__(self, handlers, respect_handler_level=False, auto_run=True, queue=Queue(-1)):
|
||||
queue = QueueHnadlerHelper.resolve_queue(queue)
|
||||
super().__init__(queue)
|
||||
handlers = QueueHnadlerHelper.resolve_handlers(handlers)
|
||||
self._listener = QueueListener(
|
||||
self.queue,
|
||||
*handlers,
|
||||
respect_handler_level=respect_handler_level)
|
||||
if auto_run:
|
||||
self.start()
|
||||
atexit.register(self.stop)
|
||||
|
||||
def start(self):
|
||||
self._listener.start()
|
||||
|
||||
def stop(self):
|
||||
self._listener.stop()
|
||||
|
||||
def emit(self, record):
|
||||
return super().emit(record)
|
||||
from logging.config import ConvertingList, ConvertingDict, valid_ident
|
||||
from logging.handlers import QueueHandler, QueueListener
|
||||
from queue import Queue
|
||||
import atexit
|
||||
|
||||
|
||||
class QueueHnadlerHelper:
|
||||
|
||||
@staticmethod
|
||||
def resolve_handlers(l):
|
||||
if not isinstance(l, ConvertingList):
|
||||
return l
|
||||
|
||||
# Indexing the list performs the evaluation.
|
||||
return [l[i] for i in range(len(l))]
|
||||
|
||||
@staticmethod
|
||||
def resolve_queue(q):
|
||||
if not isinstance(q, ConvertingDict):
|
||||
return q
|
||||
if '__resolved_value__' in q:
|
||||
return q['__resolved_value__']
|
||||
|
||||
cname = q.pop('class')
|
||||
klass = q.configurator.resolve(cname)
|
||||
props = q.pop('.', None)
|
||||
kwargs = {k: q[k] for k in q if valid_ident(k)}
|
||||
result = klass(**kwargs)
|
||||
if props:
|
||||
for name, value in props.items():
|
||||
setattr(result, name, value)
|
||||
|
||||
q['__resolved_value__'] = result
|
||||
return result
|
||||
|
||||
|
||||
# The guy from this video https://www.youtube.com/watch?v=9L77QExPmI0 is using logging features only available in 3.12
|
||||
# This article had the class required to build the queue handler in 3.11
|
||||
# https://rob-blackbourn.medium.com/how-to-use-python-logging-queuehandler-with-dictconfig-1e8b1284e27a
|
||||
class QueueListenerHandler(QueueHandler):
|
||||
|
||||
def __init__(self, handlers, respect_handler_level=False, auto_run=True, queue=Queue(-1)):
|
||||
queue = QueueHnadlerHelper.resolve_queue(queue)
|
||||
super().__init__(queue)
|
||||
handlers = QueueHnadlerHelper.resolve_handlers(handlers)
|
||||
self._listener = QueueListener(
|
||||
self.queue,
|
||||
*handlers,
|
||||
respect_handler_level=respect_handler_level)
|
||||
if auto_run:
|
||||
self.start()
|
||||
atexit.register(self.stop)
|
||||
|
||||
def start(self):
|
||||
self._listener.start()
|
||||
|
||||
def stop(self):
|
||||
self._listener.stop()
|
||||
|
||||
def emit(self, record):
|
||||
return super().emit(record)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,19 @@
|
||||
from .level import ILevelController
|
||||
from .listening import IListeningController
|
||||
from .reading import IReadingController
|
||||
from .writing import IWritingController
|
||||
from .speaking import ISpeakingController
|
||||
from .grade import IGradeController
|
||||
from .training import ITrainingController
|
||||
|
||||
__all__ = [
|
||||
"IListeningController",
|
||||
"IReadingController",
|
||||
"IWritingController",
|
||||
"ISpeakingController",
|
||||
"ILevelController",
|
||||
"IGradeController",
|
||||
"ITrainingController"
|
||||
]
|
||||
from .level import ILevelController
|
||||
from .listening import IListeningController
|
||||
from .reading import IReadingController
|
||||
from .writing import IWritingController
|
||||
from .speaking import ISpeakingController
|
||||
from .grade import IGradeController
|
||||
from .training import ITrainingController
|
||||
from .user import IUserController
|
||||
|
||||
__all__ = [
|
||||
"IListeningController",
|
||||
"IReadingController",
|
||||
"IWritingController",
|
||||
"ISpeakingController",
|
||||
"ILevelController",
|
||||
"IGradeController",
|
||||
"ITrainingController",
|
||||
"IUserController"
|
||||
]
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, data):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grading_summary(self, data: Dict):
|
||||
pass
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, data):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grading_summary(self, data: Dict):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ILevelController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, file: UploadFile):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ILevelController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, file: UploadFile):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
|
||||
class IListeningController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_listening_question(self, section_id: int, topic: str, exercises: List[str], difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, data):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
|
||||
class IListeningController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_listening_question(self, section_id: int, topic: str, exercises: List[str], difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, data):
|
||||
pass
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
|
||||
class IReadingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_reading_passage(self, passage: int, topic: str, exercises: List[str], difficulty: str):
|
||||
pass
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
|
||||
class IReadingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_reading_passage(self, passage: int, topic: str, exercises: List[str], difficulty: str):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
|
||||
class ISpeakingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(self, task: int, topic: str, difficulty: str, second_topic: Optional[str] = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_speaking(self, data, background_tasks: BackgroundTasks):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(
|
||||
self, part: int, avatar: str, topic: str, questions: list[str],
|
||||
*,
|
||||
second_topic: Optional[str] = None,
|
||||
prompts: Optional[list[str]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
|
||||
class ISpeakingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(self, task: int, topic: str, difficulty: str, second_topic: Optional[str] = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_speaking(self, data, background_tasks: BackgroundTasks):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(
|
||||
self, part: int, avatar: str, topic: str, questions: list[str],
|
||||
*,
|
||||
second_topic: Optional[str] = None,
|
||||
prompts: Optional[list[str]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ITrainingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, data):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, data):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ITrainingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, data):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, data):
|
||||
pass
|
||||
|
||||
10
app/controllers/abc/user.py
Normal file
10
app/controllers/abc/user.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.dtos.user_batch import BatchUsersDTO
|
||||
|
||||
|
||||
class IUserController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def batch_import(self, batch: BatchUsersDTO):
|
||||
pass
|
||||
@@ -1,8 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IWritingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IWritingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from .level import LevelController
|
||||
from .listening import ListeningController
|
||||
from .reading import ReadingController
|
||||
from .speaking import SpeakingController
|
||||
from .writing import WritingController
|
||||
from .training import TrainingController
|
||||
from .grade import GradeController
|
||||
|
||||
__all__ = [
|
||||
"LevelController",
|
||||
"ListeningController",
|
||||
"ReadingController",
|
||||
"SpeakingController",
|
||||
"WritingController",
|
||||
"TrainingController",
|
||||
"GradeController"
|
||||
]
|
||||
from .level import LevelController
|
||||
from .listening import ListeningController
|
||||
from .reading import ReadingController
|
||||
from .speaking import SpeakingController
|
||||
from .writing import WritingController
|
||||
from .training import TrainingController
|
||||
from .grade import GradeController
|
||||
from .user import UserController
|
||||
|
||||
__all__ = [
|
||||
"LevelController",
|
||||
"ListeningController",
|
||||
"ReadingController",
|
||||
"SpeakingController",
|
||||
"WritingController",
|
||||
"TrainingController",
|
||||
"GradeController",
|
||||
"UserController"
|
||||
]
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from app.configs.constants import FilePaths
|
||||
from app.controllers.abc import IGradeController
|
||||
from app.dtos.writing import WritingGradeTaskDTO
|
||||
from app.helpers import FileHelper
|
||||
from app.services.abc import ISpeakingService, IWritingService, IGradeService
|
||||
from app.utils import handle_exception
|
||||
|
||||
|
||||
class GradeController(IGradeController):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grade_service: IGradeService,
|
||||
speaking_service: ISpeakingService,
|
||||
writing_service: IWritingService
|
||||
):
|
||||
self._service = grade_service
|
||||
self._speaking_service = speaking_service
|
||||
self._writing_service = writing_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def grade_writing_task(self, task: int, data: WritingGradeTaskDTO):
|
||||
return await self._writing_service.grade_writing_task(task, data.question, data.answer)
|
||||
|
||||
@handle_exception(400)
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
FileHelper.delete_files_older_than_one_day(FilePaths.AUDIO_FILES_PATH)
|
||||
return await self._speaking_service.grade_speaking_task(task, answers)
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
return await self._service.grade_short_answers(data)
|
||||
|
||||
async def grading_summary(self, data: Dict):
|
||||
section_keys = ['reading', 'listening', 'writing', 'speaking', 'level']
|
||||
extracted_sections = self._extract_existing_sections_from_body(data, section_keys)
|
||||
return await self._service.calculate_grading_summary(extracted_sections)
|
||||
|
||||
@staticmethod
|
||||
def _extract_existing_sections_from_body(my_dict, keys_to_extract):
|
||||
if 'sections' in my_dict and isinstance(my_dict['sections'], list) and len(my_dict['sections']) > 0:
|
||||
return list(
|
||||
filter(
|
||||
lambda item:
|
||||
'code' in item and
|
||||
item['code'] in keys_to_extract and
|
||||
'grade' in item and
|
||||
'name' in item,
|
||||
my_dict['sections']
|
||||
)
|
||||
)
|
||||
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from app.configs.constants import FilePaths
|
||||
from app.controllers.abc import IGradeController
|
||||
from app.dtos.writing import WritingGradeTaskDTO
|
||||
from app.helpers import FileHelper
|
||||
from app.services.abc import ISpeakingService, IWritingService, IGradeService
|
||||
from app.utils import handle_exception
|
||||
|
||||
|
||||
class GradeController(IGradeController):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grade_service: IGradeService,
|
||||
speaking_service: ISpeakingService,
|
||||
writing_service: IWritingService
|
||||
):
|
||||
self._service = grade_service
|
||||
self._speaking_service = speaking_service
|
||||
self._writing_service = writing_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def grade_writing_task(self, task: int, data: WritingGradeTaskDTO):
|
||||
return await self._writing_service.grade_writing_task(task, data.question, data.answer)
|
||||
|
||||
@handle_exception(400)
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
FileHelper.delete_files_older_than_one_day(FilePaths.AUDIO_FILES_PATH)
|
||||
return await self._speaking_service.grade_speaking_task(task, answers)
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
return await self._service.grade_short_answers(data)
|
||||
|
||||
async def grading_summary(self, data: Dict):
|
||||
section_keys = ['reading', 'listening', 'writing', 'speaking', 'level']
|
||||
extracted_sections = self._extract_existing_sections_from_body(data, section_keys)
|
||||
return await self._service.calculate_grading_summary(extracted_sections)
|
||||
|
||||
@staticmethod
|
||||
def _extract_existing_sections_from_body(my_dict, keys_to_extract):
|
||||
if 'sections' in my_dict and isinstance(my_dict['sections'], list) and len(my_dict['sections']) > 0:
|
||||
return list(
|
||||
filter(
|
||||
lambda item:
|
||||
'code' in item and
|
||||
item['code'] in keys_to_extract and
|
||||
'grade' in item and
|
||||
'name' in item,
|
||||
my_dict['sections']
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict
|
||||
|
||||
from app.controllers.abc import ILevelController
|
||||
from app.services.abc import ILevelService
|
||||
|
||||
|
||||
class LevelController(ILevelController):
|
||||
|
||||
def __init__(self, level_service: ILevelService):
|
||||
self._service = level_service
|
||||
|
||||
async def get_level_exam(self):
|
||||
return await self._service.get_level_exam()
|
||||
|
||||
async def get_level_utas(self):
|
||||
return await self._service.get_level_utas()
|
||||
|
||||
async def upload_level(self, file: UploadFile):
|
||||
return await self._service.upload_level(file)
|
||||
|
||||
async def get_custom_level(self, data: Dict):
|
||||
return await self._service.get_custom_level(data)
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict
|
||||
|
||||
from app.controllers.abc import ILevelController
|
||||
from app.services.abc import ILevelService
|
||||
|
||||
|
||||
class LevelController(ILevelController):
|
||||
|
||||
def __init__(self, level_service: ILevelService):
|
||||
self._service = level_service
|
||||
|
||||
async def get_level_exam(self):
|
||||
return await self._service.get_level_exam()
|
||||
|
||||
async def get_level_utas(self):
|
||||
return await self._service.get_level_utas()
|
||||
|
||||
async def upload_level(self, file: UploadFile):
|
||||
return await self._service.upload_level(file)
|
||||
|
||||
async def get_custom_level(self, data: Dict):
|
||||
return await self._service.get_custom_level(data)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
from typing import List
|
||||
|
||||
from app.controllers.abc import IListeningController
|
||||
from app.dtos.listening import SaveListeningDTO
|
||||
from app.services.abc import IListeningService
|
||||
|
||||
|
||||
class ListeningController(IListeningController):
|
||||
|
||||
def __init__(self, listening_service: IListeningService):
|
||||
self._service = listening_service
|
||||
|
||||
async def get_listening_question(
|
||||
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str
|
||||
):
|
||||
return await self._service.get_listening_question(section_id, topic, req_exercises, difficulty)
|
||||
|
||||
async def save_listening(self, data: SaveListeningDTO):
|
||||
return await self._service.save_listening(data.parts, data.minTimer, data.difficulty, data.id)
|
||||
from typing import List
|
||||
|
||||
from app.controllers.abc import IListeningController
|
||||
from app.dtos.listening import SaveListeningDTO
|
||||
from app.services.abc import IListeningService
|
||||
|
||||
|
||||
class ListeningController(IListeningController):
|
||||
|
||||
def __init__(self, listening_service: IListeningService):
|
||||
self._service = listening_service
|
||||
|
||||
async def get_listening_question(
|
||||
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str
|
||||
):
|
||||
return await self._service.get_listening_question(section_id, topic, req_exercises, difficulty)
|
||||
|
||||
async def save_listening(self, data: SaveListeningDTO):
|
||||
return await self._service.save_listening(data.parts, data.minTimer, data.difficulty, data.id)
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import random
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from app.controllers.abc import IReadingController
|
||||
from app.services.abc import IReadingService
|
||||
from app.configs.constants import FieldsAndExercises
|
||||
from app.helpers import ExercisesHelper
|
||||
|
||||
|
||||
class ReadingController(IReadingController):
|
||||
|
||||
def __init__(self, reading_service: IReadingService):
|
||||
self._service = reading_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._passages = {
|
||||
"passage_1": {
|
||||
"start_id": 1,
|
||||
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_1_EXERCISES
|
||||
},
|
||||
"passage_2": {
|
||||
"start_id": 14,
|
||||
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_2_EXERCISES
|
||||
},
|
||||
"passage_3": {
|
||||
"start_id": 27,
|
||||
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_3_EXERCISES
|
||||
}
|
||||
}
|
||||
|
||||
async def get_reading_passage(self, passage_id: int, topic: str, req_exercises: List[str], difficulty: str):
|
||||
passage = self._passages[f'passage_{str(passage_id)}']
|
||||
|
||||
if len(req_exercises) == 0:
|
||||
req_exercises = random.sample(FieldsAndExercises.READING_EXERCISE_TYPES, 2)
|
||||
|
||||
number_of_exercises_q = ExercisesHelper.divide_number_into_parts(
|
||||
passage["total_exercises"], len(req_exercises)
|
||||
)
|
||||
|
||||
return await self._service.gen_reading_passage(
|
||||
passage_id, topic, req_exercises, number_of_exercises_q, difficulty, passage["start_id"]
|
||||
)
|
||||
import random
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from app.controllers.abc import IReadingController
|
||||
from app.services.abc import IReadingService
|
||||
from app.configs.constants import FieldsAndExercises
|
||||
from app.helpers import ExercisesHelper
|
||||
|
||||
|
||||
class ReadingController(IReadingController):
|
||||
|
||||
def __init__(self, reading_service: IReadingService):
|
||||
self._service = reading_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._passages = {
|
||||
"passage_1": {
|
||||
"start_id": 1,
|
||||
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_1_EXERCISES
|
||||
},
|
||||
"passage_2": {
|
||||
"start_id": 14,
|
||||
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_2_EXERCISES
|
||||
},
|
||||
"passage_3": {
|
||||
"start_id": 27,
|
||||
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_3_EXERCISES
|
||||
}
|
||||
}
|
||||
|
||||
async def get_reading_passage(self, passage_id: int, topic: str, req_exercises: List[str], difficulty: str):
|
||||
passage = self._passages[f'passage_{str(passage_id)}']
|
||||
|
||||
if len(req_exercises) == 0:
|
||||
req_exercises = random.sample(FieldsAndExercises.READING_EXERCISE_TYPES, 2)
|
||||
|
||||
number_of_exercises_q = ExercisesHelper.divide_number_into_parts(
|
||||
passage["total_exercises"], len(req_exercises)
|
||||
)
|
||||
|
||||
return await self._service.gen_reading_passage(
|
||||
passage_id, topic, req_exercises, number_of_exercises_q, difficulty, passage["start_id"]
|
||||
)
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
from app.controllers.abc import ISpeakingController
|
||||
from app.dtos.speaking import SaveSpeakingDTO
|
||||
|
||||
from app.services.abc import ISpeakingService
|
||||
from app.configs.constants import ExamVariant, MinTimers
|
||||
from app.configs.question_templates import getSpeakingTemplate
|
||||
|
||||
|
||||
class SpeakingController(ISpeakingController):
|
||||
|
||||
def __init__(self, speaking_service: ISpeakingService):
|
||||
self._service = speaking_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def get_speaking_part(self, task: int, topic: str, difficulty: str, second_topic: Optional[str] = None):
|
||||
return await self._service.get_speaking_part(task, topic, difficulty, second_topic)
|
||||
|
||||
async def save_speaking(self, data: SaveSpeakingDTO, background_tasks: BackgroundTasks):
|
||||
exercises = data.exercises
|
||||
min_timer = data.minTimer
|
||||
|
||||
template = getSpeakingTemplate()
|
||||
template["minTimer"] = min_timer
|
||||
|
||||
if min_timer < MinTimers.SPEAKING_MIN_TIMER_DEFAULT:
|
||||
template["variant"] = ExamVariant.PARTIAL.value
|
||||
else:
|
||||
template["variant"] = ExamVariant.FULL.value
|
||||
|
||||
req_id = str(uuid.uuid4())
|
||||
self._logger.info(f'Received request to save speaking with id: {req_id}')
|
||||
|
||||
background_tasks.add_task(self._service.create_videos_and_save_to_db, exercises, template, req_id)
|
||||
|
||||
self._logger.info('Started background task to save speaking.')
|
||||
|
||||
# Return response without waiting for create_videos_and_save_to_db to finish
|
||||
return {**template, "id": req_id}
|
||||
|
||||
async def generate_video(self, *args, **kwargs):
|
||||
return await self._service.generate_video(*args, **kwargs)
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
from app.controllers.abc import ISpeakingController
|
||||
from app.dtos.speaking import SaveSpeakingDTO
|
||||
|
||||
from app.services.abc import ISpeakingService
|
||||
from app.configs.constants import ExamVariant, MinTimers
|
||||
from app.configs.question_templates import getSpeakingTemplate
|
||||
|
||||
|
||||
class SpeakingController(ISpeakingController):
|
||||
|
||||
def __init__(self, speaking_service: ISpeakingService):
|
||||
self._service = speaking_service
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def get_speaking_part(self, task: int, topic: str, difficulty: str, second_topic: Optional[str] = None):
|
||||
return await self._service.get_speaking_part(task, topic, difficulty, second_topic)
|
||||
|
||||
async def save_speaking(self, data: SaveSpeakingDTO, background_tasks: BackgroundTasks):
|
||||
exercises = data.exercises
|
||||
min_timer = data.minTimer
|
||||
|
||||
template = getSpeakingTemplate()
|
||||
template["minTimer"] = min_timer
|
||||
|
||||
if min_timer < MinTimers.SPEAKING_MIN_TIMER_DEFAULT:
|
||||
template["variant"] = ExamVariant.PARTIAL.value
|
||||
else:
|
||||
template["variant"] = ExamVariant.FULL.value
|
||||
|
||||
req_id = str(uuid.uuid4())
|
||||
self._logger.info(f'Received request to save speaking with id: {req_id}')
|
||||
|
||||
background_tasks.add_task(self._service.create_videos_and_save_to_db, exercises, template, req_id)
|
||||
|
||||
self._logger.info('Started background task to save speaking.')
|
||||
|
||||
# Return response without waiting for create_videos_and_save_to_db to finish
|
||||
return {**template, "id": req_id}
|
||||
|
||||
async def generate_video(self, *args, **kwargs):
|
||||
return await self._service.generate_video(*args, **kwargs)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from typing import Dict
|
||||
|
||||
from app.controllers.abc import ITrainingController
|
||||
from app.dtos.training import FetchTipsDTO
|
||||
from app.services.abc import ITrainingService
|
||||
|
||||
|
||||
class TrainingController(ITrainingController):
|
||||
|
||||
def __init__(self, training_service: ITrainingService):
|
||||
self._service = training_service
|
||||
|
||||
async def fetch_tips(self, data: FetchTipsDTO):
|
||||
return await self._service.fetch_tips(data.context, data.question, data.answer, data.correct_answer)
|
||||
|
||||
async def get_training_content(self, data: Dict):
|
||||
return await self._service.get_training_content(data)
|
||||
from typing import Dict
|
||||
|
||||
from app.controllers.abc import ITrainingController
|
||||
from app.dtos.training import FetchTipsDTO
|
||||
from app.services.abc import ITrainingService
|
||||
|
||||
|
||||
class TrainingController(ITrainingController):
|
||||
|
||||
def __init__(self, training_service: ITrainingService):
|
||||
self._service = training_service
|
||||
|
||||
async def fetch_tips(self, data: FetchTipsDTO):
|
||||
return await self._service.fetch_tips(data.context, data.question, data.answer, data.correct_answer)
|
||||
|
||||
async def get_training_content(self, data: Dict):
|
||||
return await self._service.get_training_content(data)
|
||||
|
||||
12
app/controllers/impl/user.py
Normal file
12
app/controllers/impl/user.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from app.controllers.abc import IUserController
|
||||
from app.dtos.user_batch import BatchUsersDTO
|
||||
from app.services.abc import IUserService
|
||||
|
||||
|
||||
class UserController(IUserController):
|
||||
|
||||
def __init__(self, user_service: IUserService):
|
||||
self._service = user_service
|
||||
|
||||
async def batch_import(self, batch: BatchUsersDTO):
|
||||
return await self._service.fetch_tips(batch)
|
||||
@@ -1,11 +1,11 @@
|
||||
from app.controllers.abc import IWritingController
|
||||
from app.services.abc import IWritingService
|
||||
|
||||
|
||||
class WritingController(IWritingController):
|
||||
|
||||
def __init__(self, writing_service: IWritingService):
|
||||
self._service = writing_service
|
||||
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
return await self._service.get_writing_task_general_question(task, topic, difficulty)
|
||||
from app.controllers.abc import IWritingController
|
||||
from app.services.abc import IWritingService
|
||||
|
||||
|
||||
class WritingController(IWritingController):
|
||||
|
||||
def __init__(self, writing_service: IWritingService):
|
||||
self._service = writing_service
|
||||
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
return await self._service.get_writing_task_general_question(task, topic, difficulty)
|
||||
|
||||
114
app/dtos/exam.py
114
app/dtos/exam.py
@@ -1,57 +1,57 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Dict, Union, Optional
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
|
||||
class Option(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MultipleChoiceQuestion(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
variant: str = "text"
|
||||
solution: str
|
||||
options: List[Option]
|
||||
|
||||
|
||||
class MultipleChoiceExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "multipleChoice"
|
||||
prompt: str = "Select the appropriate option."
|
||||
questions: List[MultipleChoiceQuestion]
|
||||
userSolutions: List = Field(default_factory=list)
|
||||
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
id: str
|
||||
options: Dict[str, str]
|
||||
|
||||
|
||||
class FillBlanksSolution(BaseModel):
|
||||
id: str
|
||||
solution: str
|
||||
|
||||
|
||||
class FillBlanksExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "fillBlanks"
|
||||
variant: str = "mc"
|
||||
prompt: str = "Click a blank to select the appropriate word for it."
|
||||
text: str
|
||||
solutions: List[FillBlanksSolution]
|
||||
words: List[FillBlanksWord]
|
||||
userSolutions: List = Field(default_factory=list)
|
||||
|
||||
|
||||
Exercise = Union[MultipleChoiceExercise, FillBlanksExercise]
|
||||
|
||||
|
||||
class Part(BaseModel):
|
||||
exercises: List[Exercise]
|
||||
context: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class Exam(BaseModel):
|
||||
parts: List[Part]
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Dict, Union, Optional
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
|
||||
class Option(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MultipleChoiceQuestion(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
variant: str = "text"
|
||||
solution: str
|
||||
options: List[Option]
|
||||
|
||||
|
||||
class MultipleChoiceExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "multipleChoice"
|
||||
prompt: str = "Select the appropriate option."
|
||||
questions: List[MultipleChoiceQuestion]
|
||||
userSolutions: List = Field(default_factory=list)
|
||||
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
id: str
|
||||
options: Dict[str, str]
|
||||
|
||||
|
||||
class FillBlanksSolution(BaseModel):
|
||||
id: str
|
||||
solution: str
|
||||
|
||||
|
||||
class FillBlanksExercise(BaseModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
type: str = "fillBlanks"
|
||||
variant: str = "mc"
|
||||
prompt: str = "Click a blank to select the appropriate word for it."
|
||||
text: str
|
||||
solutions: List[FillBlanksSolution]
|
||||
words: List[FillBlanksWord]
|
||||
userSolutions: List = Field(default_factory=list)
|
||||
|
||||
|
||||
Exercise = Union[MultipleChoiceExercise, FillBlanksExercise]
|
||||
|
||||
|
||||
class Part(BaseModel):
|
||||
exercises: List[Exercise]
|
||||
context: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class Exam(BaseModel):
|
||||
parts: List[Part]
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import random
|
||||
import uuid
|
||||
from typing import List, Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.configs.constants import MinTimers, EducationalContent
|
||||
|
||||
|
||||
class SaveListeningDTO(BaseModel):
|
||||
parts: List[Dict]
|
||||
minTimer: int = MinTimers.LISTENING_MIN_TIMER_DEFAULT
|
||||
difficulty: str = random.choice(EducationalContent.DIFFICULTIES)
|
||||
id: str = str(uuid.uuid4())
|
||||
import random
|
||||
import uuid
|
||||
from typing import List, Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.configs.constants import MinTimers, EducationalContent
|
||||
|
||||
|
||||
class SaveListeningDTO(BaseModel):
|
||||
parts: List[Dict]
|
||||
minTimer: int = MinTimers.LISTENING_MIN_TIMER_DEFAULT
|
||||
difficulty: str = random.choice(EducationalContent.DIFFICULTIES)
|
||||
id: str = str(uuid.uuid4())
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Union, Any, Optional
|
||||
|
||||
|
||||
class Option(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MultipleChoiceQuestion(BaseModel):
|
||||
type: str = "multipleChoice"
|
||||
id: str
|
||||
prompt: str
|
||||
variant: str = "text"
|
||||
options: List[Option]
|
||||
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
type: str = "fillBlanks"
|
||||
id: str
|
||||
options: Dict[str, str]
|
||||
|
||||
|
||||
Component = Union[MultipleChoiceQuestion, FillBlanksWord, Dict[str, Any]]
|
||||
|
||||
|
||||
class Sheet(BaseModel):
|
||||
batch: Optional[int] = None
|
||||
components: List[Component]
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Union, Any, Optional
|
||||
|
||||
|
||||
class Option(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
|
||||
|
||||
class MultipleChoiceQuestion(BaseModel):
|
||||
type: str = "multipleChoice"
|
||||
id: str
|
||||
prompt: str
|
||||
variant: str = "text"
|
||||
options: List[Option]
|
||||
|
||||
|
||||
class FillBlanksWord(BaseModel):
|
||||
type: str = "fillBlanks"
|
||||
id: str
|
||||
options: Dict[str, str]
|
||||
|
||||
|
||||
Component = Union[MultipleChoiceQuestion, FillBlanksWord, Dict[str, Any]]
|
||||
|
||||
|
||||
class Sheet(BaseModel):
|
||||
batch: Optional[int] = None
|
||||
components: List[Component]
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import random
|
||||
from typing import List, Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.configs.constants import MinTimers, AvatarEnum
|
||||
|
||||
|
||||
class SaveSpeakingDTO(BaseModel):
|
||||
exercises: List[Dict]
|
||||
minTimer: int = MinTimers.SPEAKING_MIN_TIMER_DEFAULT
|
||||
|
||||
|
||||
class GradeSpeakingDTO(BaseModel):
|
||||
question: str
|
||||
answer: str
|
||||
|
||||
|
||||
class GradeSpeakingAnswersDTO(BaseModel):
|
||||
answers: List[Dict]
|
||||
|
||||
|
||||
class GenerateVideo1DTO(BaseModel):
|
||||
avatar: str = (random.choice(list(AvatarEnum))).value
|
||||
questions: List[str]
|
||||
first_topic: str
|
||||
second_topic: str
|
||||
|
||||
|
||||
class GenerateVideo2DTO(BaseModel):
|
||||
avatar: str = (random.choice(list(AvatarEnum))).value
|
||||
prompts: List[str] = []
|
||||
suffix: str = ""
|
||||
question: str
|
||||
topic: str
|
||||
|
||||
|
||||
class GenerateVideo3DTO(BaseModel):
|
||||
avatar: str = (random.choice(list(AvatarEnum))).value
|
||||
questions: List[str]
|
||||
topic: str
|
||||
|
||||
import random
|
||||
from typing import List, Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.configs.constants import MinTimers, AvatarEnum
|
||||
|
||||
|
||||
class SaveSpeakingDTO(BaseModel):
|
||||
exercises: List[Dict]
|
||||
minTimer: int = MinTimers.SPEAKING_MIN_TIMER_DEFAULT
|
||||
|
||||
|
||||
class GradeSpeakingDTO(BaseModel):
|
||||
question: str
|
||||
answer: str
|
||||
|
||||
|
||||
class GradeSpeakingAnswersDTO(BaseModel):
|
||||
answers: List[Dict]
|
||||
|
||||
|
||||
class GenerateVideo1DTO(BaseModel):
|
||||
avatar: str = (random.choice(list(AvatarEnum))).value
|
||||
questions: List[str]
|
||||
first_topic: str
|
||||
second_topic: str
|
||||
|
||||
|
||||
class GenerateVideo2DTO(BaseModel):
|
||||
avatar: str = (random.choice(list(AvatarEnum))).value
|
||||
prompts: List[str] = []
|
||||
suffix: str = ""
|
||||
question: str
|
||||
topic: str
|
||||
|
||||
|
||||
class GenerateVideo3DTO(BaseModel):
|
||||
avatar: str = (random.choice(list(AvatarEnum))).value
|
||||
questions: List[str]
|
||||
topic: str
|
||||
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
|
||||
class FetchTipsDTO(BaseModel):
|
||||
context: str
|
||||
question: str
|
||||
answer: str
|
||||
correct_answer: str
|
||||
|
||||
|
||||
class QueryDTO(BaseModel):
|
||||
category: str
|
||||
text: str
|
||||
|
||||
|
||||
class DetailsDTO(BaseModel):
|
||||
exam_id: str
|
||||
date: int
|
||||
performance_comment: str
|
||||
detailed_summary: str
|
||||
|
||||
|
||||
class WeakAreaDTO(BaseModel):
|
||||
area: str
|
||||
comment: str
|
||||
|
||||
|
||||
class TrainingContentDTO(BaseModel):
|
||||
details: List[DetailsDTO]
|
||||
weak_areas: List[WeakAreaDTO]
|
||||
queries: List[QueryDTO]
|
||||
|
||||
|
||||
class TipsDTO(BaseModel):
|
||||
tip_ids: List[str]
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
|
||||
class FetchTipsDTO(BaseModel):
|
||||
context: str
|
||||
question: str
|
||||
answer: str
|
||||
correct_answer: str
|
||||
|
||||
|
||||
class QueryDTO(BaseModel):
|
||||
category: str
|
||||
text: str
|
||||
|
||||
|
||||
class DetailsDTO(BaseModel):
|
||||
exam_id: str
|
||||
date: int
|
||||
performance_comment: str
|
||||
detailed_summary: str
|
||||
|
||||
|
||||
class WeakAreaDTO(BaseModel):
|
||||
area: str
|
||||
comment: str
|
||||
|
||||
|
||||
class TrainingContentDTO(BaseModel):
|
||||
details: List[DetailsDTO]
|
||||
weak_areas: List[WeakAreaDTO]
|
||||
queries: List[QueryDTO]
|
||||
|
||||
|
||||
class TipsDTO(BaseModel):
|
||||
tip_ids: List[str]
|
||||
|
||||
|
||||
30
app/dtos/user_batch.py
Normal file
30
app/dtos/user_batch.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DemographicInfo(BaseModel):
|
||||
phone: str
|
||||
passport_id: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
|
||||
|
||||
class UserDTO(BaseModel):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
||||
email: str
|
||||
name: str
|
||||
type: str
|
||||
passport_id: str
|
||||
passwordHash: str
|
||||
passwordSalt: str
|
||||
groupName: Optional[str] = None
|
||||
corporate: Optional[str] = None
|
||||
studentID: Optional[str | int] = None
|
||||
expiryDate: Optional[str] = None
|
||||
demographicInformation: Optional[DemographicInfo] = None
|
||||
|
||||
|
||||
class BatchUsersDTO(BaseModel):
|
||||
makerID: str
|
||||
users: list[UserDTO]
|
||||
@@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WritingGradeTaskDTO(BaseModel):
|
||||
question: str
|
||||
answer: str
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WritingGradeTaskDTO(BaseModel):
|
||||
question: str
|
||||
answer: str
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .exceptions import CustomException, UnauthorizedException
|
||||
|
||||
__all__ = [
|
||||
"CustomException",
|
||||
"UnauthorizedException"
|
||||
]
|
||||
from .exceptions import CustomException, UnauthorizedException
|
||||
|
||||
__all__ = [
|
||||
"CustomException",
|
||||
"UnauthorizedException"
|
||||
]
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
|
||||
class CustomException(Exception):
|
||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
error_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
message = HTTPStatus.INTERNAL_SERVER_ERROR.description
|
||||
|
||||
def __init__(self, message=None):
|
||||
if message:
|
||||
self.message = message
|
||||
|
||||
|
||||
class UnauthorizedException(CustomException):
|
||||
code = HTTPStatus.UNAUTHORIZED
|
||||
error_code = HTTPStatus.UNAUTHORIZED
|
||||
message = HTTPStatus.UNAUTHORIZED.description
|
||||
from http import HTTPStatus
|
||||
|
||||
|
||||
class CustomException(Exception):
|
||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
error_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
message = HTTPStatus.INTERNAL_SERVER_ERROR.description
|
||||
|
||||
def __init__(self, message=None):
|
||||
if message:
|
||||
self.message = message
|
||||
|
||||
|
||||
class UnauthorizedException(CustomException):
|
||||
code = HTTPStatus.UNAUTHORIZED
|
||||
error_code = HTTPStatus.UNAUTHORIZED
|
||||
message = HTTPStatus.UNAUTHORIZED.description
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from .file import FileHelper
|
||||
from .text import TextHelper
|
||||
from .token_counter import count_tokens
|
||||
from .exercises import ExercisesHelper
|
||||
from .logger import LoggerHelper
|
||||
|
||||
__all__ = [
|
||||
"FileHelper",
|
||||
"TextHelper",
|
||||
"count_tokens",
|
||||
"ExercisesHelper",
|
||||
"LoggerHelper"
|
||||
]
|
||||
from .file import FileHelper
|
||||
from .text import TextHelper
|
||||
from .token_counter import count_tokens
|
||||
from .exercises import ExercisesHelper
|
||||
from .logger import LoggerHelper
|
||||
|
||||
__all__ = [
|
||||
"FileHelper",
|
||||
"TextHelper",
|
||||
"count_tokens",
|
||||
"ExercisesHelper",
|
||||
"LoggerHelper"
|
||||
]
|
||||
|
||||
@@ -1,249 +1,249 @@
|
||||
import queue
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from wonderwords import RandomWord
|
||||
|
||||
from .text import TextHelper
|
||||
|
||||
|
||||
class ExercisesHelper:
|
||||
|
||||
@staticmethod
|
||||
def divide_number_into_parts(number, parts):
|
||||
if number < parts:
|
||||
return None
|
||||
|
||||
part_size = number // parts
|
||||
remaining = number % parts
|
||||
|
||||
q = queue.Queue()
|
||||
|
||||
for i in range(parts):
|
||||
if i < remaining:
|
||||
q.put(part_size + 1)
|
||||
else:
|
||||
q.put(part_size)
|
||||
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def fix_exercise_ids(exercise, start_id):
|
||||
# Initialize the starting ID for the first exercise
|
||||
current_id = start_id
|
||||
|
||||
questions = exercise["questions"]
|
||||
|
||||
# Iterate through questions and update the "id" value
|
||||
for question in questions:
|
||||
question["id"] = str(current_id)
|
||||
current_id += 1
|
||||
|
||||
return exercise
|
||||
|
||||
@staticmethod
|
||||
def replace_first_occurrences_with_placeholders(text: str, words_to_replace: list, start_id):
|
||||
for i, word in enumerate(words_to_replace, start=start_id):
|
||||
# Create a case-insensitive regular expression pattern
|
||||
pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE)
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
text = pattern.sub(placeholder, text, 1)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def replace_first_occurrences_with_placeholders_notes(notes: list, words_to_replace: list, start_id):
|
||||
replaced_notes = []
|
||||
for i, note in enumerate(notes, start=0):
|
||||
word = words_to_replace[i]
|
||||
pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE)
|
||||
placeholder = '{{' + str(start_id + i) + '}}'
|
||||
note = pattern.sub(placeholder, note, 1)
|
||||
replaced_notes.append(note)
|
||||
return replaced_notes
|
||||
|
||||
@staticmethod
|
||||
def add_random_words_and_shuffle(word_array, num_random_words):
|
||||
r = RandomWord()
|
||||
random_words_selected = r.random_words(num_random_words)
|
||||
|
||||
combined_array = word_array + random_words_selected
|
||||
|
||||
random.shuffle(combined_array)
|
||||
|
||||
result = []
|
||||
for i, word in enumerate(combined_array):
|
||||
letter = chr(65 + i) # chr(65) is 'A'
|
||||
result.append({"letter": letter, "word": word})
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def fillblanks_build_solutions_array(words, start_id):
|
||||
solutions = []
|
||||
for i, word in enumerate(words, start=start_id):
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": word
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def remove_excess_questions(questions: [], quantity):
|
||||
count_true = 0
|
||||
result = []
|
||||
|
||||
for item in reversed(questions):
|
||||
if item.get('solution') == 'true' and count_true < quantity:
|
||||
count_true += 1
|
||||
else:
|
||||
result.append(item)
|
||||
|
||||
result.reverse()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_text(questions: [], start_id):
|
||||
result = ""
|
||||
for i, q in enumerate(questions, start=start_id):
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
result = result + q["question"] + placeholder + "\\n"
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_text_form(form: [], start_id):
|
||||
result = ""
|
||||
replaced_words = []
|
||||
for i, entry in enumerate(form, start=start_id):
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
# Use regular expression to find the string after ':'
|
||||
match = re.search(r'(?<=:)\s*(.*)', entry)
|
||||
# Extract the matched string
|
||||
original_string = match.group(1)
|
||||
# Split the string into words
|
||||
words = re.findall(r'\b\w+\b', original_string)
|
||||
# Remove words with only one letter
|
||||
filtered_words = [word for word in words if len(word) > 1]
|
||||
# Choose a random word from the list of words
|
||||
selected_word = random.choice(filtered_words)
|
||||
pattern = re.compile(r'\b' + re.escape(selected_word) + r'\b', re.IGNORECASE)
|
||||
|
||||
# Replace the chosen word with the placeholder
|
||||
replaced_string = pattern.sub(placeholder, original_string, 1)
|
||||
# Construct the final replaced string
|
||||
replaced_string = entry.replace(original_string, replaced_string)
|
||||
|
||||
result = result + replaced_string + "\\n"
|
||||
# Save the replaced word or use it as needed
|
||||
# For example, you can save it to a file or a list
|
||||
replaced_words.append(selected_word)
|
||||
return result, replaced_words
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_solutions(questions: [], start_id):
|
||||
solutions = []
|
||||
for i, q in enumerate(questions, start=start_id):
|
||||
solution = [q["possible_answers"]] if isinstance(q["possible_answers"], str) else q["possible_answers"]
|
||||
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": solution
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_solutions_listening(words: [], start_id):
|
||||
solutions = []
|
||||
for i, word in enumerate(words, start=start_id):
|
||||
solution = [word] if isinstance(word, str) else word
|
||||
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": solution
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def answer_word_limit_ok(question):
|
||||
# Check if any option in any solution has more than three words
|
||||
return not any(
|
||||
len(option.split()) > 3
|
||||
for solution in question["solutions"]
|
||||
for option in solution["solution"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assign_letters_to_paragraphs(paragraphs):
|
||||
result = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for paragraph in paragraphs.split("\n\n"):
|
||||
if TextHelper.has_x_words(paragraph, 10):
|
||||
result.append({'paragraph': paragraph.strip(), 'letter': next(letters)})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def contains_empty_dict(arr):
|
||||
return any(elem == {} for elem in arr)
|
||||
|
||||
@staticmethod
|
||||
def fix_writing_overall(overall: float, task_response: dict):
|
||||
grades = [category["grade"] for category in task_response.values()]
|
||||
|
||||
if overall > max(grades) or overall < min(grades):
|
||||
total_sum = sum(grades)
|
||||
average = total_sum / len(grades)
|
||||
rounded_average = round(average, 0)
|
||||
return rounded_average
|
||||
|
||||
return overall
|
||||
|
||||
@staticmethod
|
||||
def build_options(ideas):
|
||||
options = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for idea in ideas:
|
||||
options.append({
|
||||
"id": next(letters),
|
||||
"sentence": idea["from"]
|
||||
})
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def build_sentences(ideas, start_id):
|
||||
sentences = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for idea in ideas:
|
||||
sentences.append({
|
||||
"solution": next(letters),
|
||||
"sentence": idea["idea"]
|
||||
})
|
||||
|
||||
random.shuffle(sentences)
|
||||
for i, sentence in enumerate(sentences, start=start_id):
|
||||
sentence["id"] = i
|
||||
return sentences
|
||||
|
||||
@staticmethod
|
||||
def randomize_mc_options_order(questions):
|
||||
option_ids = ['A', 'B', 'C', 'D']
|
||||
|
||||
for question in questions:
|
||||
# Store the original solution text
|
||||
original_solution_text = next(
|
||||
option['text'] for option in question['options'] if option['id'] == question['solution'])
|
||||
|
||||
# Shuffle the options
|
||||
random.shuffle(question['options'])
|
||||
|
||||
# Update the option ids and find the new solution id
|
||||
for idx, option in enumerate(question['options']):
|
||||
option['id'] = option_ids[idx]
|
||||
if option['text'] == original_solution_text:
|
||||
question['solution'] = option['id']
|
||||
|
||||
return questions
|
||||
import queue
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from wonderwords import RandomWord
|
||||
|
||||
from .text import TextHelper
|
||||
|
||||
|
||||
class ExercisesHelper:
|
||||
|
||||
@staticmethod
|
||||
def divide_number_into_parts(number, parts):
|
||||
if number < parts:
|
||||
return None
|
||||
|
||||
part_size = number // parts
|
||||
remaining = number % parts
|
||||
|
||||
q = queue.Queue()
|
||||
|
||||
for i in range(parts):
|
||||
if i < remaining:
|
||||
q.put(part_size + 1)
|
||||
else:
|
||||
q.put(part_size)
|
||||
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def fix_exercise_ids(exercise, start_id):
|
||||
# Initialize the starting ID for the first exercise
|
||||
current_id = start_id
|
||||
|
||||
questions = exercise["questions"]
|
||||
|
||||
# Iterate through questions and update the "id" value
|
||||
for question in questions:
|
||||
question["id"] = str(current_id)
|
||||
current_id += 1
|
||||
|
||||
return exercise
|
||||
|
||||
@staticmethod
|
||||
def replace_first_occurrences_with_placeholders(text: str, words_to_replace: list, start_id):
|
||||
for i, word in enumerate(words_to_replace, start=start_id):
|
||||
# Create a case-insensitive regular expression pattern
|
||||
pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE)
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
text = pattern.sub(placeholder, text, 1)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def replace_first_occurrences_with_placeholders_notes(notes: list, words_to_replace: list, start_id):
|
||||
replaced_notes = []
|
||||
for i, note in enumerate(notes, start=0):
|
||||
word = words_to_replace[i]
|
||||
pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE)
|
||||
placeholder = '{{' + str(start_id + i) + '}}'
|
||||
note = pattern.sub(placeholder, note, 1)
|
||||
replaced_notes.append(note)
|
||||
return replaced_notes
|
||||
|
||||
@staticmethod
|
||||
def add_random_words_and_shuffle(word_array, num_random_words):
|
||||
r = RandomWord()
|
||||
random_words_selected = r.random_words(num_random_words)
|
||||
|
||||
combined_array = word_array + random_words_selected
|
||||
|
||||
random.shuffle(combined_array)
|
||||
|
||||
result = []
|
||||
for i, word in enumerate(combined_array):
|
||||
letter = chr(65 + i) # chr(65) is 'A'
|
||||
result.append({"letter": letter, "word": word})
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def fillblanks_build_solutions_array(words, start_id):
|
||||
solutions = []
|
||||
for i, word in enumerate(words, start=start_id):
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": word
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def remove_excess_questions(questions: [], quantity):
|
||||
count_true = 0
|
||||
result = []
|
||||
|
||||
for item in reversed(questions):
|
||||
if item.get('solution') == 'true' and count_true < quantity:
|
||||
count_true += 1
|
||||
else:
|
||||
result.append(item)
|
||||
|
||||
result.reverse()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_text(questions: [], start_id):
|
||||
result = ""
|
||||
for i, q in enumerate(questions, start=start_id):
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
result = result + q["question"] + placeholder + "\\n"
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_text_form(form: [], start_id):
|
||||
result = ""
|
||||
replaced_words = []
|
||||
for i, entry in enumerate(form, start=start_id):
|
||||
placeholder = '{{' + str(i) + '}}'
|
||||
# Use regular expression to find the string after ':'
|
||||
match = re.search(r'(?<=:)\s*(.*)', entry)
|
||||
# Extract the matched string
|
||||
original_string = match.group(1)
|
||||
# Split the string into words
|
||||
words = re.findall(r'\b\w+\b', original_string)
|
||||
# Remove words with only one letter
|
||||
filtered_words = [word for word in words if len(word) > 1]
|
||||
# Choose a random word from the list of words
|
||||
selected_word = random.choice(filtered_words)
|
||||
pattern = re.compile(r'\b' + re.escape(selected_word) + r'\b', re.IGNORECASE)
|
||||
|
||||
# Replace the chosen word with the placeholder
|
||||
replaced_string = pattern.sub(placeholder, original_string, 1)
|
||||
# Construct the final replaced string
|
||||
replaced_string = entry.replace(original_string, replaced_string)
|
||||
|
||||
result = result + replaced_string + "\\n"
|
||||
# Save the replaced word or use it as needed
|
||||
# For example, you can save it to a file or a list
|
||||
replaced_words.append(selected_word)
|
||||
return result, replaced_words
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_solutions(questions: [], start_id):
|
||||
solutions = []
|
||||
for i, q in enumerate(questions, start=start_id):
|
||||
solution = [q["possible_answers"]] if isinstance(q["possible_answers"], str) else q["possible_answers"]
|
||||
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": solution
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def build_write_blanks_solutions_listening(words: [], start_id):
|
||||
solutions = []
|
||||
for i, word in enumerate(words, start=start_id):
|
||||
solution = [word] if isinstance(word, str) else word
|
||||
|
||||
solutions.append(
|
||||
{
|
||||
"id": str(i),
|
||||
"solution": solution
|
||||
}
|
||||
)
|
||||
return solutions
|
||||
|
||||
@staticmethod
|
||||
def answer_word_limit_ok(question):
|
||||
# Check if any option in any solution has more than three words
|
||||
return not any(
|
||||
len(option.split()) > 3
|
||||
for solution in question["solutions"]
|
||||
for option in solution["solution"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assign_letters_to_paragraphs(paragraphs):
|
||||
result = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for paragraph in paragraphs.split("\n\n"):
|
||||
if TextHelper.has_x_words(paragraph, 10):
|
||||
result.append({'paragraph': paragraph.strip(), 'letter': next(letters)})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def contains_empty_dict(arr):
|
||||
return any(elem == {} for elem in arr)
|
||||
|
||||
@staticmethod
|
||||
def fix_writing_overall(overall: float, task_response: dict):
|
||||
grades = [category["grade"] for category in task_response.values()]
|
||||
|
||||
if overall > max(grades) or overall < min(grades):
|
||||
total_sum = sum(grades)
|
||||
average = total_sum / len(grades)
|
||||
rounded_average = round(average, 0)
|
||||
return rounded_average
|
||||
|
||||
return overall
|
||||
|
||||
@staticmethod
|
||||
def build_options(ideas):
|
||||
options = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for idea in ideas:
|
||||
options.append({
|
||||
"id": next(letters),
|
||||
"sentence": idea["from"]
|
||||
})
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def build_sentences(ideas, start_id):
|
||||
sentences = []
|
||||
letters = iter(string.ascii_uppercase)
|
||||
for idea in ideas:
|
||||
sentences.append({
|
||||
"solution": next(letters),
|
||||
"sentence": idea["idea"]
|
||||
})
|
||||
|
||||
random.shuffle(sentences)
|
||||
for i, sentence in enumerate(sentences, start=start_id):
|
||||
sentence["id"] = i
|
||||
return sentences
|
||||
|
||||
@staticmethod
|
||||
def randomize_mc_options_order(questions):
|
||||
option_ids = ['A', 'B', 'C', 'D']
|
||||
|
||||
for question in questions:
|
||||
# Store the original solution text
|
||||
original_solution_text = next(
|
||||
option['text'] for option in question['options'] if option['id'] == question['solution'])
|
||||
|
||||
# Shuffle the options
|
||||
random.shuffle(question['options'])
|
||||
|
||||
# Update the option ids and find the new solution id
|
||||
for idx, option in enumerate(question['options']):
|
||||
option['id'] = option_ids[idx]
|
||||
if option['text'] == original_solution_text:
|
||||
question['solution'] = option['id']
|
||||
|
||||
return questions
|
||||
|
||||
@@ -1,95 +1,114 @@
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import pypandoc
|
||||
from PIL import Image
|
||||
|
||||
import aiofiles
|
||||
|
||||
|
||||
class FileHelper:
|
||||
|
||||
@staticmethod
|
||||
def delete_files_older_than_one_day(directory: str):
|
||||
current_time = datetime.datetime.now()
|
||||
|
||||
for entry in os.scandir(directory):
|
||||
if entry.is_file():
|
||||
file_path = Path(entry)
|
||||
file_name = file_path.name
|
||||
file_modified_time = datetime.datetime.fromtimestamp(file_path.stat().st_mtime)
|
||||
time_difference = current_time - file_modified_time
|
||||
if time_difference.days > 1 and "placeholder" not in file_name:
|
||||
file_path.unlink()
|
||||
print(f"Deleted file: {file_path}")
|
||||
|
||||
# Supposedly pandoc covers a wide range of file extensions only tested with docx
|
||||
@staticmethod
|
||||
def convert_file_to_pdf(input_path: str, output_path: str):
|
||||
pypandoc.convert_file(input_path, 'pdf', outputfile=output_path, extra_args=[
|
||||
'-V', 'geometry:paperwidth=5.5in',
|
||||
'-V', 'geometry:paperheight=8.5in',
|
||||
'-V', 'geometry:margin=0.5in',
|
||||
'-V', 'pagestyle=empty'
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def convert_file_to_html(input_path: str, output_path: str):
|
||||
pypandoc.convert_file(input_path, 'html', outputfile=output_path)
|
||||
|
||||
@staticmethod
|
||||
def pdf_to_png(path_id: str):
|
||||
to_png = f"pdftoppm -png exercises.pdf page"
|
||||
result = subprocess.run(to_png, shell=True, cwd=f'./tmp/{path_id}', capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise Exception(
|
||||
f"Couldn't convert pdf to png. Failed to run command '{to_png}' -> ```cmd {result.stderr}```")
|
||||
|
||||
@staticmethod
|
||||
def is_page_blank(image_bytes: bytes, image_threshold=10) -> bool:
|
||||
with Image.open(io.BytesIO(image_bytes)) as img:
|
||||
img_gray = img.convert('L')
|
||||
img_array = np.array(img_gray)
|
||||
non_white_pixels = np.sum(img_array < 255)
|
||||
|
||||
return non_white_pixels <= image_threshold
|
||||
|
||||
@classmethod
|
||||
async def _encode_image(cls, image_path: str, image_threshold=10) -> Optional[str]:
|
||||
async with aiofiles.open(image_path, "rb") as image_file:
|
||||
image_bytes = await image_file.read()
|
||||
|
||||
if cls.is_page_blank(image_bytes, image_threshold):
|
||||
return None
|
||||
|
||||
return base64.b64encode(image_bytes).decode('utf-8')
|
||||
|
||||
@classmethod
|
||||
def b64_pngs(cls, path_id: str, files: list[str]):
|
||||
png_messages = []
|
||||
for filename in files:
|
||||
b64_string = cls._encode_image(os.path.join(f'./tmp/{path_id}', filename))
|
||||
if b64_string:
|
||||
png_messages.append({
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/png;base64,{b64_string}"
|
||||
}
|
||||
})
|
||||
return png_messages
|
||||
|
||||
@staticmethod
|
||||
def remove_directory(path):
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
except Exception as e:
|
||||
print(f"An error occurred while trying to remove {path}: {str(e)}")
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import aiofiles
|
||||
import numpy as np
|
||||
import pypandoc
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class FileHelper:
|
||||
|
||||
@staticmethod
|
||||
def delete_files_older_than_one_day(directory: str):
|
||||
current_time = datetime.datetime.now()
|
||||
|
||||
for entry in os.scandir(directory):
|
||||
if entry.is_file():
|
||||
file_path = Path(entry)
|
||||
file_name = file_path.name
|
||||
file_modified_time = datetime.datetime.fromtimestamp(file_path.stat().st_mtime)
|
||||
time_difference = current_time - file_modified_time
|
||||
if time_difference.days > 1 and "placeholder" not in file_name:
|
||||
file_path.unlink()
|
||||
print(f"Deleted file: {file_path}")
|
||||
|
||||
# Supposedly pandoc covers a wide range of file extensions only tested with docx
|
||||
@staticmethod
|
||||
def convert_file_to_pdf(input_path: str, output_path: str):
|
||||
pypandoc.convert_file(input_path, 'pdf', outputfile=output_path, extra_args=[
|
||||
'-V', 'geometry:paperwidth=5.5in',
|
||||
'-V', 'geometry:paperheight=8.5in',
|
||||
'-V', 'geometry:margin=0.5in',
|
||||
'-V', 'pagestyle=empty'
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def convert_file_to_html(input_path: str, output_path: str):
|
||||
pypandoc.convert_file(input_path, 'html', outputfile=output_path)
|
||||
|
||||
@staticmethod
|
||||
def pdf_to_png(path_id: str):
|
||||
to_png = f"pdftoppm -png exercises.pdf page"
|
||||
result = subprocess.run(to_png, shell=True, cwd=f'./tmp/{path_id}', capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise Exception(
|
||||
f"Couldn't convert pdf to png. Failed to run command '{to_png}' -> ```cmd {result.stderr}```")
|
||||
|
||||
@staticmethod
|
||||
def is_page_blank(image_bytes: bytes, image_threshold=10) -> bool:
|
||||
with Image.open(io.BytesIO(image_bytes)) as img:
|
||||
img_gray = img.convert('L')
|
||||
img_array = np.array(img_gray)
|
||||
non_white_pixels = np.sum(img_array < 255)
|
||||
|
||||
return non_white_pixels <= image_threshold
|
||||
|
||||
@classmethod
|
||||
async def _encode_image(cls, image_path: str, image_threshold=10) -> Optional[str]:
|
||||
async with aiofiles.open(image_path, "rb") as image_file:
|
||||
image_bytes = await image_file.read()
|
||||
|
||||
if cls.is_page_blank(image_bytes, image_threshold):
|
||||
return None
|
||||
|
||||
return base64.b64encode(image_bytes).decode('utf-8')
|
||||
|
||||
@classmethod
|
||||
async def b64_pngs(cls, path_id: str, files: list[str]):
|
||||
png_messages = []
|
||||
for filename in files:
|
||||
b64_string = await cls._encode_image(os.path.join(f'./tmp/{path_id}', filename))
|
||||
if b64_string:
|
||||
png_messages.append({
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/png;base64,{b64_string}"
|
||||
}
|
||||
})
|
||||
return png_messages
|
||||
|
||||
@staticmethod
|
||||
def remove_directory(path):
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
except Exception as e:
|
||||
print(f"An error occurred while trying to remove {path}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def remove_file(file_path):
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
print(f"An error occurred while trying to remove the file {file_path}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def save_upload(file) -> Tuple[str, str]:
|
||||
ext = file.filename.split('.')[-1]
|
||||
path_id = str(uuid.uuid4())
|
||||
os.makedirs(f'./tmp/{path_id}', exist_ok=True)
|
||||
|
||||
tmp_filename = f'./tmp/{path_id}/uploaded.{ext}'
|
||||
file.save(tmp_filename)
|
||||
return ext, path_id
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class LoggerHelper:
|
||||
|
||||
@staticmethod
|
||||
def suppress_loggers():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
root_logger = logging.getLogger()
|
||||
original_level = root_logger.level
|
||||
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
root_logger.setLevel(original_level)
|
||||
|
||||
return wrapped
|
||||
return decorator
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class LoggerHelper:
|
||||
|
||||
@staticmethod
|
||||
def suppress_loggers():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
root_logger = logging.getLogger()
|
||||
original_level = root_logger.level
|
||||
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
root_logger.setLevel(original_level)
|
||||
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
from nltk.corpus import words
|
||||
|
||||
|
||||
class TextHelper:
|
||||
|
||||
@classmethod
|
||||
def has_words(cls, text: str):
|
||||
if not cls._has_common_words(text):
|
||||
return False
|
||||
english_words = set(words.words())
|
||||
words_in_input = text.split()
|
||||
return any(word.lower() in english_words for word in words_in_input)
|
||||
|
||||
@classmethod
|
||||
def has_x_words(cls, text: str, quantity):
|
||||
if not cls._has_common_words(text):
|
||||
return False
|
||||
english_words = set(words.words())
|
||||
words_in_input = text.split()
|
||||
english_word_count = sum(1 for word in words_in_input if word.lower() in english_words)
|
||||
return english_word_count >= quantity
|
||||
|
||||
@staticmethod
|
||||
def _has_common_words(text: str):
|
||||
english_words = {"the", "be", "to", "of", "and", "a", "in", "that", "have", "i"}
|
||||
words_in_input = text.split()
|
||||
english_word_count = sum(1 for word in words_in_input if word.lower() in english_words)
|
||||
return english_word_count >= 10
|
||||
from nltk.corpus import words
|
||||
|
||||
|
||||
class TextHelper:
|
||||
|
||||
@classmethod
|
||||
def has_words(cls, text: str):
|
||||
if not cls._has_common_words(text):
|
||||
return False
|
||||
english_words = set(words.words())
|
||||
words_in_input = text.split()
|
||||
return any(word.lower() in english_words for word in words_in_input)
|
||||
|
||||
@classmethod
|
||||
def has_x_words(cls, text: str, quantity):
|
||||
if not cls._has_common_words(text):
|
||||
return False
|
||||
english_words = set(words.words())
|
||||
words_in_input = text.split()
|
||||
english_word_count = sum(1 for word in words_in_input if word.lower() in english_words)
|
||||
return english_word_count >= quantity
|
||||
|
||||
@staticmethod
|
||||
def _has_common_words(text: str):
|
||||
english_words = {"the", "be", "to", "of", "and", "a", "in", "that", "have", "i"}
|
||||
words_in_input = text.split()
|
||||
english_word_count = sum(1 for word in words_in_input if word.lower() in english_words)
|
||||
return english_word_count >= 10
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
# This is a work in progress. There are still bugs. Once it is production-ready this will become a full repo.
|
||||
|
||||
import tiktoken
|
||||
import nltk
|
||||
|
||||
|
||||
def count_tokens(text, model_name="gpt-3.5-turbo", debug=False):
|
||||
"""
|
||||
Count the number of tokens in a given text string without using the OpenAI API.
|
||||
|
||||
This function tries three methods in the following order:
|
||||
1. tiktoken (preferred): Accurate token counting similar to the OpenAI API.
|
||||
2. nltk: Token counting using the Natural Language Toolkit library.
|
||||
3. split: Simple whitespace-based token counting as a fallback.
|
||||
|
||||
Usage:
|
||||
------
|
||||
text = "Your text here"
|
||||
result = count_tokens(text, model_name="gpt-3.5-turbo", debug=True)
|
||||
print(result)
|
||||
|
||||
Required libraries:
|
||||
-------------------
|
||||
- tiktoken: Install with 'pip install tiktoken'
|
||||
- nltk: Install with 'pip install nltk'
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
text : str
|
||||
The text string for which you want to count tokens.
|
||||
model_name : str, optional
|
||||
The OpenAI model for which you want to count tokens (default: "gpt-3.5-turbo").
|
||||
debug : bool, optional
|
||||
Set to True to print error messages (default: False).
|
||||
|
||||
Returns:
|
||||
--------
|
||||
result : dict
|
||||
A dictionary containing the number of tokens and the method used for counting.
|
||||
"""
|
||||
|
||||
# Try using tiktoken
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model_name)
|
||||
num_tokens = len(encoding.encode(text))
|
||||
result = {"n_tokens": num_tokens, "method": "tiktoken"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using tiktoken: {e}")
|
||||
pass
|
||||
|
||||
# Try using nltk
|
||||
try:
|
||||
# Passed nltk.download("punkt") to server.py's @asynccontextmanager
|
||||
tokens = nltk.word_tokenize(text)
|
||||
result = {"n_tokens": len(tokens), "method": "nltk"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using nltk: {e}")
|
||||
pass
|
||||
|
||||
# If nltk and tiktoken fail, use a simple split-based method
|
||||
tokens = text.split()
|
||||
result = {"n_tokens": len(tokens), "method": "split"}
|
||||
return result
|
||||
|
||||
|
||||
class TokenBuffer:
|
||||
def __init__(self, max_tokens=2048):
|
||||
self.max_tokens = max_tokens
|
||||
self.buffer = ""
|
||||
self.token_lengths = []
|
||||
self.token_count = 0
|
||||
|
||||
def update(self, text, model_name="gpt-3.5-turbo", debug=False):
|
||||
new_tokens = count_tokens(text, model_name=model_name, debug=debug)["n_tokens"]
|
||||
self.token_count += new_tokens
|
||||
self.buffer += text
|
||||
self.token_lengths.append(new_tokens)
|
||||
|
||||
while self.token_count > self.max_tokens:
|
||||
removed_tokens = self.token_lengths.pop(0)
|
||||
self.token_count -= removed_tokens
|
||||
self.buffer = self.buffer.split(" ", removed_tokens)[-1]
|
||||
|
||||
def get_buffer(self):
|
||||
return self.buffer
|
||||
# This is a work in progress. There are still bugs. Once it is production-ready this will become a full repo.
|
||||
|
||||
import tiktoken
|
||||
import nltk
|
||||
|
||||
|
||||
def count_tokens(text, model_name="gpt-3.5-turbo", debug=False):
|
||||
"""
|
||||
Count the number of tokens in a given text string without using the OpenAI API.
|
||||
|
||||
This function tries three methods in the following order:
|
||||
1. tiktoken (preferred): Accurate token counting similar to the OpenAI API.
|
||||
2. nltk: Token counting using the Natural Language Toolkit library.
|
||||
3. split: Simple whitespace-based token counting as a fallback.
|
||||
|
||||
Usage:
|
||||
------
|
||||
text = "Your text here"
|
||||
result = count_tokens(text, model_name="gpt-3.5-turbo", debug=True)
|
||||
print(result)
|
||||
|
||||
Required libraries:
|
||||
-------------------
|
||||
- tiktoken: Install with 'pip install tiktoken'
|
||||
- nltk: Install with 'pip install nltk'
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
text : str
|
||||
The text string for which you want to count tokens.
|
||||
model_name : str, optional
|
||||
The OpenAI model for which you want to count tokens (default: "gpt-3.5-turbo").
|
||||
debug : bool, optional
|
||||
Set to True to print error messages (default: False).
|
||||
|
||||
Returns:
|
||||
--------
|
||||
result : dict
|
||||
A dictionary containing the number of tokens and the method used for counting.
|
||||
"""
|
||||
|
||||
# Try using tiktoken
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model_name)
|
||||
num_tokens = len(encoding.encode(text))
|
||||
result = {"n_tokens": num_tokens, "method": "tiktoken"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using tiktoken: {e}")
|
||||
pass
|
||||
|
||||
# Try using nltk
|
||||
try:
|
||||
# Passed nltk.download("punkt") to server.py's @asynccontextmanager
|
||||
tokens = nltk.word_tokenize(text)
|
||||
result = {"n_tokens": len(tokens), "method": "nltk"}
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"Error using nltk: {e}")
|
||||
pass
|
||||
|
||||
# If nltk and tiktoken fail, use a simple split-based method
|
||||
tokens = text.split()
|
||||
result = {"n_tokens": len(tokens), "method": "split"}
|
||||
return result
|
||||
|
||||
|
||||
class TokenBuffer:
|
||||
def __init__(self, max_tokens=2048):
|
||||
self.max_tokens = max_tokens
|
||||
self.buffer = ""
|
||||
self.token_lengths = []
|
||||
self.token_count = 0
|
||||
|
||||
def update(self, text, model_name="gpt-3.5-turbo", debug=False):
|
||||
new_tokens = count_tokens(text, model_name=model_name, debug=debug)["n_tokens"]
|
||||
self.token_count += new_tokens
|
||||
self.buffer += text
|
||||
self.token_lengths.append(new_tokens)
|
||||
|
||||
while self.token_count > self.max_tokens:
|
||||
removed_tokens = self.token_lengths.pop(0)
|
||||
self.token_count -= removed_tokens
|
||||
self.buffer = self.buffer.split(" ", removed_tokens)[-1]
|
||||
|
||||
def get_buffer(self):
|
||||
return self.buffer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .exam import ExamMapper
|
||||
|
||||
__all__ = [
|
||||
"ExamMapper"
|
||||
]
|
||||
from .exam import ExamMapper
|
||||
|
||||
__all__ = [
|
||||
"ExamMapper"
|
||||
]
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.dtos.exam import (
|
||||
MultipleChoiceExercise,
|
||||
FillBlanksExercise,
|
||||
Part, Exam
|
||||
)
|
||||
from app.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
|
||||
|
||||
|
||||
class ExamMapper:
|
||||
|
||||
@staticmethod
|
||||
def map_to_exam_model(response: Dict[str, Any]) -> Exam:
|
||||
parts = []
|
||||
for part in response['parts']:
|
||||
part_exercises = part['exercises']
|
||||
context = part.get('context', None)
|
||||
|
||||
exercises = []
|
||||
for exercise in part_exercises:
|
||||
exercise_type = exercise['type']
|
||||
if exercise_type == 'multipleChoice':
|
||||
exercise_model = MultipleChoiceExercise(**exercise)
|
||||
elif exercise_type == 'fillBlanks':
|
||||
exercise_model = FillBlanksExercise(**exercise)
|
||||
else:
|
||||
raise ValidationError(f"Unknown exercise type: {exercise_type}")
|
||||
|
||||
exercises.append(exercise_model)
|
||||
|
||||
part_kwargs = {"exercises": exercises}
|
||||
if context is not None:
|
||||
part_kwargs["context"] = context
|
||||
|
||||
part_model = Part(**part_kwargs)
|
||||
parts.append(part_model)
|
||||
|
||||
return Exam(parts=parts)
|
||||
|
||||
@staticmethod
|
||||
def map_to_sheet(response: Dict[str, Any]) -> Sheet:
|
||||
components = []
|
||||
|
||||
for item in response["components"]:
|
||||
component_type = item["type"]
|
||||
|
||||
if component_type == "multipleChoice":
|
||||
options = [Option(id=opt["id"], text=opt["text"]) for opt in item["options"]]
|
||||
components.append(MultipleChoiceQuestion(
|
||||
id=item["id"],
|
||||
prompt=item["prompt"],
|
||||
variant=item.get("variant", "text"),
|
||||
options=options
|
||||
))
|
||||
elif component_type == "fillBlanks":
|
||||
components.append(FillBlanksWord(
|
||||
id=item["id"],
|
||||
options=item["options"]
|
||||
))
|
||||
else:
|
||||
components.append(item)
|
||||
|
||||
return Sheet(components=components)
|
||||
from typing import Dict, Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.dtos.exam import (
|
||||
MultipleChoiceExercise,
|
||||
FillBlanksExercise,
|
||||
Part, Exam
|
||||
)
|
||||
from app.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
|
||||
|
||||
|
||||
class ExamMapper:
|
||||
|
||||
@staticmethod
|
||||
def map_to_exam_model(response: Dict[str, Any]) -> Exam:
|
||||
parts = []
|
||||
for part in response['parts']:
|
||||
part_exercises = part['exercises']
|
||||
context = part.get('context', None)
|
||||
|
||||
exercises = []
|
||||
for exercise in part_exercises:
|
||||
exercise_type = exercise['type']
|
||||
if exercise_type == 'multipleChoice':
|
||||
exercise_model = MultipleChoiceExercise(**exercise)
|
||||
elif exercise_type == 'fillBlanks':
|
||||
exercise_model = FillBlanksExercise(**exercise)
|
||||
else:
|
||||
raise ValidationError(f"Unknown exercise type: {exercise_type}")
|
||||
|
||||
exercises.append(exercise_model)
|
||||
|
||||
part_kwargs = {"exercises": exercises}
|
||||
if context is not None:
|
||||
part_kwargs["context"] = context
|
||||
|
||||
part_model = Part(**part_kwargs)
|
||||
parts.append(part_model)
|
||||
|
||||
return Exam(parts=parts)
|
||||
|
||||
@staticmethod
|
||||
def map_to_sheet(response: Dict[str, Any]) -> Sheet:
|
||||
components = []
|
||||
|
||||
for item in response["components"]:
|
||||
component_type = item["type"]
|
||||
|
||||
if component_type == "multipleChoice":
|
||||
options = [Option(id=opt["id"], text=opt["text"]) for opt in item["options"]]
|
||||
components.append(MultipleChoiceQuestion(
|
||||
id=item["id"],
|
||||
prompt=item["prompt"],
|
||||
variant=item.get("variant", "text"),
|
||||
options=options
|
||||
))
|
||||
elif component_type == "fillBlanks":
|
||||
components.append(FillBlanksWord(
|
||||
id=item["id"],
|
||||
options=item["options"]
|
||||
))
|
||||
else:
|
||||
components.append(item)
|
||||
|
||||
return Sheet(components=components)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from .authentication import AuthBackend, AuthenticationMiddleware
|
||||
from .authorization import Authorized, IsAuthenticatedViaBearerToken
|
||||
|
||||
__all__ = [
|
||||
"AuthBackend",
|
||||
"AuthenticationMiddleware",
|
||||
"Authorized",
|
||||
"IsAuthenticatedViaBearerToken"
|
||||
from .authentication import AuthBackend, AuthenticationMiddleware
|
||||
from .authorization import Authorized, IsAuthenticatedViaBearerToken
|
||||
|
||||
__all__ = [
|
||||
"AuthBackend",
|
||||
"AuthenticationMiddleware",
|
||||
"Authorized",
|
||||
"IsAuthenticatedViaBearerToken"
|
||||
]
|
||||
@@ -1,48 +1,48 @@
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
import jwt
|
||||
from jwt import InvalidTokenError
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.authentication import AuthenticationBackend
|
||||
from starlette.middleware.authentication import (
|
||||
AuthenticationMiddleware as BaseAuthenticationMiddleware,
|
||||
)
|
||||
from starlette.requests import HTTPConnection
|
||||
|
||||
|
||||
class Session(BaseModel):
|
||||
authenticated: bool = Field(False, description="Is user authenticated?")
|
||||
|
||||
|
||||
class AuthBackend(AuthenticationBackend):
|
||||
async def authenticate(
|
||||
self, conn: HTTPConnection
|
||||
) -> Tuple[bool, Session]:
|
||||
session = Session()
|
||||
authorization: str = conn.headers.get("Authorization")
|
||||
if not authorization:
|
||||
return False, session
|
||||
|
||||
try:
|
||||
scheme, token = authorization.split(" ")
|
||||
if scheme.lower() != "bearer":
|
||||
return False, session
|
||||
except ValueError:
|
||||
return False, session
|
||||
|
||||
jwt_secret_key = os.getenv("JWT_SECRET_KEY")
|
||||
if not jwt_secret_key:
|
||||
return False, session
|
||||
|
||||
try:
|
||||
jwt.decode(token, jwt_secret_key, algorithms=["HS256"])
|
||||
except InvalidTokenError:
|
||||
return False, session
|
||||
|
||||
session.authenticated = True
|
||||
return True, session
|
||||
|
||||
|
||||
class AuthenticationMiddleware(BaseAuthenticationMiddleware):
|
||||
pass
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
import jwt
|
||||
from jwt import InvalidTokenError
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.authentication import AuthenticationBackend
|
||||
from starlette.middleware.authentication import (
|
||||
AuthenticationMiddleware as BaseAuthenticationMiddleware,
|
||||
)
|
||||
from starlette.requests import HTTPConnection
|
||||
|
||||
|
||||
class Session(BaseModel):
|
||||
authenticated: bool = Field(False, description="Is user authenticated?")
|
||||
|
||||
|
||||
class AuthBackend(AuthenticationBackend):
|
||||
async def authenticate(
|
||||
self, conn: HTTPConnection
|
||||
) -> Tuple[bool, Session]:
|
||||
session = Session()
|
||||
authorization: str = conn.headers.get("Authorization")
|
||||
if not authorization:
|
||||
return False, session
|
||||
|
||||
try:
|
||||
scheme, token = authorization.split(" ")
|
||||
if scheme.lower() != "bearer":
|
||||
return False, session
|
||||
except ValueError:
|
||||
return False, session
|
||||
|
||||
jwt_secret_key = os.getenv("JWT_SECRET_KEY")
|
||||
if not jwt_secret_key:
|
||||
return False, session
|
||||
|
||||
try:
|
||||
jwt.decode(token, jwt_secret_key, algorithms=["HS256"])
|
||||
except InvalidTokenError:
|
||||
return False, session
|
||||
|
||||
session.authenticated = True
|
||||
return True, session
|
||||
|
||||
|
||||
class AuthenticationMiddleware(BaseAuthenticationMiddleware):
|
||||
pass
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Type
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from app.exceptions import CustomException, UnauthorizedException
|
||||
|
||||
|
||||
class BaseAuthorization(ABC):
|
||||
exception = CustomException
|
||||
|
||||
@abstractmethod
|
||||
async def has_permission(self, request: Request) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class IsAuthenticatedViaBearerToken(BaseAuthorization):
|
||||
exception = UnauthorizedException
|
||||
|
||||
async def has_permission(self, request: Request) -> bool:
|
||||
return request.user.authenticated
|
||||
|
||||
|
||||
class Authorized(SecurityBase):
|
||||
def __init__(self, permissions: List[Type[BaseAuthorization]]):
|
||||
self.permissions = permissions
|
||||
self.model: APIKey = APIKey(**{"in": APIKeyIn.header}, name="Authorization")
|
||||
self.scheme_name = self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
for permission in self.permissions:
|
||||
cls = permission()
|
||||
if not await cls.has_permission(request=request):
|
||||
raise cls.exception
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Type
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from app.exceptions import CustomException, UnauthorizedException
|
||||
|
||||
|
||||
class BaseAuthorization(ABC):
|
||||
exception = CustomException
|
||||
|
||||
@abstractmethod
|
||||
async def has_permission(self, request: Request) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class IsAuthenticatedViaBearerToken(BaseAuthorization):
|
||||
exception = UnauthorizedException
|
||||
|
||||
async def has_permission(self, request: Request) -> bool:
|
||||
return request.user.authenticated
|
||||
|
||||
|
||||
class Authorized(SecurityBase):
|
||||
def __init__(self, permissions: List[Type[BaseAuthorization]]):
|
||||
self.permissions = permissions
|
||||
self.model: APIKey = APIKey(**{"in": APIKeyIn.header}, name="Authorization")
|
||||
self.scheme_name = self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
for permission in self.permissions:
|
||||
cls = permission()
|
||||
if not await cls.has_permission(request=request):
|
||||
raise cls.exception
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .file_storage import IFileStorage
|
||||
from .document_store import IDocumentStore
|
||||
|
||||
__all__ = [
|
||||
"IFileStorage",
|
||||
"IDocumentStore"
|
||||
from .file_storage import IFileStorage
|
||||
from .document_store import IDocumentStore
|
||||
|
||||
__all__ = [
|
||||
"IFileStorage",
|
||||
"IDocumentStore"
|
||||
]
|
||||
@@ -1,16 +1,15 @@
|
||||
from abc import ABC
|
||||
|
||||
|
||||
class IDocumentStore(ABC):
|
||||
|
||||
async def save_to_db(self, collection: str, item):
|
||||
pass
|
||||
|
||||
async def save_to_db_with_id(self, collection: str, item, id: str):
|
||||
pass
|
||||
|
||||
async def get_all(self, collection: str):
|
||||
pass
|
||||
|
||||
async def get_doc_by_id(self, collection: str, doc_id: str):
|
||||
pass
|
||||
from abc import ABC
|
||||
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
|
||||
class IDocumentStore(ABC):
|
||||
|
||||
async def save_to_db(self, collection: str, item: Dict, doc_id: Optional[str]) -> Optional[str]:
|
||||
pass
|
||||
|
||||
async def get_all(self, collection: str) -> List[Dict]:
|
||||
pass
|
||||
|
||||
async def get_doc_by_id(self, collection: str, doc_id: str) -> Optional[Dict]:
|
||||
pass
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IFileStorage(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def download_firebase_file(self, source_blob_name, destination_file_name):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_file_firebase_get_url(self, destination_blob_name, source_file_name):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def make_public(self, blob_name: str):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IFileStorage(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def download_firebase_file(self, source_blob_name, destination_file_name):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_file_firebase_get_url(self, destination_blob_name, source_file_name):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def make_public(self, blob_name: str):
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from .document_stores import *
|
||||
from .firebase import FirebaseStorage
|
||||
|
||||
__all__ = [
|
||||
"FirebaseStorage"
|
||||
]
|
||||
|
||||
__all__.extend(document_stores.__all__)
|
||||
from .document_stores import *
|
||||
from app.repositories.impl.file_storage.firebase import FirebaseStorage
|
||||
|
||||
__all__ = [
|
||||
"FirebaseStorage"
|
||||
]
|
||||
|
||||
__all__.extend(document_stores.__all__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .firestore import Firestore
|
||||
#from .mongo import MongoDB
|
||||
|
||||
__all__ = [
|
||||
"Firestore",
|
||||
#"MongoDB"
|
||||
]
|
||||
from .firestore import Firestore
|
||||
#from .mongo import MongoDB
|
||||
|
||||
__all__ = [
|
||||
"Firestore",
|
||||
#"MongoDB"
|
||||
]
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import logging
|
||||
from google.cloud.firestore_v1.async_client import AsyncClient
|
||||
from google.cloud.firestore_v1.async_collection import AsyncCollectionReference
|
||||
from google.cloud.firestore_v1.async_document import AsyncDocumentReference
|
||||
from app.repositories.abc import IDocumentStore
|
||||
|
||||
|
||||
class Firestore(IDocumentStore):
|
||||
def __init__(self, client: AsyncClient):
|
||||
self._client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def save_to_db(self, collection: str, item):
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
update_time, document_ref = await collection_ref.add(item)
|
||||
if document_ref:
|
||||
self._logger.info(f"Document added with ID: {document_ref.id}")
|
||||
return document_ref.id
|
||||
else:
|
||||
return None
|
||||
|
||||
async def save_to_db_with_id(self, collection: str, item, id: str):
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
document_ref: AsyncDocumentReference = collection_ref.document(id)
|
||||
await document_ref.set(item)
|
||||
doc_snapshot = await document_ref.get()
|
||||
if doc_snapshot.exists:
|
||||
self._logger.info(f"Document added with ID: {document_ref.id}")
|
||||
return document_ref.id
|
||||
else:
|
||||
return None
|
||||
|
||||
async def get_all(self, collection: str):
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
docs = []
|
||||
async for doc in collection_ref.stream():
|
||||
docs.append(doc.to_dict())
|
||||
return docs
|
||||
|
||||
async def get_doc_by_id(self, collection: str, doc_id: str):
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
doc_ref: AsyncDocumentReference = collection_ref.document(doc_id)
|
||||
doc = await doc_ref.get()
|
||||
|
||||
if doc.exists:
|
||||
return doc.to_dict()
|
||||
return None
|
||||
import logging
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
from google.cloud.firestore_v1.async_client import AsyncClient
|
||||
from google.cloud.firestore_v1.async_collection import AsyncCollectionReference
|
||||
from google.cloud.firestore_v1.async_document import AsyncDocumentReference
|
||||
from app.repositories.abc import IDocumentStore
|
||||
|
||||
|
||||
class Firestore(IDocumentStore):
|
||||
def __init__(self, client: AsyncClient):
|
||||
self._client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def save_to_db(self, collection: str, item, doc_id: Optional[str] = None) -> Optional[str]:
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
|
||||
if doc_id:
|
||||
document_ref: AsyncDocumentReference = collection_ref.document(doc_id)
|
||||
await document_ref.set(item)
|
||||
doc_snapshot = await document_ref.get()
|
||||
if doc_snapshot.exists:
|
||||
self._logger.info(f"Document added with ID: {document_ref.id}")
|
||||
return document_ref.id
|
||||
else:
|
||||
update_time, document_ref = await collection_ref.add(item)
|
||||
if document_ref:
|
||||
self._logger.info(f"Document added with ID: {document_ref.id}")
|
||||
return document_ref.id
|
||||
|
||||
return None
|
||||
|
||||
async def get_all(self, collection: str) -> List[Dict]:
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
docs = []
|
||||
async for doc in collection_ref.stream():
|
||||
docs.append(doc.to_dict())
|
||||
return docs
|
||||
|
||||
async def get_doc_by_id(self, collection: str, doc_id: str) -> Optional[Dict]:
|
||||
collection_ref: AsyncCollectionReference = self._client.collection(collection)
|
||||
doc_ref: AsyncDocumentReference = collection_ref.document(doc_id)
|
||||
doc = await doc_ref.get()
|
||||
|
||||
if doc.exists:
|
||||
return doc.to_dict()
|
||||
return None
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
"""import logging
|
||||
from pymongo import MongoClient
|
||||
|
||||
from app.repositories.abc import IDocumentStore
|
||||
|
||||
|
||||
class MongoDB(IDocumentStore):
|
||||
|
||||
def __init__(self, client: MongoClient):
|
||||
self._client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
def save_to_db(self, collection: str, item):
|
||||
collection_ref = self._client[collection]
|
||||
result = collection_ref.insert_one(item)
|
||||
if result.inserted_id:
|
||||
self._logger.info(f"Document added with ID: {result.inserted_id}")
|
||||
return True, str(result.inserted_id)
|
||||
else:
|
||||
return False, None
|
||||
|
||||
def save_to_db_with_id(self, collection: str, item, doc_id: str):
|
||||
collection_ref = self._client[collection]
|
||||
item['_id'] = doc_id
|
||||
result = collection_ref.replace_one({'_id': id}, item, upsert=True)
|
||||
if result.upserted_id or result.matched_count:
|
||||
self._logger.info(f"Document added with ID: {doc_id}")
|
||||
return True, doc_id
|
||||
else:
|
||||
return False, None
|
||||
|
||||
def get_all(self, collection: str):
|
||||
collection_ref = self._client[collection]
|
||||
all_documents = list(collection_ref.find())
|
||||
return all_documents
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||
|
||||
from app.repositories.abc import IDocumentStore
|
||||
|
||||
|
||||
class MongoDB(IDocumentStore):
|
||||
|
||||
def __init__(self, mongo_db: AsyncIOMotorDatabase):
|
||||
self._mongo_db = mongo_db
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def save_to_db(self, collection: str, item, doc_id: Optional[str] = None) -> Optional[str]:
|
||||
collection_ref = self._mongo_db[collection]
|
||||
|
||||
if doc_id is None:
|
||||
doc_id = str(uuid.uuid4())
|
||||
|
||||
item['id'] = doc_id
|
||||
|
||||
result = await collection_ref.insert_one(item)
|
||||
if result.inserted_id:
|
||||
# returning id instead of _id
|
||||
self._logger.info(f"Document added with ID: {doc_id}")
|
||||
return doc_id
|
||||
|
||||
return None
|
||||
|
||||
async def get_all(self, collection: str) -> List[Dict]:
|
||||
cursor = self._mongo_db[collection].find()
|
||||
return [document async for document in cursor]
|
||||
|
||||
async def get_doc_by_id(self, collection: str, doc_id: str) -> Optional[Dict]:
|
||||
return await self._mongo_db[collection].find_one({"id": doc_id})
|
||||
|
||||
5
app/repositories/impl/file_storage/__init__.py
Normal file
5
app/repositories/impl/file_storage/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .firebase import FirebaseStorage
|
||||
|
||||
__all__ = [
|
||||
"FirebaseStorage"
|
||||
]
|
||||
@@ -1,83 +1,83 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.repositories.abc import IFileStorage
|
||||
|
||||
|
||||
class FirebaseStorage(IFileStorage):
|
||||
|
||||
def __init__(self, client: AsyncClient, token: str, bucket: str):
|
||||
self._httpx_client = client
|
||||
self._token = token
|
||||
self._storage_url = f'https://firebasestorage.googleapis.com/v0/b/{bucket}'
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def download_firebase_file(self, source_blob_name: str, destination_file_name: str) -> Optional[str]:
|
||||
source_blob_name = source_blob_name.replace('/', '%2F')
|
||||
download_url = f"{self._storage_url}/o/{source_blob_name}?alt=media"
|
||||
|
||||
response = await self._httpx_client.get(
|
||||
download_url,
|
||||
headers={'Authorization': f'Firebase {self._token}'}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
async with aiofiles.open(destination_file_name, 'wb') as file:
|
||||
await file.write(response.content)
|
||||
self._logger.info(f"File downloaded to {destination_file_name}")
|
||||
return destination_file_name
|
||||
else:
|
||||
self._logger.error(f"Failed to download blob {source_blob_name}. {response.status_code} - {response.content}")
|
||||
return None
|
||||
|
||||
async def upload_file_firebase_get_url(self, destination_blob_name: str, source_file_name: str) -> Optional[str]:
|
||||
destination_blob_name = destination_blob_name.replace('/', '%2F')
|
||||
upload_url = f"{self._storage_url}/o/{destination_blob_name}"
|
||||
|
||||
async with aiofiles.open(source_file_name, 'rb') as file:
|
||||
file_bytes = await file.read()
|
||||
|
||||
response = await self._httpx_client.post(
|
||||
upload_url,
|
||||
headers={
|
||||
'Authorization': f'Firebase {self._token}',
|
||||
"X-Goog-Upload-Protocol": "multipart"
|
||||
},
|
||||
files={
|
||||
'metadata': (None, '{"metadata":{"test":"testMetadata"}}', 'application/json'),
|
||||
'file': file_bytes
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self._logger.info(f"File {source_file_name} uploaded to {self._storage_url}/o/{destination_blob_name}.")
|
||||
|
||||
# TODO: Test this
|
||||
#await self.make_public(destination_blob_name)
|
||||
|
||||
file_url = f"{self._storage_url}/o/{destination_blob_name}"
|
||||
return file_url
|
||||
else:
|
||||
self._logger.error(f"Failed to upload file {source_file_name}. Error: {response.status_code} - {str(response.content)}")
|
||||
return None
|
||||
|
||||
async def make_public(self, destination_blob_name: str):
|
||||
acl_url = f"{self._storage_url}/o/{destination_blob_name}/acl"
|
||||
acl = {'entity': 'allUsers', 'role': 'READER'}
|
||||
|
||||
response = await self._httpx_client.post(
|
||||
acl_url,
|
||||
headers={
|
||||
'Authorization': f'Bearer {self._token}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json=acl
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self._logger.info(f"Blob {destination_blob_name} is now public.")
|
||||
else:
|
||||
self._logger.error(f"Failed to make blob {destination_blob_name} public. {response.status_code} - {response.content}")
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.repositories.abc import IFileStorage
|
||||
|
||||
|
||||
class FirebaseStorage(IFileStorage):
|
||||
|
||||
def __init__(self, client: AsyncClient, token: str, bucket: str):
|
||||
self._httpx_client = client
|
||||
self._token = token
|
||||
self._storage_url = f'https://firebasestorage.googleapis.com/v0/b/{bucket}'
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def download_firebase_file(self, source_blob_name: str, destination_file_name: str) -> Optional[str]:
|
||||
source_blob_name = source_blob_name.replace('/', '%2F')
|
||||
download_url = f"{self._storage_url}/o/{source_blob_name}?alt=media"
|
||||
|
||||
response = await self._httpx_client.get(
|
||||
download_url,
|
||||
headers={'Authorization': f'Firebase {self._token}'}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
async with aiofiles.open(destination_file_name, 'wb') as file:
|
||||
await file.write(response.content)
|
||||
self._logger.info(f"File downloaded to {destination_file_name}")
|
||||
return destination_file_name
|
||||
else:
|
||||
self._logger.error(f"Failed to download blob {source_blob_name}. {response.status_code} - {response.content}")
|
||||
return None
|
||||
|
||||
async def upload_file_firebase_get_url(self, destination_blob_name: str, source_file_name: str) -> Optional[str]:
|
||||
destination_blob_name = destination_blob_name.replace('/', '%2F')
|
||||
upload_url = f"{self._storage_url}/o/{destination_blob_name}"
|
||||
|
||||
async with aiofiles.open(source_file_name, 'rb') as file:
|
||||
file_bytes = await file.read()
|
||||
|
||||
response = await self._httpx_client.post(
|
||||
upload_url,
|
||||
headers={
|
||||
'Authorization': f'Firebase {self._token}',
|
||||
"X-Goog-Upload-Protocol": "multipart"
|
||||
},
|
||||
files={
|
||||
'metadata': (None, '{"metadata":{"test":"testMetadata"}}', 'application/json'),
|
||||
'file': file_bytes
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self._logger.info(f"File {source_file_name} uploaded to {self._storage_url}/o/{destination_blob_name}.")
|
||||
|
||||
# TODO: Test this
|
||||
#await self.make_public(destination_blob_name)
|
||||
|
||||
file_url = f"{self._storage_url}/o/{destination_blob_name}"
|
||||
return file_url
|
||||
else:
|
||||
self._logger.error(f"Failed to upload file {source_file_name}. Error: {response.status_code} - {str(response.content)}")
|
||||
return None
|
||||
|
||||
async def make_public(self, destination_blob_name: str):
|
||||
acl_url = f"{self._storage_url}/o/{destination_blob_name}/acl"
|
||||
acl = {'entity': 'allUsers', 'role': 'READER'}
|
||||
|
||||
response = await self._httpx_client.post(
|
||||
acl_url,
|
||||
headers={
|
||||
'Authorization': f'Bearer {self._token}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json=acl
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self._logger.info(f"Blob {destination_blob_name} is now public.")
|
||||
else:
|
||||
self._logger.error(f"Failed to make blob {destination_blob_name} public. {response.status_code} - {response.content}")
|
||||
316
app/server.py
316
app/server.py
@@ -1,160 +1,156 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
|
||||
import aioboto3
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
import whisper
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
import nltk
|
||||
from dotenv import load_dotenv
|
||||
from starlette import status
|
||||
|
||||
from app.api import router
|
||||
from app.configs import config_di
|
||||
from app.exceptions import CustomException
|
||||
from app.middlewares import AuthenticationMiddleware, AuthBackend
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
"""
|
||||
Startup and Shutdown logic is in this lifespan method
|
||||
|
||||
https://fastapi.tiangolo.com/advanced/events/
|
||||
"""
|
||||
# Whisper model
|
||||
whisper_model = whisper.load_model("base")
|
||||
|
||||
# NLTK required datasets download
|
||||
nltk.download('words')
|
||||
nltk.download("punkt")
|
||||
|
||||
# AWS Polly client instantiation
|
||||
context_stack = contextlib.AsyncExitStack()
|
||||
session = aioboto3.Session()
|
||||
polly_client = await context_stack.enter_async_context(
|
||||
session.client(
|
||||
'polly',
|
||||
region_name='eu-west-1',
|
||||
aws_secret_access_key=os.getenv("AWS_ACCESS_KEY_ID"),
|
||||
aws_access_key_id=os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||
)
|
||||
)
|
||||
|
||||
# HTTP Client
|
||||
http_client = httpx.AsyncClient()
|
||||
|
||||
config_di(
|
||||
polly_client=polly_client,
|
||||
http_client=http_client,
|
||||
whisper_model=whisper_model
|
||||
)
|
||||
|
||||
# Setup logging
|
||||
config_file = pathlib.Path("./app/configs/logging/logging_config.json")
|
||||
with open(config_file) as f_in:
|
||||
config = json.load(f_in)
|
||||
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
yield
|
||||
|
||||
await http_client.aclose()
|
||||
await polly_client.close()
|
||||
await context_stack.aclose()
|
||||
|
||||
|
||||
def setup_listeners(_app: FastAPI) -> None:
|
||||
@_app.exception_handler(RequestValidationError)
|
||||
async def custom_form_validation_error(request, exc):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
reformatted_message = defaultdict(list)
|
||||
for pydantic_error in exc.errors():
|
||||
loc, msg = pydantic_error["loc"], pydantic_error["msg"]
|
||||
filtered_loc = loc[1:] if loc[0] in ("body", "query", "path") else loc
|
||||
field_string = ".".join(filtered_loc)
|
||||
if field_string == "cookie.refresh_token":
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"error_code": 401, "message": HTTPStatus.UNAUTHORIZED.description},
|
||||
)
|
||||
reformatted_message[field_string].append(msg)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content=jsonable_encoder(
|
||||
{"details": "Invalid request!", "errors": reformatted_message}
|
||||
),
|
||||
)
|
||||
|
||||
@_app.exception_handler(CustomException)
|
||||
async def custom_exception_handler(request: Request, exc: CustomException):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content={"error_code": exc.error_code, "message": exc.message},
|
||||
)
|
||||
|
||||
@_app.exception_handler(Exception)
|
||||
async def default_exception_handler(request: Request, exc: Exception):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def setup_middleware() -> List[Middleware]:
|
||||
middleware = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
),
|
||||
Middleware(
|
||||
AuthenticationMiddleware,
|
||||
backend=AuthBackend()
|
||||
)
|
||||
]
|
||||
return middleware
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
env = os.getenv("ENV")
|
||||
_app = FastAPI(
|
||||
docs_url="/docs" if env != "prod" else None,
|
||||
redoc_url="/redoc" if env != "prod" else None,
|
||||
middleware=setup_middleware(),
|
||||
lifespan=lifespan
|
||||
)
|
||||
_app.include_router(router)
|
||||
setup_listeners(_app)
|
||||
return _app
|
||||
|
||||
|
||||
app = create_app()
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
|
||||
import aioboto3
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
import whisper
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
import nltk
|
||||
from starlette import status
|
||||
|
||||
from app.api import router
|
||||
from app.configs import DependencyInjector
|
||||
from app.exceptions import CustomException
|
||||
from app.middlewares import AuthenticationMiddleware, AuthBackend
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
"""
|
||||
Startup and Shutdown logic is in this lifespan method
|
||||
|
||||
https://fastapi.tiangolo.com/advanced/events/
|
||||
"""
|
||||
# Whisper model
|
||||
whisper_model = whisper.load_model("base")
|
||||
|
||||
# NLTK required datasets download
|
||||
nltk.download('words')
|
||||
nltk.download("punkt")
|
||||
|
||||
# AWS Polly client instantiation
|
||||
context_stack = contextlib.AsyncExitStack()
|
||||
session = aioboto3.Session()
|
||||
polly_client = await context_stack.enter_async_context(
|
||||
session.client(
|
||||
'polly',
|
||||
region_name='eu-west-1',
|
||||
aws_secret_access_key=os.getenv("AWS_ACCESS_KEY_ID"),
|
||||
aws_access_key_id=os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||
)
|
||||
)
|
||||
|
||||
http_client = httpx.AsyncClient()
|
||||
|
||||
DependencyInjector(
|
||||
polly_client,
|
||||
http_client,
|
||||
whisper_model
|
||||
).inject()
|
||||
|
||||
# Setup logging
|
||||
config_file = pathlib.Path("./app/configs/logging/logging_config.json")
|
||||
with open(config_file) as f_in:
|
||||
config = json.load(f_in)
|
||||
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
yield
|
||||
|
||||
await http_client.aclose()
|
||||
await polly_client.close()
|
||||
await context_stack.aclose()
|
||||
|
||||
|
||||
def setup_listeners(_app: FastAPI) -> None:
|
||||
@_app.exception_handler(RequestValidationError)
|
||||
async def custom_form_validation_error(request, exc):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
reformatted_message = defaultdict(list)
|
||||
for pydantic_error in exc.errors():
|
||||
loc, msg = pydantic_error["loc"], pydantic_error["msg"]
|
||||
filtered_loc = loc[1:] if loc[0] in ("body", "query", "path") else loc
|
||||
field_string = ".".join(filtered_loc)
|
||||
if field_string == "cookie.refresh_token":
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"error_code": 401, "message": HTTPStatus.UNAUTHORIZED.description},
|
||||
)
|
||||
reformatted_message[field_string].append(msg)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content=jsonable_encoder(
|
||||
{"details": "Invalid request!", "errors": reformatted_message}
|
||||
),
|
||||
)
|
||||
|
||||
@_app.exception_handler(CustomException)
|
||||
async def custom_exception_handler(request: Request, exc: CustomException):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content={"error_code": exc.error_code, "message": exc.message},
|
||||
)
|
||||
|
||||
@_app.exception_handler(Exception)
|
||||
async def default_exception_handler(request: Request, exc: Exception):
|
||||
"""
|
||||
Don't delete request param
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def setup_middleware() -> List[Middleware]:
|
||||
middleware = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
),
|
||||
Middleware(
|
||||
AuthenticationMiddleware,
|
||||
backend=AuthBackend()
|
||||
)
|
||||
]
|
||||
return middleware
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
env = os.getenv("ENV")
|
||||
_app = FastAPI(
|
||||
docs_url="/docs" if env != "production" else None,
|
||||
redoc_url="/redoc" if env != "production" else None,
|
||||
middleware=setup_middleware(),
|
||||
lifespan=lifespan
|
||||
)
|
||||
_app.include_router(router)
|
||||
setup_listeners(_app)
|
||||
return _app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
from .level import ILevelService
|
||||
from .listening import IListeningService
|
||||
from .writing import IWritingService
|
||||
from .speaking import ISpeakingService
|
||||
from .reading import IReadingService
|
||||
from .grade import IGradeService
|
||||
from .training import ITrainingService
|
||||
from .kb import IKnowledgeBase
|
||||
from .third_parties import *
|
||||
|
||||
__all__ = [
|
||||
"ILevelService",
|
||||
"IListeningService",
|
||||
"IWritingService",
|
||||
"ISpeakingService",
|
||||
"IReadingService",
|
||||
"IGradeService",
|
||||
"ITrainingService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
from .third_parties import *
|
||||
from .exam import *
|
||||
from .training import *
|
||||
from .user import IUserService
|
||||
|
||||
__all__ = [
|
||||
"IUserService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
__all__.extend(exam.__all__)
|
||||
__all__.extend(training.__all__)
|
||||
|
||||
15
app/services/abc/exam/__init__.py
Normal file
15
app/services/abc/exam/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from .level import ILevelService
|
||||
from .listening import IListeningService
|
||||
from .writing import IWritingService
|
||||
from .speaking import ISpeakingService
|
||||
from .reading import IReadingService
|
||||
from .grade import IGradeService
|
||||
|
||||
__all__ = [
|
||||
"ILevelService",
|
||||
"IListeningService",
|
||||
"IWritingService",
|
||||
"ISpeakingService",
|
||||
"IReadingService",
|
||||
"IGradeService",
|
||||
]
|
||||
@@ -1,13 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
pass
|
||||
@@ -1,47 +1,47 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import EducationalContent
|
||||
|
||||
|
||||
class ILevelService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(
|
||||
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, upload: UploadFile) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_multiple_choice(
|
||||
self, mc_variant: str, quantity: int, start_id: int = 1, *, utas: bool = False, all_exams=None
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_blank_space_text_utas(
|
||||
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import EducationalContent
|
||||
|
||||
|
||||
class ILevelService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(
|
||||
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, upload: UploadFile) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_multiple_choice(
|
||||
self, mc_variant: str, quantity: int, start_id: int = 1, *, utas: bool = False, all_exams=None
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_blank_space_text_utas(
|
||||
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
pass
|
||||
@@ -1,18 +1,18 @@
|
||||
import queue
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IListeningService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_listening_question(
|
||||
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str,
|
||||
number_of_exercises_q=queue.Queue(), start_id=-1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str, listening_id: str) -> Dict:
|
||||
pass
|
||||
import queue
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IListeningService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_listening_question(
|
||||
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str,
|
||||
number_of_exercises_q=queue.Queue(), start_id=-1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str, listening_id: str) -> Dict:
|
||||
pass
|
||||
@@ -1,22 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import List
|
||||
|
||||
|
||||
class IReadingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage(
|
||||
self,
|
||||
passage_id: int,
|
||||
topic: str,
|
||||
req_exercises: List[str],
|
||||
number_of_exercises_q: Queue,
|
||||
difficulty: str,
|
||||
start_id: int
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_passage(self, part: int, topic: str, word_count: int = 800):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import List
|
||||
|
||||
|
||||
class IReadingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage(
|
||||
self,
|
||||
passage_id: int,
|
||||
topic: str,
|
||||
req_exercises: List[str],
|
||||
number_of_exercises_q: Queue,
|
||||
difficulty: str,
|
||||
start_id: int
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_passage(self, part: int, topic: str, word_count: int = 800):
|
||||
pass
|
||||
@@ -1,29 +1,29 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ISpeakingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(
|
||||
self, part: int, topic: str, difficulty: str, second_topic: Optional[str] = None
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_videos_and_save_to_db(self, exercises: List[Dict], template: Dict, req_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(
|
||||
self, part: int, avatar: str, topic: str, questions: list[str],
|
||||
*,
|
||||
second_topic: Optional[str] = None,
|
||||
prompts: Optional[list[str]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ISpeakingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(
|
||||
self, part: int, topic: str, difficulty: str, second_topic: Optional[str] = None
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_videos_and_save_to_db(self, exercises: List[Dict], template: Dict, req_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(
|
||||
self, part: int, avatar: str, topic: str, questions: list[str],
|
||||
*,
|
||||
second_topic: Optional[str] = None,
|
||||
prompts: Optional[list[str]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
@@ -1,11 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class IWritingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class IWritingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
pass
|
||||
@@ -1,13 +1,13 @@
|
||||
from .stt import ISpeechToTextService
|
||||
from .tts import ITextToSpeechService
|
||||
from .llm import ILLMService
|
||||
from .vid_gen import IVideoGeneratorService
|
||||
from .ai_detector import IAIDetectorService
|
||||
|
||||
__all__ = [
|
||||
"ISpeechToTextService",
|
||||
"ITextToSpeechService",
|
||||
"ILLMService",
|
||||
"IVideoGeneratorService",
|
||||
"IAIDetectorService"
|
||||
]
|
||||
from .stt import ISpeechToTextService
|
||||
from .tts import ITextToSpeechService
|
||||
from .llm import ILLMService
|
||||
from .vid_gen import IVideoGeneratorService
|
||||
from .ai_detector import IAIDetectorService
|
||||
|
||||
__all__ = [
|
||||
"ISpeechToTextService",
|
||||
"ITextToSpeechService",
|
||||
"ILLMService",
|
||||
"IVideoGeneratorService",
|
||||
"IAIDetectorService"
|
||||
]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class IAIDetectorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def run_detection(self, text: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class IAIDetectorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def run_detection(self, text: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
pass
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, TypeVar, Callable
|
||||
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
class ILLMService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def prediction_override(self, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def pydantic_prediction(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
map_to_model: Callable,
|
||||
json_scheme: str,
|
||||
*,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_retries: int = 3
|
||||
) -> List[T] | T | None:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, TypeVar, Callable
|
||||
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
class ILLMService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def prediction_override(self, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def pydantic_prediction(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
map_to_model: Callable,
|
||||
json_scheme: str,
|
||||
*,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_retries: int = 3
|
||||
) -> List[T] | T | None:
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ISpeechToTextService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def speech_to_text(self, file_path):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ISpeechToTextService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def speech_to_text(self, file_path):
|
||||
pass
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ITextToSpeechService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str, output_format: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _text_to_speech(self, text: str):
|
||||
pass
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ITextToSpeechService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str, output_format: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _text_to_speech(self, text: str):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.configs.constants import AvatarEnum
|
||||
|
||||
|
||||
class IVideoGeneratorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.configs.constants import AvatarEnum
|
||||
|
||||
|
||||
class IVideoGeneratorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
pass
|
||||
|
||||
7
app/services/abc/training/__init__.py
Normal file
7
app/services/abc/training/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .training import ITrainingService
|
||||
from .kb import IKnowledgeBase
|
||||
|
||||
__all__ = [
|
||||
"ITrainingService",
|
||||
"IKnowledgeBase"
|
||||
]
|
||||
@@ -1,10 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class IKnowledgeBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def query_knowledge_base(self, query: str, category: str, top_k: int = 5) -> List[Dict[str, str]]:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class IKnowledgeBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def query_knowledge_base(self, query: str, category: str, top_k: int = 5) -> List[Dict[str, str]]:
|
||||
pass
|
||||
@@ -1,14 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ITrainingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, training_content: Dict) -> Dict:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ITrainingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, training_content: Dict) -> Dict:
|
||||
pass
|
||||
10
app/services/abc/user.py
Normal file
10
app/services/abc/user.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.dtos.user_batch import BatchUsersDTO
|
||||
|
||||
|
||||
class IUserService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, batch: BatchUsersDTO):
|
||||
pass
|
||||
@@ -1,19 +1,11 @@
|
||||
from .level import LevelService
|
||||
from .listening import ListeningService
|
||||
from .reading import ReadingService
|
||||
from .speaking import SpeakingService
|
||||
from .writing import WritingService
|
||||
from .grade import GradeService
|
||||
from .training import *
|
||||
from .third_parties import *
|
||||
|
||||
__all__ = [
|
||||
"LevelService",
|
||||
"ListeningService",
|
||||
"ReadingService",
|
||||
"SpeakingService",
|
||||
"WritingService",
|
||||
"GradeService",
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
__all__.extend(training.__all__)
|
||||
from .user import UserService
|
||||
from .training import *
|
||||
from .third_parties import *
|
||||
from .exam import *
|
||||
|
||||
__all__ = [
|
||||
"UserService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
__all__.extend(training.__all__)
|
||||
__all__.extend(exam.__all__)
|
||||
|
||||
16
app/services/impl/exam/__init__.py
Normal file
16
app/services/impl/exam/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from .level import LevelService
|
||||
from .listening import ListeningService
|
||||
from .reading import ReadingService
|
||||
from .speaking import SpeakingService
|
||||
from .writing import WritingService
|
||||
from .grade import GradeService
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LevelService",
|
||||
"ListeningService",
|
||||
"ReadingService",
|
||||
"SpeakingService",
|
||||
"WritingService",
|
||||
"GradeService",
|
||||
]
|
||||
@@ -1,200 +1,200 @@
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings
|
||||
from app.services.abc import ILLMService, IGradeService
|
||||
|
||||
|
||||
class GradeService(IGradeService):
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
json_format = {
|
||||
"exercises": [
|
||||
{
|
||||
"id": 1,
|
||||
"correct": True,
|
||||
"correct_answer": " correct answer if wrong"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Grade these answers according to the text content and write a correct answer if they are '
|
||||
f'wrong. Text, questions and answers:\n {data}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["exercises"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
ret = []
|
||||
|
||||
for section in extracted_sections:
|
||||
openai_response_dict = await self._calculate_section_grade_summary(section)
|
||||
ret.append(
|
||||
{
|
||||
'code': section['code'],
|
||||
'name': section['name'],
|
||||
'grade': section['grade'],
|
||||
'evaluation': openai_response_dict['evaluation'],
|
||||
'suggestions': openai_response_dict['suggestions'],
|
||||
'bullet_points': self._parse_bullet_points(openai_response_dict['bullet_points'], section['grade'])
|
||||
}
|
||||
)
|
||||
|
||||
return {'sections': ret}
|
||||
|
||||
async def _calculate_section_grade_summary(self, section):
|
||||
section_name = section['name']
|
||||
section_grade = section['grade']
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'You are a IELTS test section grade evaluator. You will receive a IELTS test section name and the '
|
||||
'grade obtained in the section. You should offer a evaluation comment on this grade and separately '
|
||||
'suggestions on how to possibly get a better grade.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Section: {str(section_name)} Grade: {str(section_grade)}',
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Speak in third person."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Don't offer suggestions in the evaluation comment. Only in the suggestions section."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your evaluation comment on the grade should enunciate the grade, be insightful, be speculative, "
|
||||
"be one paragraph long."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please save the evaluation comment and suggestions generated."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Offer bullet points to improve the english {str(section_name)} ability."
|
||||
},
|
||||
]
|
||||
|
||||
if section['code'] == "level":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is comprised of multiple choice questions that measure the user's overall english "
|
||||
"level. These multiple choice questions are about knowledge on vocabulary, syntax, grammar rules, "
|
||||
"and contextual usage. The grade obtained measures the ability in these areas and english language "
|
||||
"overall."
|
||||
)
|
||||
}]
|
||||
elif section['code'] == "speaking":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is s designed to assess the English language proficiency of individuals who want to "
|
||||
"study or work in English-speaking countries. The speaking section evaluates a candidate's ability "
|
||||
"to communicate effectively in spoken English."
|
||||
)
|
||||
}]
|
||||
|
||||
chat_config = {'max_tokens': 1000, 'temperature': 0.2}
|
||||
tools = self.get_tools()
|
||||
|
||||
res = await self._llm.prediction_override(
|
||||
model="gpt-3.5-turbo",
|
||||
max_tokens=chat_config['max_tokens'],
|
||||
temperature=chat_config['temperature'],
|
||||
tools=tools,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
return self._parse_openai_response(res)
|
||||
|
||||
@staticmethod
|
||||
def _parse_openai_response(response):
|
||||
if 'choices' in response and len(response['choices']) > 0 and 'message' in response['choices'][
|
||||
0] and 'tool_calls' in response['choices'][0]['message'] and isinstance(
|
||||
response['choices'][0]['message']['tool_calls'], list) and len(
|
||||
response['choices'][0]['message']['tool_calls']) > 0 and \
|
||||
response['choices'][0]['message']['tool_calls'][0]['function']['arguments']:
|
||||
return json.loads(response['choices'][0]['message']['tool_calls'][0]['function']['arguments'])
|
||||
else:
|
||||
return {'evaluation': "", 'suggestions': "", 'bullet_points': []}
|
||||
|
||||
@staticmethod
|
||||
def _parse_bullet_points(bullet_points_str, grade):
|
||||
max_grade_for_suggestions = 9
|
||||
if isinstance(bullet_points_str, str) and grade < max_grade_for_suggestions:
|
||||
# Split the string by '\n'
|
||||
lines = bullet_points_str.split('\n')
|
||||
|
||||
# Remove '-' and trim whitespace from each line
|
||||
cleaned_lines = [line.replace('-', '').strip() for line in lines]
|
||||
|
||||
# Add '.' to lines that don't end with it
|
||||
return [line + '.' if line and not line.endswith('.') else line for line in cleaned_lines]
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_tools():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "save_evaluation_and_suggestions",
|
||||
"description": "Saves the evaluation and suggestions requested by input.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"evaluation": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A comment on the IELTS section grade obtained in the specific section and what "
|
||||
"it could mean without suggestions."
|
||||
),
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A small paragraph text with suggestions on how to possibly get a better grade "
|
||||
"than the one obtained."
|
||||
),
|
||||
},
|
||||
"bullet_points": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Text with four bullet points to improve the english speaking ability. Only "
|
||||
"include text for the bullet points separated by a paragraph."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["evaluation", "suggestions"],
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings
|
||||
from app.services.abc import ILLMService, IGradeService
|
||||
|
||||
|
||||
class GradeService(IGradeService):
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
json_format = {
|
||||
"exercises": [
|
||||
{
|
||||
"id": 1,
|
||||
"correct": True,
|
||||
"correct_answer": " correct answer if wrong"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Grade these answers according to the text content and write a correct answer if they are '
|
||||
f'wrong. Text, questions and answers:\n {data}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["exercises"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
ret = []
|
||||
|
||||
for section in extracted_sections:
|
||||
openai_response_dict = await self._calculate_section_grade_summary(section)
|
||||
ret.append(
|
||||
{
|
||||
'code': section['code'],
|
||||
'name': section['name'],
|
||||
'grade': section['grade'],
|
||||
'evaluation': openai_response_dict['evaluation'],
|
||||
'suggestions': openai_response_dict['suggestions'],
|
||||
'bullet_points': self._parse_bullet_points(openai_response_dict['bullet_points'], section['grade'])
|
||||
}
|
||||
)
|
||||
|
||||
return {'sections': ret}
|
||||
|
||||
async def _calculate_section_grade_summary(self, section):
|
||||
section_name = section['name']
|
||||
section_grade = section['grade']
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'You are a IELTS test section grade evaluator. You will receive a IELTS test section name and the '
|
||||
'grade obtained in the section. You should offer a evaluation comment on this grade and separately '
|
||||
'suggestions on how to possibly get a better grade.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Section: {str(section_name)} Grade: {str(section_grade)}',
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Speak in third person."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Don't offer suggestions in the evaluation comment. Only in the suggestions section."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your evaluation comment on the grade should enunciate the grade, be insightful, be speculative, "
|
||||
"be one paragraph long."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please save the evaluation comment and suggestions generated."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Offer bullet points to improve the english {str(section_name)} ability."
|
||||
},
|
||||
]
|
||||
|
||||
if section['code'] == "level":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is comprised of multiple choice questions that measure the user's overall english "
|
||||
"level. These multiple choice questions are about knowledge on vocabulary, syntax, grammar rules, "
|
||||
"and contextual usage. The grade obtained measures the ability in these areas and english language "
|
||||
"overall."
|
||||
)
|
||||
}]
|
||||
elif section['code'] == "speaking":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is s designed to assess the English language proficiency of individuals who want to "
|
||||
"study or work in English-speaking countries. The speaking section evaluates a candidate's ability "
|
||||
"to communicate effectively in spoken English."
|
||||
)
|
||||
}]
|
||||
|
||||
chat_config = {'max_tokens': 1000, 'temperature': 0.2}
|
||||
tools = self.get_tools()
|
||||
|
||||
res = await self._llm.prediction_override(
|
||||
model="gpt-3.5-turbo",
|
||||
max_tokens=chat_config['max_tokens'],
|
||||
temperature=chat_config['temperature'],
|
||||
tools=tools,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
return self._parse_openai_response(res)
|
||||
|
||||
@staticmethod
|
||||
def _parse_openai_response(response):
|
||||
if 'choices' in response and len(response['choices']) > 0 and 'message' in response['choices'][
|
||||
0] and 'tool_calls' in response['choices'][0]['message'] and isinstance(
|
||||
response['choices'][0]['message']['tool_calls'], list) and len(
|
||||
response['choices'][0]['message']['tool_calls']) > 0 and \
|
||||
response['choices'][0]['message']['tool_calls'][0]['function']['arguments']:
|
||||
return json.loads(response['choices'][0]['message']['tool_calls'][0]['function']['arguments'])
|
||||
else:
|
||||
return {'evaluation': "", 'suggestions': "", 'bullet_points': []}
|
||||
|
||||
@staticmethod
|
||||
def _parse_bullet_points(bullet_points_str, grade):
|
||||
max_grade_for_suggestions = 9
|
||||
if isinstance(bullet_points_str, str) and grade < max_grade_for_suggestions:
|
||||
# Split the string by '\n'
|
||||
lines = bullet_points_str.split('\n')
|
||||
|
||||
# Remove '-' and trim whitespace from each line
|
||||
cleaned_lines = [line.replace('-', '').strip() for line in lines]
|
||||
|
||||
# Add '.' to lines that don't end with it
|
||||
return [line + '.' if line and not line.endswith('.') else line for line in cleaned_lines]
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_tools():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "save_evaluation_and_suggestions",
|
||||
"description": "Saves the evaluation and suggestions requested by input.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"evaluation": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A comment on the IELTS section grade obtained in the specific section and what "
|
||||
"it could mean without suggestions."
|
||||
),
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A small paragraph text with suggestions on how to possibly get a better grade "
|
||||
"than the one obtained."
|
||||
),
|
||||
},
|
||||
"bullet_points": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Text with four bullet points to improve the english speaking ability. Only "
|
||||
"include text for the bullet points separated by a paragraph."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["evaluation", "suggestions"],
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
from .level import LevelService
|
||||
|
||||
__all__ = [
|
||||
"LevelService"
|
||||
from .level import LevelService
|
||||
|
||||
__all__ = [
|
||||
"LevelService"
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user