Phase 1 Step 4, file 2 of 18. Same cookbook as audit_routes (4a91814+883ef70) applied to banner_routes.py. compliance/api/banner_routes.py (653 LOC) is decomposed into: compliance/api/banner_routes.py (255) — thin handlers compliance/services/banner_consent_service.py (298) — public SDK surface compliance/services/banner_admin_service.py (238) — site/category/vendor CRUD compliance/services/_banner_serializers.py ( 81) — ORM-to-dict helpers shared between the two services compliance/schemas/banner.py ( 85) — Pydantic request models Split rationale: the SDK-facing endpoints (consent CRUD, config retrieval, export, stats) and the admin CRUD endpoints (sites + categories + vendors) have distinct audiences and different auth stories, and combined they would push the service file over the 500 hard cap. Two focused services is cleaner than one ~540-line god class. The shared ORM-to-dict helpers live in a private sibling module (_banner_serializers) rather than a static method on either service, so both services can import without a cycle. Handlers follow the established pattern: - Depends(get_consent_service) or Depends(get_admin_service) - `with translate_domain_errors():` wrapping the service call - Explicit return type annotations - ~3-5 lines per handler Services raise NotFoundError / ConflictError / ValidationError from compliance.domain; no HTTPException in the service layer. mypy.ini flips compliance.api.banner_routes from ignore_errors=True to False, joining audit_routes in the strict scope. The services carry the same scoped `# mypy: disable-error-code="arg-type,assignment"` header used by the audit services for the ORM Column[T] issue. Pydantic schemas moved to compliance.schemas.banner (mirroring the Step 3 schemas split). They were previously defined inline in banner_routes.py and not referenced by anything outside it, so no backwards-compat shim is needed. Verified: - 224/224 pytest (173 baseline + 26 audit integration + 25 banner integration) pass - tests/contracts/test_openapi_baseline.py green (360/484 unchanged) - mypy compliance/ -> Success: no issues found in 123 source files - All new files under the 300 soft target (largest: 298) - banner_routes.py drops from 653 -> 255 LOC (below hard cap) Hard-cap violations remaining: 16 (was 17). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
"""
|
|
Internal ORM-to-dict serializers shared by the banner services.
|
|
|
|
Kept as plain module-level functions (not part of either service class) so
|
|
they can be imported by both ``banner_consent_service`` and
|
|
``banner_admin_service`` without cyclic dependencies.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from compliance.db.banner_models import (
|
|
BannerCategoryConfigDB,
|
|
BannerConsentDB,
|
|
BannerSiteConfigDB,
|
|
BannerVendorConfigDB,
|
|
)
|
|
|
|
|
|
def consent_to_dict(c: BannerConsentDB) -> dict[str, Any]:
|
|
return {
|
|
"id": str(c.id),
|
|
"site_id": c.site_id,
|
|
"device_fingerprint": c.device_fingerprint,
|
|
"categories": c.categories or [],
|
|
"vendors": c.vendors or [],
|
|
"ip_hash": c.ip_hash,
|
|
"consent_string": c.consent_string,
|
|
"expires_at": c.expires_at.isoformat() if c.expires_at else None,
|
|
"created_at": c.created_at.isoformat() if c.created_at else None,
|
|
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
|
}
|
|
|
|
|
|
def site_config_to_dict(s: BannerSiteConfigDB) -> dict[str, Any]:
|
|
return {
|
|
"id": str(s.id),
|
|
"site_id": s.site_id,
|
|
"site_name": s.site_name,
|
|
"site_url": s.site_url,
|
|
"banner_title": s.banner_title,
|
|
"banner_description": s.banner_description,
|
|
"privacy_url": s.privacy_url,
|
|
"imprint_url": s.imprint_url,
|
|
"dsb_name": s.dsb_name,
|
|
"dsb_email": s.dsb_email,
|
|
"theme": s.theme or {},
|
|
"tcf_enabled": s.tcf_enabled,
|
|
"is_active": s.is_active,
|
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
|
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
|
}
|
|
|
|
|
|
def category_to_dict(c: BannerCategoryConfigDB) -> dict[str, Any]:
|
|
return {
|
|
"id": str(c.id),
|
|
"site_config_id": str(c.site_config_id),
|
|
"category_key": c.category_key,
|
|
"name_de": c.name_de,
|
|
"name_en": c.name_en,
|
|
"description_de": c.description_de,
|
|
"description_en": c.description_en,
|
|
"is_required": c.is_required,
|
|
"sort_order": c.sort_order,
|
|
"is_active": c.is_active,
|
|
}
|
|
|
|
|
|
def vendor_to_dict(v: BannerVendorConfigDB) -> dict[str, Any]:
|
|
return {
|
|
"id": str(v.id),
|
|
"site_config_id": str(v.site_config_id),
|
|
"vendor_name": v.vendor_name,
|
|
"vendor_url": v.vendor_url,
|
|
"category_key": v.category_key,
|
|
"description_de": v.description_de,
|
|
"description_en": v.description_en,
|
|
"cookie_names": v.cookie_names or [],
|
|
"retention_days": v.retention_days,
|
|
"is_active": v.is_active,
|
|
}
|