Merged in release/async (pull request #44)
Release/async Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -29,6 +29,11 @@ RUN apt update && apt install -y \
|
||||
librsvg2-bin \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN npm install -g firebase-tools
|
||||
|
||||
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
@@ -38,4 +43,4 @@ EXPOSE 8000
|
||||
# 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.
|
||||
ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "--timeout", "0", "-k", "uvicorn.workers.UvicornWorker", "app.server:app"]
|
||||
ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "--timeout", "0", "-k", "uvicorn.workers.UvicornWorker", "ielts_be:app"]
|
||||
|
||||
2
app.py
2
app.py
@@ -13,7 +13,7 @@ load_dotenv()
|
||||
)
|
||||
def main(env: str):
|
||||
uvicorn.run(
|
||||
app="app.server:app",
|
||||
app="ielts_be:app",
|
||||
host="localhost",
|
||||
port=8000,
|
||||
reload=True if env != "production" else False,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
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
|
||||
from .level import level_router
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Home"])
|
||||
|
||||
@router.get('/healthcheck')
|
||||
async def healthcheck():
|
||||
return {"healthy": True}
|
||||
|
||||
exercises_router = APIRouter()
|
||||
exercises_router.include_router(listening_router, prefix="/listening", tags=["Listening"])
|
||||
exercises_router.include_router(reading_router, prefix="/reading", tags=["Reading"])
|
||||
exercises_router.include_router(speaking_router, prefix="/speaking", tags=["Speaking"])
|
||||
exercises_router.include_router(writing_router, prefix="/writing", tags=["Writing"])
|
||||
exercises_router.include_router(level_router, prefix="/level", tags=["Level"])
|
||||
|
||||
router.include_router(grade_router, prefix="/grade", tags=["Grade"])
|
||||
router.include_router(training_router, prefix="/training", tags=["Training"])
|
||||
router.include_router(user_router, prefix="/user", tags=["Users"])
|
||||
router.include_router(exercises_router)
|
||||
@@ -1,27 +0,0 @@
|
||||
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 generate_writing(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
difficulty: str = Query(default=None),
|
||||
topic: str = Query(default=None),
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
|
||||
topic = random.choice(EducationalContent.MTI_TOPICS) if not topic else topic
|
||||
return await writing_controller.get_writing_task_general_question(task, topic, difficulty)
|
||||
@@ -1,10 +0,0 @@
|
||||
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 +0,0 @@
|
||||
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,11 +0,0 @@
|
||||
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)
|
||||
@@ -1,8 +0,0 @@
|
||||
from .document_stores import *
|
||||
from app.repositories.impl.file_storage.firebase import FirebaseStorage
|
||||
|
||||
__all__ = [
|
||||
"FirebaseStorage"
|
||||
]
|
||||
|
||||
__all__.extend(document_stores.__all__)
|
||||
@@ -1,156 +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
|
||||
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
|
||||
from app.services.impl import OpenAIWhisper
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
"""
|
||||
Startup and Shutdown logic is in this lifespan method
|
||||
|
||||
https://fastapi.tiangolo.com/advanced/events/
|
||||
"""
|
||||
|
||||
# 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_SECRET_ACCESS_KEY"),
|
||||
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID")
|
||||
)
|
||||
)
|
||||
|
||||
http_client = httpx.AsyncClient()
|
||||
stt = OpenAIWhisper()
|
||||
|
||||
DependencyInjector(
|
||||
polly_client,
|
||||
http_client,
|
||||
stt
|
||||
).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
|
||||
|
||||
stt.close()
|
||||
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()
|
||||
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
|
||||
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 ielts_be.api import router
|
||||
from ielts_be.configs import DependencyInjector
|
||||
from ielts_be.exceptions import CustomException
|
||||
from ielts_be.middlewares import AuthenticationMiddleware, AuthBackend
|
||||
from ielts_be.services.impl import OpenAIWhisper
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
"""
|
||||
Startup and Shutdown logic is in this lifespan method
|
||||
|
||||
https://fastapi.tiangolo.com/advanced/events/
|
||||
"""
|
||||
|
||||
# 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_SECRET_ACCESS_KEY"),
|
||||
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID")
|
||||
)
|
||||
)
|
||||
|
||||
http_client = httpx.AsyncClient()
|
||||
stt = OpenAIWhisper()
|
||||
|
||||
DependencyInjector(
|
||||
polly_client,
|
||||
http_client,
|
||||
stt
|
||||
).inject()
|
||||
|
||||
# Setup logging
|
||||
config_file = pathlib.Path("./ielts_be/configs/logging/logging_config.json")
|
||||
with open(config_file) as f_in:
|
||||
config = json.load(f_in)
|
||||
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
yield
|
||||
|
||||
stt.close()
|
||||
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()
|
||||
15
ielts_be/api/__init__.py
Normal file
15
ielts_be/api/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .training import training_router
|
||||
from .user import user_router
|
||||
from .exam import exam_router
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Home"])
|
||||
|
||||
@router.get('/healthcheck')
|
||||
async def healthcheck():
|
||||
return {"healthy": True}
|
||||
|
||||
router.include_router(training_router, prefix="/training", tags=["Training"])
|
||||
router.include_router(user_router, prefix="/user", tags=["Users"])
|
||||
router.include_router(exam_router)
|
||||
16
ielts_be/api/exam/__init__.py
Normal file
16
ielts_be/api/exam/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .listening import listening_router
|
||||
from .reading import reading_router
|
||||
from .speaking import speaking_router
|
||||
from .writing import writing_router
|
||||
from .level import level_router
|
||||
from .grade import grade_router
|
||||
|
||||
exam_router = APIRouter()
|
||||
exam_router.include_router(listening_router, prefix="/listening", tags=["Listening"])
|
||||
exam_router.include_router(reading_router, prefix="/reading", tags=["Reading"])
|
||||
exam_router.include_router(speaking_router, prefix="/speaking", tags=["Speaking"])
|
||||
exam_router.include_router(writing_router, prefix="/writing", tags=["Writing"])
|
||||
exam_router.include_router(level_router, prefix="/level", tags=["Level"])
|
||||
exam_router.include_router(grade_router, prefix="/grade", tags=["Grade"])
|
||||
@@ -1,9 +1,9 @@
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Depends, Path, Request, BackgroundTasks
|
||||
|
||||
from app.controllers.abc import IGradeController
|
||||
from app.dtos.writing import WritingGradeTaskDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IGradeController
|
||||
from ielts_be.dtos.writing import WritingGradeTaskDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
|
||||
controller = "grade_controller"
|
||||
grade_router = APIRouter()
|
||||
@@ -1,9 +1,9 @@
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, UploadFile, Request
|
||||
|
||||
from app.dtos.level import LevelExercisesDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import ILevelController
|
||||
from ielts_be.dtos.level import LevelExercisesDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import ILevelController
|
||||
|
||||
controller = "level_controller"
|
||||
level_router = APIRouter()
|
||||
@@ -3,10 +3,10 @@ import random
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile
|
||||
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import IListeningController
|
||||
from app.configs.constants import EducationalContent, ListeningExerciseType
|
||||
from app.dtos.listening import SaveListeningDTO, GenerateListeningExercises, Dialog
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IListeningController
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.dtos.listening import GenerateListeningExercises, Dialog
|
||||
|
||||
controller = "listening_controller"
|
||||
listening_router = APIRouter()
|
||||
@@ -4,10 +4,10 @@ from typing import Optional
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile
|
||||
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.dtos.reading import ReadingDTO
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.controllers.abc import IReadingController
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.dtos.reading import ReadingDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IReadingController
|
||||
|
||||
controller = "reading_controller"
|
||||
reading_router = APIRouter()
|
||||
@@ -4,10 +4,10 @@ from typing import Optional
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends
|
||||
|
||||
from app.dtos.speaking import Video
|
||||
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from app.configs.constants import EducationalContent
|
||||
from app.controllers.abc import ISpeakingController
|
||||
from ielts_be.dtos.speaking import Video
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.controllers import ISpeakingController
|
||||
|
||||
controller = "speaking_controller"
|
||||
speaking_router = APIRouter()
|
||||
42
ielts_be/api/exam/writing.py
Normal file
42
ielts_be/api/exam/writing.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import random
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import APIRouter, Path, Query, Depends, UploadFile, File
|
||||
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.configs.constants import EducationalContent
|
||||
from ielts_be.controllers import IWritingController
|
||||
|
||||
controller = "writing_controller"
|
||||
writing_router = APIRouter()
|
||||
|
||||
|
||||
@writing_router.post(
|
||||
'/{task}/attachment',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_writing_academic(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
file: UploadFile = File(...),
|
||||
difficulty: str = Query(default=None),
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
|
||||
return await writing_controller.get_writing_task_academic_question(task, file, difficulty)
|
||||
|
||||
|
||||
@writing_router.get(
|
||||
'/{task}',
|
||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||
)
|
||||
@inject
|
||||
async def generate_writing(
|
||||
task: int = Path(..., ge=1, le=2),
|
||||
difficulty: str = Query(default=None),
|
||||
topic: str = Query(default=None),
|
||||
writing_controller: IWritingController = Depends(Provide[controller])
|
||||
):
|
||||
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
|
||||
topic = random.choice(EducationalContent.MTI_TOPICS) if not topic else topic
|
||||
return await writing_controller.get_writing_task_general_question(task, topic, difficulty)
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
from ielts_be.dtos.training import FetchTipsDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import ITrainingController
|
||||
|
||||
controller = "training_controller"
|
||||
training_router = APIRouter()
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
from ielts_be.dtos.user_batch import BatchUsersDTO
|
||||
from ielts_be.middlewares import Authorized, IsAuthenticatedViaBearerToken
|
||||
from ielts_be.controllers import IUserController
|
||||
|
||||
controller = "user_controller"
|
||||
user_router = APIRouter()
|
||||
@@ -9,11 +9,9 @@ from httpx import AsyncClient as HTTPClient
|
||||
from dotenv import load_dotenv
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
from app.repositories.impl import *
|
||||
from app.repositories.impl.document_stores.mongo import MongoDB
|
||||
from app.services.impl import *
|
||||
from app.controllers.impl import *
|
||||
from app.services.impl.exam.evaluation import EvaluationService
|
||||
from ielts_be.repositories.impl import *
|
||||
from ielts_be.services.impl import *
|
||||
from ielts_be.controllers.impl import *
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -33,7 +31,7 @@ class DependencyInjector:
|
||||
self._setup_services()
|
||||
self._setup_controllers()
|
||||
self._container.wire(
|
||||
packages=["app"]
|
||||
packages=["ielts_be"]
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -48,14 +46,14 @@ class DependencyInjector:
|
||||
self._container.tts = providers.Factory(AWSPolly, client=self._container.polly_client)
|
||||
|
||||
"""
|
||||
with open('app/services/impl/third_parties/elai/conf.json', 'r') as file:
|
||||
with open('ielts_be/services/impl/third_parties/elai/conf.json', 'r') as file:
|
||||
elai_conf = json.load(file)
|
||||
|
||||
with open('app/services/impl/third_parties/elai/avatars.json', 'r') as file:
|
||||
with open('ielts_be/services/impl/third_parties/elai/avatars.json', 'r') as file:
|
||||
elai_avatars = json.load(file)
|
||||
"""
|
||||
|
||||
with open('app/services/impl/third_parties/heygen/avatars.json', 'r') as file:
|
||||
with open('ielts_be/services/impl/third_parties/heygen/avatars.json', 'r') as file:
|
||||
heygen_avatars = json.load(file)
|
||||
|
||||
self._container.vid_gen = providers.Factory(
|
||||
@@ -96,10 +94,10 @@ class DependencyInjector:
|
||||
)
|
||||
|
||||
self._container.writing_service = providers.Factory(
|
||||
WritingService, llm=self._container.llm, ai_detector=self._container.ai_detector
|
||||
WritingService, llm=self._container.llm, ai_detector=self._container.ai_detector, file_storage=self._container.firebase_instance
|
||||
)
|
||||
|
||||
with open('app/services/impl/exam/level/mc_variants.json', 'r') as file:
|
||||
with open('ielts_be/services/impl/exam/level/mc_variants.json', 'r') as file:
|
||||
mc_variants = json.load(file)
|
||||
|
||||
self._container.level_service = providers.Factory(
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"filters": {
|
||||
"error_and_above": {
|
||||
"()": "app.configs.logging.ErrorAndAboveFilter"
|
||||
"()": "ielts_be.configs.logging.ErrorAndAboveFilter"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
@@ -33,7 +33,7 @@
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"queue_handler": {
|
||||
"class": "app.configs.logging.QueueListenerHandler",
|
||||
"class": "ielts_be.configs.logging.QueueListenerHandler",
|
||||
"handlers": [
|
||||
"cfg://handlers.console",
|
||||
"cfg://handlers.error"
|
||||
@@ -4,7 +4,7 @@ from queue import Queue
|
||||
import atexit
|
||||
|
||||
|
||||
class QueueHnadlerHelper:
|
||||
class QueueHandlerHelper:
|
||||
|
||||
@staticmethod
|
||||
def resolve_handlers(l):
|
||||
@@ -40,9 +40,9 @@ class QueueHnadlerHelper:
|
||||
class QueueListenerHandler(QueueHandler):
|
||||
|
||||
def __init__(self, handlers, respect_handler_level=False, auto_run=True, queue=Queue(-1)):
|
||||
queue = QueueHnadlerHelper.resolve_queue(queue)
|
||||
queue = QueueHandlerHelper.resolve_queue(queue)
|
||||
super().__init__(queue)
|
||||
handlers = QueueHnadlerHelper.resolve_handlers(handlers)
|
||||
handlers = QueueHandlerHelper.resolve_handlers(handlers)
|
||||
self._listener = QueueListener(
|
||||
self.queue,
|
||||
*handlers,
|
||||
3
ielts_be/controllers/__init__.py
Normal file
3
ielts_be/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .abc import *
|
||||
|
||||
__all__ = abc.__all__
|
||||
11
ielts_be/controllers/abc/__init__.py
Normal file
11
ielts_be/controllers/abc/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .grade import IGradeController
|
||||
from .training import ITrainingController
|
||||
from .user import IUserController
|
||||
from .exam import *
|
||||
|
||||
__all__ = [
|
||||
"IGradeController",
|
||||
"ITrainingController",
|
||||
"IUserController",
|
||||
]
|
||||
__all__.extend(exam.__all__)
|
||||
@@ -1,19 +1,13 @@
|
||||
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",
|
||||
]
|
||||
from .level import ILevelController
|
||||
from .listening import IListeningController
|
||||
from .reading import IReadingController
|
||||
from .writing import IWritingController
|
||||
from .speaking import ISpeakingController
|
||||
|
||||
__all__ = [
|
||||
"IListeningController",
|
||||
"IReadingController",
|
||||
"IWritingController",
|
||||
"ISpeakingController",
|
||||
"ILevelController",
|
||||
]
|
||||
@@ -1,7 +1,4 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
|
||||
class ISpeakingController(ABC):
|
||||
14
ielts_be/controllers/abc/exam/writing.py
Normal file
14
ielts_be/controllers/abc/exam/writing.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from fastapi.datastructures import UploadFile
|
||||
|
||||
|
||||
class IWritingController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
|
||||
pass
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict
|
||||
from fastapi import BackgroundTasks
|
||||
from fastapi.datastructures import FormData
|
||||
|
||||
8
ielts_be/controllers/abc/user.py
Normal file
8
ielts_be/controllers/abc/user.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IUserController(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def batch_import(self, batch):
|
||||
pass
|
||||
12
ielts_be/controllers/impl/__init__.py
Normal file
12
ielts_be/controllers/impl/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .training import TrainingController
|
||||
from .grade import GradeController
|
||||
from .user import UserController
|
||||
from .exam import *
|
||||
|
||||
__all__ = [
|
||||
"TrainingController",
|
||||
"GradeController",
|
||||
"UserController"
|
||||
]
|
||||
|
||||
__all__.extend(exam.__all__)
|
||||
@@ -1,19 +1,13 @@
|
||||
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"
|
||||
]
|
||||
from .level import LevelController
|
||||
from .listening import ListeningController
|
||||
from .reading import ReadingController
|
||||
from .speaking import SpeakingController
|
||||
from .writing import WritingController
|
||||
|
||||
__all__ = [
|
||||
"LevelController",
|
||||
"ListeningController",
|
||||
"ReadingController",
|
||||
"SpeakingController",
|
||||
"WritingController",
|
||||
]
|
||||
@@ -1,10 +1,8 @@
|
||||
from fastapi import UploadFile
|
||||
from typing import Dict, Optional
|
||||
|
||||
from watchfiles import awatch
|
||||
|
||||
from app.controllers.abc import ILevelController
|
||||
from app.services.abc import ILevelService
|
||||
from ielts_be.controllers import ILevelController
|
||||
from ielts_be.services import ILevelService
|
||||
|
||||
|
||||
class LevelController(ILevelController):
|
||||
@@ -3,9 +3,9 @@ import io
|
||||
from fastapi import UploadFile
|
||||
from starlette.responses import StreamingResponse, Response
|
||||
|
||||
from app.controllers.abc import IListeningController
|
||||
from app.dtos.listening import GenerateListeningExercises, Dialog
|
||||
from app.services.abc import IListeningService
|
||||
from ielts_be.controllers import IListeningController
|
||||
from ielts_be.services import IListeningService
|
||||
from ielts_be.dtos.listening import GenerateListeningExercises, Dialog
|
||||
|
||||
|
||||
class ListeningController(IListeningController):
|
||||
@@ -3,9 +3,9 @@ from typing import Optional
|
||||
|
||||
from fastapi import UploadFile, Response
|
||||
|
||||
from app.controllers.abc import IReadingController
|
||||
from app.dtos.reading import ReadingDTO
|
||||
from app.services.abc import IReadingService
|
||||
from ielts_be.controllers import IReadingController
|
||||
from ielts_be.services import IReadingService
|
||||
from ielts_be.dtos.reading import ReadingDTO
|
||||
|
||||
|
||||
class ReadingController(IReadingController):
|
||||
@@ -1,9 +1,7 @@
|
||||
import logging
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from app.controllers.abc import ISpeakingController
|
||||
from app.services.abc import ISpeakingService, IVideoGeneratorService
|
||||
from ielts_be.controllers import ISpeakingController
|
||||
from ielts_be.services import ISpeakingService, IVideoGeneratorService
|
||||
|
||||
|
||||
class SpeakingController(ISpeakingController):
|
||||
18
ielts_be/controllers/impl/exam/writing.py
Normal file
18
ielts_be/controllers/impl/exam/writing.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import UploadFile, HTTPException
|
||||
|
||||
from ielts_be.controllers import IWritingController
|
||||
from ielts_be.services 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)
|
||||
|
||||
async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
|
||||
if attachment.content_type not in ['image/jpeg', 'image/png']:
|
||||
raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG and PNG allowed.")
|
||||
return await self._service.get_writing_task_academic_question(task, attachment, difficulty)
|
||||
@@ -1,15 +1,14 @@
|
||||
import logging
|
||||
from typing import Dict, List, Union
|
||||
from uuid import uuid4
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import BackgroundTasks, Response, HTTPException
|
||||
from fastapi.datastructures import FormData
|
||||
|
||||
from app.controllers.abc import IGradeController
|
||||
from app.dtos.evaluation import EvaluationType
|
||||
from app.dtos.speaking import GradeSpeakingItem
|
||||
from app.dtos.writing import WritingGradeTaskDTO
|
||||
from app.services.abc import IGradeService, IEvaluationService
|
||||
from ielts_be.controllers import IGradeController
|
||||
from ielts_be.services import IGradeService, IEvaluationService
|
||||
from ielts_be.dtos.evaluation import EvaluationType
|
||||
from ielts_be.dtos.speaking import GradeSpeakingItem
|
||||
from ielts_be.dtos.writing import WritingGradeTaskDTO
|
||||
|
||||
class GradeController(IGradeController):
|
||||
|
||||
@@ -26,6 +25,9 @@ class GradeController(IGradeController):
|
||||
self,
|
||||
task: int, dto: WritingGradeTaskDTO, background_tasks: BackgroundTasks
|
||||
):
|
||||
if task == 1 and dto.type == "academic" and dto.attachment is None:
|
||||
raise HTTPException(status_code=400, detail="Academic writing task requires a picture!")
|
||||
|
||||
await self._evaluation_service.create_evaluation(
|
||||
dto.userId, dto.sessionId, dto.exerciseId, EvaluationType.WRITING, task
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import Dict
|
||||
|
||||
from app.controllers.abc import ITrainingController
|
||||
from app.dtos.training import FetchTipsDTO
|
||||
from app.services.abc import ITrainingService
|
||||
from ielts_be.controllers import ITrainingController
|
||||
from ielts_be.services import ITrainingService
|
||||
from ielts_be.dtos.training import FetchTipsDTO
|
||||
|
||||
|
||||
class TrainingController(ITrainingController):
|
||||
@@ -1,6 +1,6 @@
|
||||
from app.controllers.abc import IUserController
|
||||
from app.dtos.user_batch import BatchUsersDTO
|
||||
from app.services.abc import IUserService
|
||||
from ielts_be.controllers import IUserController
|
||||
from ielts_be.services import IUserService
|
||||
from ielts_be.dtos.user_batch import BatchUsersDTO
|
||||
|
||||
|
||||
class UserController(IUserController):
|
||||
@@ -2,7 +2,7 @@ from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.configs.constants import LevelExerciseType
|
||||
from ielts_be.configs.constants import LevelExerciseType
|
||||
|
||||
|
||||
class LevelExercises(BaseModel):
|
||||
@@ -4,7 +4,7 @@ from typing import List, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.configs.constants import MinTimers, EducationalContent, ListeningExerciseType
|
||||
from ielts_be.configs.constants import MinTimers, EducationalContent, ListeningExerciseType
|
||||
|
||||
|
||||
class SaveListeningDTO(BaseModel):
|
||||
@@ -3,7 +3,7 @@ from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.configs.constants import ReadingExerciseType, EducationalContent
|
||||
from ielts_be.configs.constants import ReadingExerciseType, EducationalContent
|
||||
|
||||
class ReadingExercise(BaseModel):
|
||||
type: ReadingExerciseType
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import List, Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -7,3 +9,4 @@ class WritingGradeTaskDTO(BaseModel):
|
||||
exerciseId: str
|
||||
question: str
|
||||
answer: str
|
||||
attachment: Optional[str]
|
||||
@@ -117,3 +117,9 @@ class FileHelper:
|
||||
await file.write(file_bytes)
|
||||
|
||||
return ext, path_id
|
||||
|
||||
@staticmethod
|
||||
async def encode_image(image_path: str) -> str:
|
||||
async with aiofiles.open(image_path, "rb") as image_file:
|
||||
img = await image_file.read()
|
||||
return base64.b64encode(img).decode('utf-8')
|
||||
@@ -2,12 +2,12 @@ from typing import Dict, Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.dtos.exams.level import (
|
||||
from ielts_be.dtos.exams.level import (
|
||||
MultipleChoiceExercise,
|
||||
FillBlanksExercise,
|
||||
Part, Exam, Text
|
||||
)
|
||||
from app.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
|
||||
from ielts_be.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
|
||||
|
||||
|
||||
class LevelMapper:
|
||||
@@ -2,7 +2,7 @@ from typing import Dict, Any, List, Union, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.dtos.exams.listening import (
|
||||
from ielts_be.dtos.exams.listening import (
|
||||
TrueFalseExercise,
|
||||
MultipleChoiceExercise,
|
||||
WriteBlanksExercise,
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.dtos.exams.reading import (
|
||||
from ielts_be.dtos.exams.reading import (
|
||||
Part, Exam, Context, FillBlanksExercise,
|
||||
TrueFalseExercise, MatchSentencesExercise,
|
||||
WriteBlanksExercise, MultipleChoice
|
||||
@@ -5,7 +5,7 @@ from fastapi import Request
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from app.exceptions import CustomException, UnauthorizedException
|
||||
from ielts_be.exceptions import CustomException, UnauthorizedException
|
||||
|
||||
|
||||
class BaseAuthorization(ABC):
|
||||
3
ielts_be/repositories/__init__.py
Normal file
3
ielts_be/repositories/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .abc import *
|
||||
|
||||
__all__ = abc.__all__
|
||||
6
ielts_be/repositories/impl/__init__.py
Normal file
6
ielts_be/repositories/impl/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .document_stores import *
|
||||
from .file_storage import *
|
||||
|
||||
__all__ = []
|
||||
__all__.extend(document_stores.__all__)
|
||||
__all__.extend(file_storage.__all__)
|
||||
@@ -1,7 +1,7 @@
|
||||
from .firestore import Firestore
|
||||
#from .mongo import MongoDB
|
||||
from .mongo import MongoDB
|
||||
|
||||
__all__ = [
|
||||
"Firestore",
|
||||
#"MongoDB"
|
||||
"MongoDB"
|
||||
]
|
||||
@@ -4,7 +4,7 @@ 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
|
||||
from ielts_be.repositories import IDocumentStore
|
||||
|
||||
|
||||
class Firestore(IDocumentStore):
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, List, Dict
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||
|
||||
from app.repositories.abc import IDocumentStore
|
||||
from ielts_be.repositories import IDocumentStore
|
||||
|
||||
|
||||
class MongoDB(IDocumentStore):
|
||||
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
import aiofiles
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.repositories.abc import IFileStorage
|
||||
from ielts_be.repositories import IFileStorage
|
||||
|
||||
|
||||
class FirebaseStorage(IFileStorage):
|
||||
3
ielts_be/services/__init__.py
Normal file
3
ielts_be/services/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .abc import *
|
||||
|
||||
__all__ = abc.__all__
|
||||
@@ -1,9 +1,8 @@
|
||||
from abc import abstractmethod, ABC
|
||||
from typing import Union, List, Dict
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
from app.dtos.evaluation import EvaluationType
|
||||
from ielts_be.dtos.evaluation import EvaluationType
|
||||
|
||||
class IEvaluationService(ABC):
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import EducationalContent
|
||||
|
||||
|
||||
class ILevelService(ABC):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user