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>
86 lines
2.4 KiB
Python
86 lines
2.4 KiB
Python
"""
|
|
Banner consent schemas — cookie consent SDK + admin configuration.
|
|
|
|
Phase 1 Step 4: extracted from ``compliance.api.banner_routes`` so the
|
|
route layer becomes thin delegation to ``compliance.services.banner_*``.
|
|
"""
|
|
|
|
from typing import Any, List, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
|
|
class ConsentCreate(BaseModel):
|
|
"""Request body for recording a device consent."""
|
|
site_id: str
|
|
device_fingerprint: str
|
|
categories: List[str] = []
|
|
vendors: List[str] = []
|
|
ip_address: Optional[str] = None
|
|
user_agent: Optional[str] = None
|
|
consent_string: Optional[str] = None
|
|
|
|
|
|
class SiteConfigCreate(BaseModel):
|
|
"""Request body for creating a banner site configuration."""
|
|
site_id: str
|
|
site_name: Optional[str] = None
|
|
site_url: Optional[str] = None
|
|
banner_title: Optional[str] = None
|
|
banner_description: Optional[str] = None
|
|
privacy_url: Optional[str] = None
|
|
imprint_url: Optional[str] = None
|
|
dsb_name: Optional[str] = None
|
|
dsb_email: Optional[str] = None
|
|
theme: Optional[dict[str, Any]] = None
|
|
tcf_enabled: bool = False
|
|
|
|
|
|
class SiteConfigUpdate(BaseModel):
|
|
"""Partial update for a banner site configuration."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
site_name: Optional[str] = None
|
|
site_url: Optional[str] = None
|
|
banner_title: Optional[str] = None
|
|
banner_description: Optional[str] = None
|
|
privacy_url: Optional[str] = None
|
|
imprint_url: Optional[str] = None
|
|
dsb_name: Optional[str] = None
|
|
dsb_email: Optional[str] = None
|
|
theme: Optional[dict[str, Any]] = None
|
|
tcf_enabled: Optional[bool] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class CategoryConfigCreate(BaseModel):
|
|
"""Request body for adding a cookie category to a site."""
|
|
category_key: str
|
|
name_de: str
|
|
name_en: Optional[str] = None
|
|
description_de: Optional[str] = None
|
|
description_en: Optional[str] = None
|
|
is_required: bool = False
|
|
sort_order: int = 0
|
|
|
|
|
|
class VendorConfigCreate(BaseModel):
|
|
"""Request body for adding a vendor under a site's category."""
|
|
vendor_name: str
|
|
vendor_url: Optional[str] = None
|
|
category_key: str
|
|
description_de: Optional[str] = None
|
|
description_en: Optional[str] = None
|
|
cookie_names: List[str] = []
|
|
retention_days: int = 365
|
|
|
|
|
|
__all__ = [
|
|
"ConsentCreate",
|
|
"SiteConfigCreate",
|
|
"SiteConfigUpdate",
|
|
"CategoryConfigCreate",
|
|
"VendorConfigCreate",
|
|
]
|