Phase 1 Step 5 of PHASE1_RUNBOOK.md.
compliance/db/repository.py (1547 LOC) decomposed into seven sibling
per-aggregate repository modules:
regulation_repository.py (268) — Regulation + Requirement
control_repository.py (291) — Control + ControlMapping
evidence_repository.py (143)
risk_repository.py (148)
audit_export_repository.py (110)
service_module_repository.py (247)
audit_session_repository.py (478) — AuditSession + AuditSignOff
compliance/db/isms_repository.py (838 LOC) decomposed into two
sub-aggregate modules mirroring the models split:
isms_governance_repository.py (354) — Scope, Policy, Objective, SoA
isms_audit_repository.py (499) — Finding, CAPA, Review, Internal Audit,
Trail, Readiness
Both original files become thin re-export shims (37 and 25 LOC
respectively) so every existing import continues to work unchanged.
New code SHOULD import from the aggregate module directly.
All new sibling files under the 500-line hard cap; largest is
isms_audit_repository.py at 499 (on the edge; when Phase 1 Step 4
router->service extraction lands, the audit_session repo may split
further if growth exceeds 500).
Verified:
- 173/173 pytest compliance/tests/ tests/contracts/ pass
- OpenAPI 360 paths / 484 operations unchanged
- All repo files under 500 LOC
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
248 lines
9.0 KiB
Python
248 lines
9.0 KiB
Python
"""
|
|
Compliance repositories — extracted from compliance/db/repository.py.
|
|
|
|
Phase 1 Step 5: the monolithic repository module is decomposed per
|
|
aggregate. Every repository class is re-exported from
|
|
``compliance.db.repository`` for backwards compatibility.
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime, date, timezone
|
|
from typing import List, Optional, Dict, Any, Tuple
|
|
|
|
from sqlalchemy.orm import Session as DBSession, selectinload, joinedload
|
|
from sqlalchemy import func, and_, or_
|
|
|
|
from compliance.db.models import (
|
|
RegulationDB, RequirementDB, ControlDB, ControlMappingDB,
|
|
EvidenceDB, RiskDB, AuditExportDB,
|
|
AuditSessionDB, AuditSignOffDB, AuditResultEnum, AuditSessionStatusEnum,
|
|
RegulationTypeEnum, ControlDomainEnum, ControlStatusEnum,
|
|
RiskLevelEnum, EvidenceStatusEnum, ExportStatusEnum,
|
|
ServiceModuleDB, ModuleRegulationMappingDB,
|
|
)
|
|
|
|
class ServiceModuleRepository:
|
|
"""Repository for service modules (Sprint 3)."""
|
|
|
|
def __init__(self, db: DBSession):
|
|
self.db = db
|
|
|
|
def create(
|
|
self,
|
|
name: str,
|
|
display_name: str,
|
|
service_type: str,
|
|
description: Optional[str] = None,
|
|
port: Optional[int] = None,
|
|
technology_stack: Optional[List[str]] = None,
|
|
repository_path: Optional[str] = None,
|
|
docker_image: Optional[str] = None,
|
|
data_categories: Optional[List[str]] = None,
|
|
processes_pii: bool = False,
|
|
processes_health_data: bool = False,
|
|
ai_components: bool = False,
|
|
criticality: str = "medium",
|
|
owner_team: Optional[str] = None,
|
|
owner_contact: Optional[str] = None,
|
|
) -> "ServiceModuleDB":
|
|
"""Create a service module."""
|
|
from .models import ServiceModuleDB, ServiceTypeEnum
|
|
|
|
module = ServiceModuleDB(
|
|
id=str(uuid.uuid4()),
|
|
name=name,
|
|
display_name=display_name,
|
|
description=description,
|
|
service_type=ServiceTypeEnum(service_type),
|
|
port=port,
|
|
technology_stack=technology_stack or [],
|
|
repository_path=repository_path,
|
|
docker_image=docker_image,
|
|
data_categories=data_categories or [],
|
|
processes_pii=processes_pii,
|
|
processes_health_data=processes_health_data,
|
|
ai_components=ai_components,
|
|
criticality=criticality,
|
|
owner_team=owner_team,
|
|
owner_contact=owner_contact,
|
|
)
|
|
self.db.add(module)
|
|
self.db.commit()
|
|
self.db.refresh(module)
|
|
return module
|
|
|
|
def get_by_id(self, module_id: str) -> Optional["ServiceModuleDB"]:
|
|
"""Get module by ID."""
|
|
from .models import ServiceModuleDB
|
|
return self.db.query(ServiceModuleDB).filter(ServiceModuleDB.id == module_id).first()
|
|
|
|
def get_by_name(self, name: str) -> Optional["ServiceModuleDB"]:
|
|
"""Get module by name."""
|
|
from .models import ServiceModuleDB
|
|
return self.db.query(ServiceModuleDB).filter(ServiceModuleDB.name == name).first()
|
|
|
|
def get_all(
|
|
self,
|
|
service_type: Optional[str] = None,
|
|
criticality: Optional[str] = None,
|
|
processes_pii: Optional[bool] = None,
|
|
ai_components: Optional[bool] = None,
|
|
) -> List["ServiceModuleDB"]:
|
|
"""Get all modules with filters."""
|
|
from .models import ServiceModuleDB, ServiceTypeEnum
|
|
|
|
query = self.db.query(ServiceModuleDB).filter(ServiceModuleDB.is_active)
|
|
|
|
if service_type:
|
|
query = query.filter(ServiceModuleDB.service_type == ServiceTypeEnum(service_type))
|
|
if criticality:
|
|
query = query.filter(ServiceModuleDB.criticality == criticality)
|
|
if processes_pii is not None:
|
|
query = query.filter(ServiceModuleDB.processes_pii == processes_pii)
|
|
if ai_components is not None:
|
|
query = query.filter(ServiceModuleDB.ai_components == ai_components)
|
|
|
|
return query.order_by(ServiceModuleDB.name).all()
|
|
|
|
def get_with_regulations(self, module_id: str) -> Optional["ServiceModuleDB"]:
|
|
"""Get module with regulation mappings loaded."""
|
|
from .models import ServiceModuleDB, ModuleRegulationMappingDB
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
return (
|
|
self.db.query(ServiceModuleDB)
|
|
.options(
|
|
selectinload(ServiceModuleDB.regulation_mappings)
|
|
.selectinload(ModuleRegulationMappingDB.regulation)
|
|
)
|
|
.filter(ServiceModuleDB.id == module_id)
|
|
.first()
|
|
)
|
|
|
|
def add_regulation_mapping(
|
|
self,
|
|
module_id: str,
|
|
regulation_id: str,
|
|
relevance_level: str = "medium",
|
|
notes: Optional[str] = None,
|
|
applicable_articles: Optional[List[str]] = None,
|
|
) -> "ModuleRegulationMappingDB":
|
|
"""Add a regulation mapping to a module."""
|
|
from .models import ModuleRegulationMappingDB, RelevanceLevelEnum
|
|
|
|
mapping = ModuleRegulationMappingDB(
|
|
id=str(uuid.uuid4()),
|
|
module_id=module_id,
|
|
regulation_id=regulation_id,
|
|
relevance_level=RelevanceLevelEnum(relevance_level),
|
|
notes=notes,
|
|
applicable_articles=applicable_articles,
|
|
)
|
|
self.db.add(mapping)
|
|
self.db.commit()
|
|
self.db.refresh(mapping)
|
|
return mapping
|
|
|
|
def get_overview(self) -> Dict[str, Any]:
|
|
"""Get overview statistics for all modules."""
|
|
from .models import ModuleRegulationMappingDB
|
|
|
|
modules = self.get_all()
|
|
total = len(modules)
|
|
|
|
by_type = {}
|
|
by_criticality = {}
|
|
pii_count = 0
|
|
ai_count = 0
|
|
|
|
for m in modules:
|
|
type_key = m.service_type.value if m.service_type else "unknown"
|
|
by_type[type_key] = by_type.get(type_key, 0) + 1
|
|
by_criticality[m.criticality] = by_criticality.get(m.criticality, 0) + 1
|
|
if m.processes_pii:
|
|
pii_count += 1
|
|
if m.ai_components:
|
|
ai_count += 1
|
|
|
|
# Get regulation coverage
|
|
regulation_coverage = {}
|
|
mappings = self.db.query(ModuleRegulationMappingDB).all()
|
|
for mapping in mappings:
|
|
reg = mapping.regulation
|
|
if reg:
|
|
code = reg.code
|
|
regulation_coverage[code] = regulation_coverage.get(code, 0) + 1
|
|
|
|
# Calculate average compliance score
|
|
scores = [m.compliance_score for m in modules if m.compliance_score is not None]
|
|
avg_score = sum(scores) / len(scores) if scores else None
|
|
|
|
return {
|
|
"total_modules": total,
|
|
"modules_by_type": by_type,
|
|
"modules_by_criticality": by_criticality,
|
|
"modules_processing_pii": pii_count,
|
|
"modules_with_ai": ai_count,
|
|
"average_compliance_score": round(avg_score, 1) if avg_score else None,
|
|
"regulations_coverage": regulation_coverage,
|
|
}
|
|
|
|
def seed_from_data(self, services_data: List[Dict[str, Any]], force: bool = False) -> Dict[str, int]:
|
|
"""Seed modules from service_modules.py data."""
|
|
|
|
modules_created = 0
|
|
mappings_created = 0
|
|
|
|
for svc in services_data:
|
|
# Check if module exists
|
|
existing = self.get_by_name(svc["name"])
|
|
if existing and not force:
|
|
continue
|
|
|
|
if existing and force:
|
|
# Delete existing module (cascades to mappings)
|
|
self.db.delete(existing)
|
|
self.db.commit()
|
|
|
|
# Create module
|
|
module = self.create(
|
|
name=svc["name"],
|
|
display_name=svc["display_name"],
|
|
description=svc.get("description"),
|
|
service_type=svc["service_type"],
|
|
port=svc.get("port"),
|
|
technology_stack=svc.get("technology_stack"),
|
|
repository_path=svc.get("repository_path"),
|
|
docker_image=svc.get("docker_image"),
|
|
data_categories=svc.get("data_categories"),
|
|
processes_pii=svc.get("processes_pii", False),
|
|
processes_health_data=svc.get("processes_health_data", False),
|
|
ai_components=svc.get("ai_components", False),
|
|
criticality=svc.get("criticality", "medium"),
|
|
owner_team=svc.get("owner_team"),
|
|
)
|
|
modules_created += 1
|
|
|
|
# Create regulation mappings
|
|
for reg_data in svc.get("regulations", []):
|
|
# Find regulation by code
|
|
reg = self.db.query(RegulationDB).filter(
|
|
RegulationDB.code == reg_data["code"]
|
|
).first()
|
|
|
|
if reg:
|
|
self.add_regulation_mapping(
|
|
module_id=module.id,
|
|
regulation_id=reg.id,
|
|
relevance_level=reg_data.get("relevance", "medium"),
|
|
notes=reg_data.get("notes"),
|
|
)
|
|
mappings_created += 1
|
|
|
|
return {
|
|
"modules_created": modules_created,
|
|
"mappings_created": mappings_created,
|
|
}
|
|
|