Files
breakpilot-compliance/backend-compliance/compliance/api/vendor_compliance_routes.py
Sharang Parnerkar a84dccb339 refactor(backend/api): extract vendor compliance services (Step 4)
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>
2026-04-09 20:11:24 +02:00

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()}