From d722504e553d12dc13f94f3f6be20f15fdf6cd79 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sat, 23 Aug 2025 17:53:18 +0200 Subject: [PATCH 1/3] wip: crud refactor --- app/api/stuff.py | 2 +- app/database.py | 1 + app/main.py | 16 +++++++++++++- app/models/base.py | 38 +++++++++------------------------ app/schemas/stuff.py | 4 +++- tests/api/test_chaotic_stuff.py | 0 6 files changed, 30 insertions(+), 31 deletions(-) create mode 100644 tests/api/test_chaotic_stuff.py 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..bb5ef08 100644 --- a/app/database.py +++ b/app/database.py @@ -28,6 +28,7 @@ async def get_db() -> AsyncGenerator: # logger.debug(f"ASYNC Pool: {engine.pool.status()}") try: yield session + await session.commit() except Exception as e: await logger.aerror(f"Error getting database session: {e}") raise diff --git a/app/main.py b/app/main.py index dab1176..8a63090 100644 --- a/app/main.py +++ b/app/main.py @@ -3,9 +3,10 @@ import asyncpg from fastapi import Depends, FastAPI, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates from rotoger import AppStructLogger +from sqlalchemy.exc import SQLAlchemyError from app.api.health import router as health_router from app.api.ml import router as ml_router @@ -61,6 +62,19 @@ def create_app() -> FastAPI: dependencies=[Depends(AuthBearer())], ) + @app.exception_handler(SQLAlchemyError) + async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError): + await logger.aerror( + "A database error occurred", + sql_error=repr(exc), + request_url=request.url.path, + request_body=request.body, + ) + return JSONResponse( + status_code=500, + content={"message": "A database error occurred. Please try again later."}, + ) + @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..f04cef9 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -20,64 +20,46 @@ def __tablename__(self) -> str: 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.flush() 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 + raise # This will make the exception handler catch it 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() + await db_session.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 @@ 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 From 69f3dc3fd8e674f8a0e6a45853e0b0f29d142324 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sat, 23 Aug 2025 18:22:35 +0200 Subject: [PATCH 2/3] wip: add exception handler --- app/database.py | 13 ++++++--- app/exception_handlers.py | 32 +++++++++++++++++++++ app/exceptions.py | 59 --------------------------------------- app/main.py | 19 ++++--------- app/models/base.py | 16 ++++------- 5 files changed, 52 insertions(+), 87 deletions(-) create mode 100644 app/exception_handlers.py delete mode 100644 app/exceptions.py diff --git a/app/database.py b/app/database.py index bb5ef08..e400ed6 100644 --- a/app/database.py +++ b/app/database.py @@ -2,7 +2,7 @@ from rotoger import AppStructLogger from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine - +from sqlalchemy.exc import SQLAlchemyError from app.config import settings as global_settings logger = AppStructLogger().get_logger() @@ -29,6 +29,11 @@ async def get_db() -> AsyncGenerator: try: yield session await session.commit() - except Exception as e: - await logger.aerror(f"Error getting database session: {e}") - raise + 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..c67ca13 --- /dev/null +++ b/app/exception_handlers.py @@ -0,0 +1,32 @@ +from fastapi import Request +from fastapi.responses import JSONResponse +from sqlalchemy.exc import SQLAlchemyError +import orjson +from fastapi import FastAPI +from rotoger import AppStructLogger + +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 8a63090..1bf1fee 100644 --- a/app/main.py +++ b/app/main.py @@ -3,10 +3,10 @@ import asyncpg from fastapi import Depends, FastAPI, Request -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from rotoger import AppStructLogger -from sqlalchemy.exc import SQLAlchemyError + from app.api.health import router as health_router from app.api.ml import router as ml_router @@ -17,6 +17,7 @@ from app.config import settings as global_settings from app.redis import get_redis from app.services.auth import AuthBearer +from app.exception_handlers import register_exception_handlers logger = AppStructLogger().get_logger() templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") @@ -62,18 +63,8 @@ def create_app() -> FastAPI: dependencies=[Depends(AuthBearer())], ) - @app.exception_handler(SQLAlchemyError) - async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError): - await logger.aerror( - "A database error occurred", - sql_error=repr(exc), - request_url=request.url.path, - request_body=request.body, - ) - return JSONResponse( - status_code=500, - content={"message": "A database error occurred. Please try again later."}, - ) + # Register exception handlers + register_exception_handlers(app) @app.get("/index", response_class=HTMLResponse) def get_index(request: Request): diff --git a/app/models/base.py b/app/models/base.py index f04cef9..1347c6e 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -20,14 +20,11 @@ def __tablename__(self) -> str: return self.__name__.lower() async def save(self, db_session: AsyncSession): - try: - db_session.add(self) - await db_session.flush() - await db_session.refresh(self) - return self - except SQLAlchemyError as ex: - await logger.aerror(f"Error inserting instance of {self}: {repr(ex)}") - raise # This will make the exception handler catch it + db_session.add(self) + await db_session.flush() + await db_session.refresh(self) + return self + async def delete(self, db_session: AsyncSession): try: @@ -61,5 +58,4 @@ async def save_or_update(self, db_session: AsyncSession): status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(exception), ) from exception - finally: - await db_session.close() + From 58553c6d63cb7b5128b65285299cf72e90575a7d Mon Sep 17 00:00:00 2001 From: grillazz Date: Sat, 23 Aug 2025 18:22:51 +0200 Subject: [PATCH 3/3] wip: add exception handler --- app/database.py | 3 ++- app/exception_handlers.py | 13 ++++++++----- app/main.py | 3 +-- app/models/base.py | 2 -- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/database.py b/app/database.py index e400ed6..8e00277 100644 --- a/app/database.py +++ b/app/database.py @@ -1,8 +1,9 @@ from collections.abc import AsyncGenerator from rotoger import AppStructLogger -from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + from app.config import settings as global_settings logger = AppStructLogger().get_logger() diff --git a/app/exception_handlers.py b/app/exception_handlers.py index c67ca13..55f765b 100644 --- a/app/exception_handlers.py +++ b/app/exception_handlers.py @@ -1,13 +1,15 @@ -from fastapi import Request -from fastapi.responses import JSONResponse -from sqlalchemy.exc import SQLAlchemyError import orjson -from fastapi import FastAPI +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: + +async def sqlalchemy_exception_handler( + request: Request, exc: SQLAlchemyError +) -> JSONResponse: request_path = request.url.path try: raw_body = await request.body() @@ -27,6 +29,7 @@ async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError) - 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/main.py b/app/main.py index 1bf1fee..31c9c18 100644 --- a/app/main.py +++ b/app/main.py @@ -7,7 +7,6 @@ from fastapi.templating import Jinja2Templates from rotoger import AppStructLogger - from app.api.health import router as health_router from app.api.ml import router as ml_router from app.api.nonsense import router as nonsense_router @@ -15,9 +14,9 @@ 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 -from app.exception_handlers import register_exception_handlers logger = AppStructLogger().get_logger() templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") diff --git a/app/models/base.py b/app/models/base.py index 1347c6e..faaf1a2 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -25,7 +25,6 @@ async def save(self, db_session: AsyncSession): await db_session.refresh(self) return self - async def delete(self, db_session: AsyncSession): try: await db_session.delete(self) @@ -58,4 +57,3 @@ async def save_or_update(self, db_session: AsyncSession): status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(exception), ) from exception -