mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2025-08-26 16:40:40 +03:00
Merge pull request #213 from grillazz/12-add-json-field-example
wip: crud refactor
This commit is contained in:
commit
b3c664ebfe
@ -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
|
||||
|
@ -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
|
||||
|
35
app/exception_handlers.py
Normal file
35
app/exception_handlers.py
Normal 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)
|
@ -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",
|
||||
)
|
@ -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})
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
0
tests/api/test_chaotic_stuff.py
Normal file
0
tests/api/test_chaotic_stuff.py
Normal file
Loading…
x
Reference in New Issue
Block a user