lint and format

This commit is contained in:
grillazz 2025-08-24 15:40:06 +02:00
parent 9c7db17da8
commit 978041c6ee
7 changed files with 37 additions and 106 deletions

View File

@ -1,82 +0,0 @@
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
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."},
)
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
)

View File

@ -1,8 +1,7 @@
import orjson import orjson
from attrs import define, field
from fastapi import Request from fastapi import Request
from rotoger import AppStructLogger from rotoger import AppStructLogger
from attrs import define, field
logger = AppStructLogger().get_logger() logger = AppStructLogger().get_logger()
@ -10,6 +9,7 @@ logger = AppStructLogger().get_logger()
@define(slots=True) @define(slots=True)
class RequestInfo: class RequestInfo:
"""Contains extracted request information.""" """Contains extracted request information."""
path: str = field() path: str = field()
body: dict = field(default=None) body: dict = field(default=None)
@ -39,5 +39,5 @@ class BaseExceptionHandler:
message, message,
request_url=request_info.path, request_url=request_info.path,
request_body=request_info.body, request_body=request_info.body,
**kwargs **kwargs,
) )

View File

@ -1,6 +1,7 @@
from fastapi import Request from fastapi import Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.exception_handlers.base import BaseExceptionHandler from app.exception_handlers.base import BaseExceptionHandler
@ -8,16 +9,16 @@ class SQLAlchemyExceptionHandler(BaseExceptionHandler):
"""Handles SQLAlchemy database exceptions.""" """Handles SQLAlchemy database exceptions."""
@classmethod @classmethod
async def handle_exception(cls, request: Request, exc: SQLAlchemyError) -> JSONResponse: async def handle_exception(
cls, request: Request, exc: SQLAlchemyError
) -> JSONResponse:
request_info = await cls.extract_request_info(request) request_info = await cls.extract_request_info(request)
await cls.log_error( await cls.log_error(
"Database error occurred", "Database error occurred", request_info, sql_error=repr(exc)
request_info,
sql_error=repr(exc)
) )
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
content={"message": "A database error occurred. Please try again later."} content={"message": "A database error occurred. Please try again later."},
) )

View File

@ -1,11 +1,16 @@
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy.exc import SQLAlchemyError
from fastapi.exceptions import ResponseValidationError from fastapi.exceptions import ResponseValidationError
from sqlalchemy.exc import SQLAlchemyError
from app.exception_handlers.database import SQLAlchemyExceptionHandler from app.exception_handlers.database import SQLAlchemyExceptionHandler
from app.exception_handlers.validation import ResponseValidationExceptionHandler from app.exception_handlers.validation import ResponseValidationExceptionHandler
def register_exception_handlers(app: FastAPI) -> None: def register_exception_handlers(app: FastAPI) -> None:
"""Register all exception handlers with the FastAPI app.""" """Register all exception handlers with the FastAPI app."""
app.add_exception_handler(SQLAlchemyError, SQLAlchemyExceptionHandler.handle_exception) app.add_exception_handler(
app.add_exception_handler(ResponseValidationError, ResponseValidationExceptionHandler.handle_exception) SQLAlchemyError, SQLAlchemyExceptionHandler.handle_exception
)
app.add_exception_handler(
ResponseValidationError, ResponseValidationExceptionHandler.handle_exception
)

View File

@ -9,14 +9,18 @@ class ResponseValidationExceptionHandler(BaseExceptionHandler):
"""Handles response validation exceptions.""" """Handles response validation exceptions."""
@classmethod @classmethod
async def handle_exception(cls, request: Request, exc: ResponseValidationError) -> JSONResponse: async def handle_exception(
cls, request: Request, exc: ResponseValidationError
) -> JSONResponse:
request_info = await cls.extract_request_info(request) request_info = await cls.extract_request_info(request)
errors = exc.errors() errors = exc.errors()
# Check if this is a None/null response case # Check if this is a None/null response case
is_none_response = False is_none_response = False
for error in errors: for error in errors:
if error.get("input") is None and "valid dictionary" in error.get("msg", ""): if error.get("input") is None and "valid dictionary" in error.get(
"msg", ""
):
is_none_response = True is_none_response = True
break break
@ -24,16 +28,15 @@ class ResponseValidationExceptionHandler(BaseExceptionHandler):
"Response validation error occurred", "Response validation error occurred",
request_info, request_info,
validation_errors=errors, validation_errors=errors,
is_none_response=is_none_response is_none_response=is_none_response,
) )
if is_none_response: if is_none_response:
return JSONResponse( return JSONResponse(
status_code=404, status_code=404,
content={"no_response": "The requested resource was not found"} content={"no_response": "The requested resource was not found"},
) )
else: else:
return JSONResponse( return JSONResponse(
status_code=422, status_code=422, content={"response_format_error": errors}
content={"response_format_error": errors}
) )

View File

@ -27,13 +27,17 @@ async def test_add_stuff(client: AsyncClient):
) )
response = await client.post("/stuff", json=stuff) response = await client.post("/stuff", json=stuff)
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
assert response.json() == snapshot({'message':'A database error occurred. Please try again later.'}) assert response.json() == snapshot(
{"message": "A database error occurred. Please try again later."}
)
async def test_get_stuff(client: AsyncClient): async def test_get_stuff(client: AsyncClient):
response = await client.get(f"/stuff/nonexistent") response = await client.get("/stuff/nonexistent")
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json() == snapshot({'no_response':'The requested resource was not found'}) assert response.json() == snapshot(
{"no_response": "The requested resource was not found"}
)
stuff = StuffFactory.build(factory_use_constructors=True).model_dump(mode="json") stuff = StuffFactory.build(factory_use_constructors=True).model_dump(mode="json")
await client.post("/stuff", json=stuff) await client.post("/stuff", json=stuff)
name = stuff["name"] name = stuff["name"]