Merge pull request #213 from grillazz/12-add-json-field-example

wip: crud refactor
This commit is contained in:
Ordinary Hobbit 2025-08-23 18:23:46 +02:00 committed by GitHub
commit b3c664ebfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 64 additions and 99 deletions

View File

@ -116,5 +116,5 @@ async def update_stuff(
db_session: AsyncSession = Depends(get_db), db_session: AsyncSession = Depends(get_db),
): ):
stuff = await Stuff.find(db_session, name) stuff = await Stuff.find(db_session, name)
await stuff.update(db_session, **payload.model_dump()) await stuff.update(**payload.model_dump())
return stuff return stuff

View File

@ -1,6 +1,7 @@
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from rotoger import AppStructLogger from rotoger import AppStructLogger
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.config import settings as global_settings 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()}") # logger.debug(f"ASYNC Pool: {engine.pool.status()}")
try: try:
yield session yield session
except Exception as e: await session.commit()
await logger.aerror(f"Error getting database session: {e}") except Exception as ex:
raise 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

35
app/exception_handlers.py Normal file
View File

@ -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)

View File

@ -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",
)

View File

@ -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.stuff import router as stuff_router
from app.api.user import router as user_router from app.api.user import router as user_router
from app.config import settings as global_settings from app.config import settings as global_settings
from app.exception_handlers import register_exception_handlers
from app.redis import get_redis from app.redis import get_redis
from app.services.auth import AuthBearer from app.services.auth import AuthBearer
@ -61,6 +62,9 @@ def create_app() -> FastAPI:
dependencies=[Depends(AuthBearer())], dependencies=[Depends(AuthBearer())],
) )
# Register exception handlers
register_exception_handlers(app)
@app.get("/index", response_class=HTMLResponse) @app.get("/index", response_class=HTMLResponse)
def get_index(request: Request): def get_index(request: Request):
return templates.TemplateResponse("index.html", {"request": request}) return templates.TemplateResponse("index.html", {"request": request})

View File

@ -20,64 +20,40 @@ class Base(DeclarativeBase):
return self.__name__.lower() return self.__name__.lower()
async def save(self, db_session: AsyncSession): async def save(self, db_session: AsyncSession):
""" db_session.add(self)
await db_session.flush()
:param db_session: await db_session.refresh(self)
:return: return self
"""
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
async def delete(self, db_session: AsyncSession): async def delete(self, db_session: AsyncSession):
"""
:param db_session:
:return:
"""
try: try:
await db_session.delete(self) await db_session.delete(self)
await db_session.commit()
return True return True
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex ) from ex
async def update(self, db: AsyncSession, **kwargs): async def update(self, **kwargs):
"""
:param db:
:param kwargs
:return:
"""
try: try:
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(self, k, v) setattr(self, k, v)
return await db.commit() return True
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex ) from ex
async def save_or_update(self, db: AsyncSession): async def save_or_update(self, db_session: AsyncSession):
try: try:
db.add(self) db_session.add(self)
return await db.commit() await db_session.flush()
return True
except IntegrityError as exception: except IntegrityError as exception:
if isinstance(exception.orig, UniqueViolationError): if isinstance(exception.orig, UniqueViolationError):
return await db.merge(self) return await db_session.merge(self)
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=repr(exception), detail=repr(exception),
) from exception ) from exception
finally:
await db.close()

View File

@ -7,7 +7,9 @@ config = ConfigDict(from_attributes=True)
class RandomStuff(BaseModel): 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): class StuffSchema(BaseModel):

View File