diff --git a/app/api/stuff.py b/app/api/stuff.py index 7ff8ee6..83fedf3 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -116,5 +116,5 @@ async def update_stuff( db_session: AsyncSession = Depends(get_db), ): stuff = await Stuff.find(db_session, name) - await stuff.update(db_session, **payload.model_dump()) + await stuff.update(**payload.model_dump()) return stuff diff --git a/app/database.py b/app/database.py index 872085c..8e00277 100644 --- a/app/database.py +++ b/app/database.py @@ -1,6 +1,7 @@ from collections.abc import AsyncGenerator from rotoger import AppStructLogger +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app.config import settings as global_settings @@ -28,6 +29,12 @@ async def get_db() -> AsyncGenerator: # logger.debug(f"ASYNC Pool: {engine.pool.status()}") try: yield session - except Exception as e: - await logger.aerror(f"Error getting database session: {e}") - raise + await session.commit() + except Exception as ex: + if isinstance(ex, SQLAlchemyError): + # Re-raise SQLAlchemyError directly without handling + raise + else: + # Handle other exceptions + await logger.aerror(f"NonSQLAlchemyError: {repr(ex)}") + raise # Re-raise after logging diff --git a/app/exception_handlers.py b/app/exception_handlers.py new file mode 100644 index 0000000..55f765b --- /dev/null +++ b/app/exception_handlers.py @@ -0,0 +1,35 @@ +import orjson +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from rotoger import AppStructLogger +from sqlalchemy.exc import SQLAlchemyError + +logger = AppStructLogger().get_logger() + + +async def sqlalchemy_exception_handler( + request: Request, exc: SQLAlchemyError +) -> JSONResponse: + request_path = request.url.path + try: + raw_body = await request.body() + request_body = orjson.loads(raw_body) if raw_body else None + except orjson.JSONDecodeError: + request_body = None + + await logger.aerror( + "Database error occurred", + sql_error=repr(exc), + request_url=request_path, + request_body=request_body, + ) + + return JSONResponse( + status_code=500, + content={"message": "A database error occurred. Please try again later."}, + ) + + +def register_exception_handlers(app: FastAPI) -> None: + """Register all exception handlers with the FastAPI app.""" + app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler) diff --git a/app/exceptions.py b/app/exceptions.py deleted file mode 100644 index b6d7a2e..0000000 --- a/app/exceptions.py +++ /dev/null @@ -1,59 +0,0 @@ -from fastapi import HTTPException, status - - -class BadRequestHTTPException(HTTPException): - def __init__(self, msg: str): - super().__init__( - status_code=status.HTTP_400_BAD_REQUEST, - detail=msg or "Bad request", - ) - - -class AuthFailedHTTPException(HTTPException): - def __init__(self): - super().__init__( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) - - -class AuthTokenExpiredHTTPException(HTTPException): - def __init__(self): - super().__init__( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Expired token", - headers={"WWW-Authenticate": "Bearer"}, - ) - - -class ForbiddenHTTPException(HTTPException): - def __init__(self, msg: str): - super().__init__( - status_code=status.HTTP_403_FORBIDDEN, - detail=msg or "Requested resource is forbidden", - ) - - -class NotFoundHTTPException(HTTPException): - def __init__(self, msg: str): - super().__init__( - status_code=status.HTTP_404_NOT_FOUND, - detail=msg or "Requested resource is not found", - ) - - -class ConflictHTTPException(HTTPException): - def __init__(self, msg: str): - super().__init__( - status_code=status.HTTP_409_CONFLICT, - detail=msg or "Conflicting resource request", - ) - - -class ServiceNotAvailableHTTPException(HTTPException): - def __init__(self, msg: str): - super().__init__( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail=msg or "Service not available", - ) diff --git a/app/main.py b/app/main.py index dab1176..31c9c18 100644 --- a/app/main.py +++ b/app/main.py @@ -14,6 +14,7 @@ from app.api.shakespeare import router as shakespeare_router from app.api.stuff import router as stuff_router from app.api.user import router as user_router from app.config import settings as global_settings +from app.exception_handlers import register_exception_handlers from app.redis import get_redis from app.services.auth import AuthBearer @@ -61,6 +62,9 @@ def create_app() -> FastAPI: dependencies=[Depends(AuthBearer())], ) + # Register exception handlers + register_exception_handlers(app) + @app.get("/index", response_class=HTMLResponse) def get_index(request: Request): return templates.TemplateResponse("index.html", {"request": request}) diff --git a/app/models/base.py b/app/models/base.py index b8665a1..faaf1a2 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -20,64 +20,40 @@ class Base(DeclarativeBase): return self.__name__.lower() async def save(self, db_session: AsyncSession): - """ - - :param db_session: - :return: - """ - try: - db_session.add(self) - await db_session.commit() - await db_session.refresh(self) - return self - except SQLAlchemyError as ex: - await logger.aerror(f"Error inserting instance of {self}: {repr(ex)}") - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) - ) from ex + db_session.add(self) + await db_session.flush() + await db_session.refresh(self) + return self async def delete(self, db_session: AsyncSession): - """ - - :param db_session: - :return: - """ try: await db_session.delete(self) - await db_session.commit() return True except SQLAlchemyError as ex: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) ) from ex - async def update(self, db: AsyncSession, **kwargs): - """ - - :param db: - :param kwargs - :return: - """ + async def update(self, **kwargs): try: for k, v in kwargs.items(): setattr(self, k, v) - return await db.commit() + return True except SQLAlchemyError as ex: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) ) from ex - async def save_or_update(self, db: AsyncSession): + async def save_or_update(self, db_session: AsyncSession): try: - db.add(self) - return await db.commit() + db_session.add(self) + await db_session.flush() + return True except IntegrityError as exception: if isinstance(exception.orig, UniqueViolationError): - return await db.merge(self) + return await db_session.merge(self) else: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(exception), ) from exception - finally: - await db.close() diff --git a/app/schemas/stuff.py b/app/schemas/stuff.py index 18b3700..2a9d3c9 100644 --- a/app/schemas/stuff.py +++ b/app/schemas/stuff.py @@ -7,7 +7,9 @@ config = ConfigDict(from_attributes=True) class RandomStuff(BaseModel): - chaos: dict[str, Any] = Field(..., description="JSON data for chaos field") + chaos: dict[str, Any] = Field( + ..., description="Pretty chaotic JSON data can be added here..." + ) class StuffSchema(BaseModel): diff --git a/tests/api/test_chaotic_stuff.py b/tests/api/test_chaotic_stuff.py new file mode 100644 index 0000000..e69de29