Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/api/nonsense.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ async def create_nonsense(


@router.get("/", response_model=NonsenseResponse)
async def find_nonsense(
async def get_nonsense(
name: str,
db_session: AsyncSession = Depends(get_db),
):
return await Nonsense.find(db_session, name)
nonsense = await Nonsense.get_by_name(db_session, name)
return nonsense


@router.delete("/")
Expand Down
15 changes: 5 additions & 10 deletions app/api/stuff.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,8 @@ async def create_stuff(


@router.get("/{name}", response_model=StuffResponse)
async def find_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
result = await Stuff.find(db_session, name)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Stuff with name {name} not found.",
)
async def get_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
result = await Stuff.get_by_name(db_session, name)
return result


Expand Down Expand Up @@ -89,7 +84,7 @@ async def find_stuff_pool(
HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs.
"""
try:
stmt = await Stuff.find(db_session, name, compile_sql=True)
stmt = await Stuff.get_by_name(db_session, name, compile_sql=True)
result = await request.app.postgres_pool.fetchrow(str(stmt))
except SQLAlchemyError as ex:
raise HTTPException(
Expand All @@ -105,7 +100,7 @@ async def find_stuff_pool(

@router.delete("/{name}")
async def delete_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
stuff = await Stuff.find(db_session, name)
stuff = await Stuff.get_by_name(db_session, name)
return await Stuff.delete(stuff, db_session)


Expand All @@ -115,6 +110,6 @@ async def update_stuff(
name: str,
db_session: AsyncSession = Depends(get_db),
):
stuff = await Stuff.find(db_session, name)
stuff = await Stuff.get_by_name(db_session, name)
await stuff.update(**payload.model_dump())
return stuff
16 changes: 8 additions & 8 deletions app/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections.abc import AsyncGenerator

from fastapi.exceptions import ResponseValidationError
from rotoger import AppStructLogger
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
Expand All @@ -26,15 +27,14 @@
# Dependency
async def get_db() -> AsyncGenerator:
async with AsyncSessionFactory() as session:
# logger.debug(f"ASYNC Pool: {engine.pool.status()}")
try:
yield session
await session.commit()
except SQLAlchemyError:
# Re-raise SQLAlchemy errors to be handled by the global handler
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
# Only log actual database-related issues, not response validation
if not isinstance(ex, ResponseValidationError):
await logger.aerror(f"Database-related error: {repr(ex)}")
raise # Re-raise to be handled by appropriate handlers
49 changes: 48 additions & 1 deletion app/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import orjson
from fastapi import FastAPI, Request
from fastapi.exceptions import ResponseValidationError
from fastapi.responses import JSONResponse
from rotoger import AppStructLogger
from sqlalchemy.exc import SQLAlchemyError

logger = AppStructLogger().get_logger()

#TODO: add reasoning for this in readme plus higligh using re-raise in db session

# TODO: add reasoning for this in readme plus higligh using re-raise in db session
async def sqlalchemy_exception_handler(
request: Request, exc: SQLAlchemyError
) -> JSONResponse:
Expand All @@ -30,6 +32,51 @@ async def sqlalchemy_exception_handler(
)


async def response_validation_exception_handler(
request: Request, exc: ResponseValidationError
) -> 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

errors = exc.errors()

# Check if this is a None/null response case
is_none_response = False
for error in errors:
# Check for null input pattern
if error.get("input") is None and "valid dictionary" in error.get("msg", ""):
is_none_response = True
break

await logger.aerror(
"Response validation error occurred",
validation_errors=errors,
request_url=request_path,
request_body=request_body,
is_none_response=is_none_response,
)

if is_none_response:
# Return 404 when response is None (resource not found)
return JSONResponse(
status_code=404,
content={"no_response": "The requested resource was not found"},
)
else:
# Return 422 when response exists but doesn't match expected format
return JSONResponse(
status_code=422,
content={"response_format_error": errors},
)


def register_exception_handlers(app: FastAPI) -> None:
"""Register all exception handlers with the FastAPI app."""
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
app.add_exception_handler(
ResponseValidationError, response_validation_exception_handler
)
19 changes: 2 additions & 17 deletions app/models/nonsense.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import uuid

from fastapi import HTTPException, status
from sqlalchemy import String, select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -20,22 +19,8 @@ class Nonsense(Base):
# TODO: apply relation to other tables

@classmethod
async def find(cls, db_session: AsyncSession, name: str):
"""

:param db_session:
:param name:
:return:
"""
async def get_by_name(cls, db_session: AsyncSession, name: str):
stmt = select(cls).where(cls.name == name)
result = await db_session.execute(stmt)
instance = result.scalars().first()
if instance is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"Record not found": f"There is no record for requested name value : {name}"
},
)
else:
return instance
return instance
2 changes: 1 addition & 1 deletion app/models/stuff.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Stuff(Base):

@classmethod
@compile_sql_or_scalar
async def find(cls, db_session: AsyncSession, name: str, compile_sql=False):
async def get_by_name(cls, db_session: AsyncSession, name: str, compile_sql=False):
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
return stmt

Expand Down