Files
blog.pyaqa.ru/app/infrastructure/middleware/error_handler.py
Sergey Vanyushkin 87b094220d refactor: migrate to DDD architecture with Dishka DI
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
2026-05-01 20:20:41 +03:00

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)