compliance/api/company_profile_routes.py (640 LOC) -> 154 LOC thin routes.
Unusual for this repo: persistence uses raw SQL via sqlalchemy.text()
because the underlying compliance_company_profiles table has ~45 columns
with complex jsonb coercion and there is no SQLAlchemy model for it.
New files:
compliance/schemas/company_profile.py (127) — 4 request/response models
compliance/services/company_profile_service.py (340) — Service class + row_to_response + log_audit
compliance/services/_company_profile_sql.py (139) — 70-line INSERT/UPDATE statements
separated for readability
Minor behavioral improvement: the handlers now use Depends(get_db) for
session management instead of the bespoke `db = SessionLocal(); try: ...
finally: db.close()` pattern. This makes the routes consistent with
every other refactored service, fixes the broken-ness under test
dependency_overrides, and removes 6 duplicate try/finally blocks.
Legacy exports preserved: CompanyProfileRequest, CompanyProfileResponse,
AuditEntryResponse, AuditListResponse, row_to_response, and log_audit are
re-exported from compliance.api.company_profile_routes so that the two
existing test files
(tests/test_company_profile_routes.py, tests/test_company_profile_extend.py)
keep importing from the same path.
Pre-existing broken tests noted: 6 tests in those files feed a 40-tuple
row into row_to_response, but _BASE_COLUMNS_LIST has 46 columns (has had
since the Phase 2 Stammdaten extension). These tests fail on main too
(verified via `git stash` round-trip). Not fixed in this commit — they
require a rewrite of the test's _make_row helper, which is out of scope
for a pure structural refactor. Flagged for follow-up.
Verified:
- 173/173 pytest compliance/tests/ tests/contracts/ pass
- OpenAPI 360/484 unchanged
- mypy compliance/ -> Success on 127 source files
- company_profile_routes.py 640 -> 154 LOC
- All new files under soft 300 target except service (340, under hard 500)
- Hard-cap violations: 15 -> 14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
128 lines
3.7 KiB
Python
128 lines
3.7 KiB
Python
"""
|
|
Company Profile schemas — Stammdaten for tenants + projects.
|
|
|
|
Phase 1 Step 4: extracted from ``compliance.api.company_profile_routes`` so
|
|
the route layer becomes thin delegation.
|
|
"""
|
|
|
|
from typing import Any, Optional
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class CompanyProfileRequest(BaseModel):
|
|
company_name: str = ""
|
|
legal_form: str = "GmbH"
|
|
industry: str = ""
|
|
founded_year: Optional[int] = None
|
|
business_model: str = "B2B"
|
|
offerings: list[str] = []
|
|
offering_urls: dict[str, Any] = {}
|
|
company_size: str = "small"
|
|
employee_count: str = "1-9"
|
|
annual_revenue: str = "< 2 Mio"
|
|
headquarters_country: str = "DE"
|
|
headquarters_country_other: str = ""
|
|
headquarters_street: str = ""
|
|
headquarters_zip: str = ""
|
|
headquarters_city: str = ""
|
|
headquarters_state: str = ""
|
|
has_international_locations: bool = False
|
|
international_countries: list[str] = []
|
|
target_markets: list[str] = ["DE"]
|
|
primary_jurisdiction: str = "DE"
|
|
is_data_controller: bool = True
|
|
is_data_processor: bool = False
|
|
uses_ai: bool = False
|
|
ai_use_cases: list[str] = []
|
|
dpo_name: Optional[str] = None
|
|
dpo_email: Optional[str] = None
|
|
legal_contact_name: Optional[str] = None
|
|
legal_contact_email: Optional[str] = None
|
|
machine_builder: Optional[dict[str, Any]] = None
|
|
is_complete: bool = False
|
|
# Phase 2 fields
|
|
repos: list[dict[str, Any]] = []
|
|
document_sources: list[dict[str, Any]] = []
|
|
processing_systems: list[dict[str, Any]] = []
|
|
ai_systems: list[dict[str, Any]] = []
|
|
technical_contacts: list[dict[str, Any]] = []
|
|
subject_to_nis2: bool = False
|
|
subject_to_ai_act: bool = False
|
|
subject_to_iso27001: bool = False
|
|
supervisory_authority: Optional[str] = None
|
|
review_cycle_months: int = 12
|
|
# Project ID (multi-project)
|
|
project_id: Optional[str] = None
|
|
|
|
|
|
class CompanyProfileResponse(BaseModel):
|
|
id: str
|
|
tenant_id: str
|
|
project_id: Optional[str] = None
|
|
company_name: str
|
|
legal_form: str
|
|
industry: str
|
|
founded_year: Optional[int]
|
|
business_model: str
|
|
offerings: list[str]
|
|
offering_urls: dict[str, Any] = {}
|
|
company_size: str
|
|
employee_count: str
|
|
annual_revenue: str
|
|
headquarters_country: str
|
|
headquarters_country_other: str = ""
|
|
headquarters_street: str = ""
|
|
headquarters_zip: str = ""
|
|
headquarters_city: str = ""
|
|
headquarters_state: str = ""
|
|
has_international_locations: bool
|
|
international_countries: list[str]
|
|
target_markets: list[str]
|
|
primary_jurisdiction: str
|
|
is_data_controller: bool
|
|
is_data_processor: bool
|
|
uses_ai: bool
|
|
ai_use_cases: list[str]
|
|
dpo_name: Optional[str]
|
|
dpo_email: Optional[str]
|
|
legal_contact_name: Optional[str]
|
|
legal_contact_email: Optional[str]
|
|
machine_builder: Optional[dict[str, Any]]
|
|
is_complete: bool
|
|
completed_at: Optional[str]
|
|
created_at: str
|
|
updated_at: str
|
|
# Phase 2 fields
|
|
repos: list[dict[str, Any]] = []
|
|
document_sources: list[dict[str, Any]] = []
|
|
processing_systems: list[dict[str, Any]] = []
|
|
ai_systems: list[dict[str, Any]] = []
|
|
technical_contacts: list[dict[str, Any]] = []
|
|
subject_to_nis2: bool = False
|
|
subject_to_ai_act: bool = False
|
|
subject_to_iso27001: bool = False
|
|
supervisory_authority: Optional[str] = None
|
|
review_cycle_months: int = 12
|
|
|
|
|
|
class AuditEntryResponse(BaseModel):
|
|
id: str
|
|
action: str
|
|
changed_fields: Optional[dict[str, Any]]
|
|
changed_by: Optional[str]
|
|
created_at: str
|
|
|
|
|
|
class AuditListResponse(BaseModel):
|
|
entries: list[AuditEntryResponse]
|
|
total: int
|
|
|
|
|
|
__all__ = [
|
|
"CompanyProfileRequest",
|
|
"CompanyProfileResponse",
|
|
"AuditEntryResponse",
|
|
"AuditListResponse",
|
|
]
|