Split vendor_compliance_routes.py (1107 LOC) into thin route handlers plus three service modules: VendorService (vendors CRUD/stats/status), ContractService (contracts CRUD), and FindingService + ControlInstanceService + ControlsLibraryService (findings, control instances, controls library). All files under 500 lines. 215 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
404 lines
12 KiB
Python
404 lines
12 KiB
Python
"""
|
|
FastAPI routes for Vendor Compliance — Auftragsverarbeitung (Art. 28 DSGVO).
|
|
|
|
Endpoints:
|
|
Vendors (7):
|
|
GET /vendor-compliance/vendors — Liste + Filter
|
|
GET /vendor-compliance/vendors/stats — Statistiken
|
|
GET /vendor-compliance/vendors/{id} — Detail
|
|
POST /vendor-compliance/vendors — Erstellen
|
|
PUT /vendor-compliance/vendors/{id} — Update
|
|
DELETE /vendor-compliance/vendors/{id} — Loeschen
|
|
PATCH /vendor-compliance/vendors/{id}/status — Status aendern
|
|
|
|
Contracts (5):
|
|
GET /vendor-compliance/contracts — Liste
|
|
GET /vendor-compliance/contracts/{id} — Detail
|
|
POST /vendor-compliance/contracts — Erstellen
|
|
PUT /vendor-compliance/contracts/{id} — Update
|
|
DELETE /vendor-compliance/contracts/{id} — Loeschen
|
|
|
|
Findings (5):
|
|
GET /vendor-compliance/findings — Liste
|
|
GET /vendor-compliance/findings/{id} — Detail
|
|
POST /vendor-compliance/findings — Erstellen
|
|
PUT /vendor-compliance/findings/{id} — Update
|
|
DELETE /vendor-compliance/findings/{id} — Loeschen
|
|
|
|
Control Instances (5):
|
|
GET /vendor-compliance/control-instances — Liste
|
|
GET /vendor-compliance/control-instances/{id} — Detail
|
|
POST /vendor-compliance/control-instances — Erstellen
|
|
PUT /vendor-compliance/control-instances/{id} — Update
|
|
DELETE /vendor-compliance/control-instances/{id} — Loeschen
|
|
|
|
Controls Library (3):
|
|
GET /vendor-compliance/controls — Alle Controls
|
|
POST /vendor-compliance/controls — Erstellen
|
|
DELETE /vendor-compliance/controls/{id} — Loeschen
|
|
|
|
Export Stubs (3):
|
|
POST /vendor-compliance/export — 501
|
|
GET /vendor-compliance/export/{id} — 501
|
|
GET /vendor-compliance/export/{id}/download — 501
|
|
|
|
Phase 1 Step 4 refactor: handlers delegate to VendorService,
|
|
ContractService, FindingService, ControlInstanceService, and
|
|
ControlsLibraryService. Module-level helpers re-exported for legacy
|
|
test imports.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from classroom_engine.database import get_db
|
|
from compliance.api._http_errors import translate_domain_errors
|
|
from compliance.services.vendor_compliance_service import (
|
|
DEFAULT_TENANT_ID, # noqa: F401 — re-export
|
|
VendorService,
|
|
_get, # noqa: F401 — re-export
|
|
_now_iso,
|
|
_ok, # noqa: F401 — re-export
|
|
_parse_json, # noqa: F401 — re-export
|
|
_to_camel, # noqa: F401 — re-export
|
|
_to_snake, # noqa: F401 — re-export
|
|
_ts, # noqa: F401 — re-export
|
|
_vendor_to_response, # noqa: F401 — re-export
|
|
_VENDOR_CAMEL_TO_SNAKE, # noqa: F401 — re-export
|
|
_VENDOR_SNAKE_TO_CAMEL, # noqa: F401 — re-export
|
|
)
|
|
from compliance.services.vendor_compliance_sub_service import (
|
|
ContractService,
|
|
_contract_to_response, # noqa: F401 — re-export
|
|
_control_instance_to_response, # noqa: F401 — re-export
|
|
_finding_to_response, # noqa: F401 — re-export
|
|
)
|
|
from compliance.services.vendor_compliance_extra_service import (
|
|
ControlInstanceService,
|
|
ControlsLibraryService,
|
|
FindingService,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/vendor-compliance", tags=["vendor-compliance"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Service factories
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _vendor_svc(db: Session = Depends(get_db)) -> VendorService:
|
|
return VendorService(db)
|
|
|
|
|
|
def _contract_svc(db: Session = Depends(get_db)) -> ContractService:
|
|
return ContractService(db)
|
|
|
|
|
|
def _finding_svc(db: Session = Depends(get_db)) -> FindingService:
|
|
return FindingService(db)
|
|
|
|
|
|
def _ci_svc(db: Session = Depends(get_db)) -> ControlInstanceService:
|
|
return ControlInstanceService(db)
|
|
|
|
|
|
def _ctrl_svc(db: Session = Depends(get_db)) -> ControlsLibraryService:
|
|
return ControlsLibraryService(db)
|
|
|
|
|
|
# ============================================================================
|
|
# Vendors
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/vendors/stats")
|
|
def get_vendor_stats(
|
|
tenant_id: Optional[str] = Query(None),
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.get_stats(tenant_id)
|
|
|
|
|
|
@router.get("/vendors")
|
|
def list_vendors(
|
|
tenant_id: Optional[str] = Query(None),
|
|
status: Optional[str] = Query(None),
|
|
risk_level: Optional[str] = Query(None, alias="riskLevel"),
|
|
search: Optional[str] = Query(None),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_vendors(tenant_id, status, risk_level, search, skip, limit)
|
|
|
|
|
|
@router.get("/vendors/{vendor_id}")
|
|
def get_vendor(
|
|
vendor_id: str,
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.get_vendor(vendor_id)
|
|
|
|
|
|
@router.post("/vendors", status_code=201)
|
|
def create_vendor(
|
|
body: dict = {},
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_vendor(body)
|
|
|
|
|
|
@router.put("/vendors/{vendor_id}")
|
|
def update_vendor(
|
|
vendor_id: str,
|
|
body: dict = {},
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_vendor(vendor_id, body)
|
|
|
|
|
|
@router.delete("/vendors/{vendor_id}")
|
|
def delete_vendor(
|
|
vendor_id: str,
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_vendor(vendor_id)
|
|
|
|
|
|
@router.patch("/vendors/{vendor_id}/status")
|
|
def patch_vendor_status(
|
|
vendor_id: str,
|
|
body: dict = {},
|
|
svc: VendorService = Depends(_vendor_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.patch_status(vendor_id, body)
|
|
|
|
|
|
# ============================================================================
|
|
# Contracts
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/contracts")
|
|
def list_contracts(
|
|
tenant_id: Optional[str] = Query(None),
|
|
vendor_id: Optional[str] = Query(None),
|
|
status: Optional[str] = Query(None),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
svc: ContractService = Depends(_contract_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_contracts(tenant_id, vendor_id, status, skip, limit)
|
|
|
|
|
|
@router.get("/contracts/{contract_id}")
|
|
def get_contract(
|
|
contract_id: str,
|
|
svc: ContractService = Depends(_contract_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.get_contract(contract_id)
|
|
|
|
|
|
@router.post("/contracts", status_code=201)
|
|
def create_contract(
|
|
body: dict = {},
|
|
svc: ContractService = Depends(_contract_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_contract(body)
|
|
|
|
|
|
@router.put("/contracts/{contract_id}")
|
|
def update_contract(
|
|
contract_id: str,
|
|
body: dict = {},
|
|
svc: ContractService = Depends(_contract_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_contract(contract_id, body)
|
|
|
|
|
|
@router.delete("/contracts/{contract_id}")
|
|
def delete_contract(
|
|
contract_id: str,
|
|
svc: ContractService = Depends(_contract_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_contract(contract_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Findings
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/findings")
|
|
def list_findings(
|
|
tenant_id: Optional[str] = Query(None),
|
|
vendor_id: Optional[str] = Query(None),
|
|
severity: Optional[str] = Query(None),
|
|
status: Optional[str] = Query(None),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
svc: FindingService = Depends(_finding_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_findings(tenant_id, vendor_id, severity, status, skip, limit)
|
|
|
|
|
|
@router.get("/findings/{finding_id}")
|
|
def get_finding(
|
|
finding_id: str,
|
|
svc: FindingService = Depends(_finding_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.get_finding(finding_id)
|
|
|
|
|
|
@router.post("/findings", status_code=201)
|
|
def create_finding(
|
|
body: dict = {},
|
|
svc: FindingService = Depends(_finding_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_finding(body)
|
|
|
|
|
|
@router.put("/findings/{finding_id}")
|
|
def update_finding(
|
|
finding_id: str,
|
|
body: dict = {},
|
|
svc: FindingService = Depends(_finding_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_finding(finding_id, body)
|
|
|
|
|
|
@router.delete("/findings/{finding_id}")
|
|
def delete_finding(
|
|
finding_id: str,
|
|
svc: FindingService = Depends(_finding_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_finding(finding_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Control Instances
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/control-instances")
|
|
def list_control_instances(
|
|
tenant_id: Optional[str] = Query(None),
|
|
vendor_id: Optional[str] = Query(None),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
svc: ControlInstanceService = Depends(_ci_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_instances(tenant_id, vendor_id, skip, limit)
|
|
|
|
|
|
@router.get("/control-instances/{instance_id}")
|
|
def get_control_instance(
|
|
instance_id: str,
|
|
svc: ControlInstanceService = Depends(_ci_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.get_instance(instance_id)
|
|
|
|
|
|
@router.post("/control-instances", status_code=201)
|
|
def create_control_instance(
|
|
body: dict = {},
|
|
svc: ControlInstanceService = Depends(_ci_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_instance(body)
|
|
|
|
|
|
@router.put("/control-instances/{instance_id}")
|
|
def update_control_instance(
|
|
instance_id: str,
|
|
body: dict = {},
|
|
svc: ControlInstanceService = Depends(_ci_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_instance(instance_id, body)
|
|
|
|
|
|
@router.delete("/control-instances/{instance_id}")
|
|
def delete_control_instance(
|
|
instance_id: str,
|
|
svc: ControlInstanceService = Depends(_ci_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_instance(instance_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Controls Library
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/controls")
|
|
def list_controls(
|
|
tenant_id: Optional[str] = Query(None),
|
|
domain: Optional[str] = Query(None),
|
|
svc: ControlsLibraryService = Depends(_ctrl_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_controls(tenant_id, domain)
|
|
|
|
|
|
@router.post("/controls", status_code=201)
|
|
def create_control(
|
|
body: dict = {},
|
|
svc: ControlsLibraryService = Depends(_ctrl_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_control(body)
|
|
|
|
|
|
@router.delete("/controls/{control_id}")
|
|
def delete_control(
|
|
control_id: str,
|
|
svc: ControlsLibraryService = Depends(_ctrl_svc),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_control(control_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Export Stubs (501 Not Implemented)
|
|
# ============================================================================
|
|
|
|
|
|
@router.post("/export", status_code=501)
|
|
def export_report():
|
|
return {"success": False, "error": "Export not implemented yet", "timestamp": _now_iso()}
|
|
|
|
|
|
@router.get("/export/{report_id}", status_code=501)
|
|
def get_export(report_id: str):
|
|
return {"success": False, "error": "Export not implemented yet", "timestamp": _now_iso()}
|
|
|
|
|
|
@router.get("/export/{report_id}/download", status_code=501)
|
|
def download_export(report_id: str):
|
|
return {"success": False, "error": "Export not implemented yet", "timestamp": _now_iso()}
|