Complete architectural refactoring from simple MVC to Clean Architecture/DDD pattern: Domain Layer: - Add entities (Post, BaseEntity) with business logic - Add value objects (Title, Content, Slug) with validation - Add repository interfaces (PostRepository) - Add domain exceptions Application Layer: - Add use cases (CreatePost, GetPost, UpdatePost, DeletePost, ListPosts, PublishPost) - Add DTOs for data transfer - Add TransactionManager interface Infrastructure Layer: - Add SQLAlchemy models and async database connection - Add SQLAlchemyPostRepository implementation - Add Dishka DI container with providers - Add error handlers and middleware Presentation Layer: - Add FastAPI routes with Dishka integration - Add Pydantic schemas - Add dependency injection using FromDishka[T] Other Changes: - Remove old flat structure (api/, common/, core/, modules/) - Add hatchling build system for package scripts - Add blog CLI command - Update AGENTS.md with new architecture docs - All 48 tests passing, mypy clean, ruff clean
94 lines
2.7 KiB
Python
94 lines
2.7 KiB
Python
"""Exception handling middleware."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
|
|
from app.domain.exceptions import (
|
|
AlreadyExistsException,
|
|
DomainException,
|
|
ForbiddenException,
|
|
NotFoundException,
|
|
UnauthorizedException,
|
|
ValidationException,
|
|
)
|
|
|
|
|
|
def get_status_code(exc: DomainException) -> int:
|
|
"""Map domain exceptions to HTTP status codes."""
|
|
match exc:
|
|
case ValidationException():
|
|
return 400
|
|
case UnauthorizedException():
|
|
return 401
|
|
case ForbiddenException():
|
|
return 403
|
|
case NotFoundException():
|
|
return 404
|
|
case AlreadyExistsException():
|
|
return 409
|
|
case _:
|
|
return 500
|
|
|
|
|
|
async def domain_exception_handler(
|
|
request: Request, exc: DomainException
|
|
) -> JSONResponse:
|
|
"""Handle domain exceptions."""
|
|
status_code = get_status_code(exc)
|
|
return JSONResponse(
|
|
status_code=status_code,
|
|
content={
|
|
"error": exc.__class__.__name__,
|
|
"message": exc.message,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"path": str(request.url.path),
|
|
},
|
|
)
|
|
|
|
|
|
async def http_exception_handler(
|
|
request: Request, exc: StarletteHTTPException
|
|
) -> JSONResponse:
|
|
"""Handle HTTP exceptions."""
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"error": "HTTPException",
|
|
"message": str(exc.detail),
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"path": str(request.url.path),
|
|
},
|
|
)
|
|
|
|
|
|
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
"""Handle generic exceptions."""
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "InternalServerError",
|
|
"message": "An unexpected error occurred",
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"path": str(request.url.path),
|
|
},
|
|
)
|
|
|
|
|
|
def register_exception_handlers(app: FastAPI) -> None:
|
|
"""Register all exception handlers with FastAPI app."""
|
|
if not isinstance(app, FastAPI):
|
|
raise TypeError("app must be a FastAPI instance")
|
|
|
|
# Domain exceptions
|
|
app.add_exception_handler(DomainException, domain_exception_handler) # type: ignore[arg-type]
|
|
|
|
# HTTP exceptions
|
|
app.add_exception_handler(StarletteHTTPException, http_exception_handler) # type: ignore[arg-type]
|
|
|
|
# Generic exceptions (only in production)
|
|
# In development, let FastAPI show detailed traceback
|
|
# app.add_exception_handler(Exception, generic_exception_handler)
|