refactor(backend/api): extract AuditSession service layer (Step 4 worked example)
Phase 1 Step 4 of PHASE1_RUNBOOK.md, first worked example. Demonstrates
the router -> service delegation pattern for all 18 oversized route
files still above the 500 LOC hard cap.
compliance/api/audit_routes.py (637 LOC) is decomposed into:
compliance/api/audit_routes.py (198) — thin handlers
compliance/services/audit_session_service.py (259) — session lifecycle
compliance/services/audit_signoff_service.py (319) — checklist + sign-off
compliance/api/_http_errors.py ( 43) — reusable error translator
Handlers shrink to 3-6 lines each:
@router.post("/sessions", response_model=AuditSessionResponse)
async def create_audit_session(
request: CreateAuditSessionRequest,
service: AuditSessionService = Depends(get_audit_session_service),
):
with translate_domain_errors():
return service.create(request)
Services are HTTP-agnostic: they raise NotFoundError / ConflictError /
ValidationError from compliance.domain, and the route layer translates
those to HTTPException(404/409/400) via the translate_domain_errors()
context manager in compliance.api._http_errors. The error translator is
reusable by every future Step 4 refactor.
Services take a sqlalchemy Session in the constructor and are wired via
Depends factories (get_audit_session_service / get_audit_signoff_service).
No globals, no module-level state.
Behavior is byte-identical at the HTTP boundary:
- Same paths, methods, status codes, response models
- Same error messages (domain error __str__ preserved)
- Same auto-start-on-first-signoff, same statistics calculation,
same signature hash format, same PDF streaming response
Verified:
- 173/173 pytest compliance/tests/ tests/contracts/ pass
- OpenAPI 360 paths / 484 operations unchanged
- audit_routes.py under soft 300 target
- Both new service files under soft 300 / hard 500
Note: compliance/tests/test_audit_routes.py contains placeholder tests
that do not actually import or call the handler functions — they only
assert on request-data shape. Real behavioral coverage relies on the
contract test. A follow-up commit should add TestClient-based
integration tests for the audit endpoints. Flagged in PHASE1_RUNBOOK.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
backend-compliance/compliance/api/_http_errors.py
Normal file
43
backend-compliance/compliance/api/_http_errors.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Domain error -> HTTPException translation helper.
|
||||
|
||||
Used by route handlers to keep services HTTP-agnostic while still giving
|
||||
FastAPI the status codes it needs. Routes wrap their service calls with
|
||||
the ``translate_domain_errors()`` context manager:
|
||||
|
||||
with translate_domain_errors():
|
||||
return service.create(request)
|
||||
|
||||
The helper catches ``compliance.domain.DomainError`` subclasses and
|
||||
re-raises them as ``fastapi.HTTPException`` with the appropriate status.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from compliance.domain import (
|
||||
ConflictError,
|
||||
DomainError,
|
||||
NotFoundError,
|
||||
PermissionError as DomainPermissionError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def translate_domain_errors() -> Iterator[None]:
|
||||
"""Translate domain exceptions raised inside the block into HTTPException."""
|
||||
try:
|
||||
yield
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ConflictError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
except ValidationError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except DomainPermissionError as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except DomainError as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
Reference in New Issue
Block a user