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>
269 lines
9.4 KiB
Python
269 lines
9.4 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 RegulationRepository:
|
|
"""Repository for regulations/standards."""
|
|
|
|
def __init__(self, db: DBSession):
|
|
self.db = db
|
|
|
|
def create(
|
|
self,
|
|
code: str,
|
|
name: str,
|
|
regulation_type: RegulationTypeEnum,
|
|
full_name: Optional[str] = None,
|
|
source_url: Optional[str] = None,
|
|
local_pdf_path: Optional[str] = None,
|
|
effective_date: Optional[date] = None,
|
|
description: Optional[str] = None,
|
|
) -> RegulationDB:
|
|
"""Create a new regulation."""
|
|
regulation = RegulationDB(
|
|
id=str(uuid.uuid4()),
|
|
code=code,
|
|
name=name,
|
|
full_name=full_name,
|
|
regulation_type=regulation_type,
|
|
source_url=source_url,
|
|
local_pdf_path=local_pdf_path,
|
|
effective_date=effective_date,
|
|
description=description,
|
|
)
|
|
self.db.add(regulation)
|
|
self.db.commit()
|
|
self.db.refresh(regulation)
|
|
return regulation
|
|
|
|
def get_by_id(self, regulation_id: str) -> Optional[RegulationDB]:
|
|
"""Get regulation by ID."""
|
|
return self.db.query(RegulationDB).filter(RegulationDB.id == regulation_id).first()
|
|
|
|
def get_by_code(self, code: str) -> Optional[RegulationDB]:
|
|
"""Get regulation by code (e.g., 'GDPR')."""
|
|
return self.db.query(RegulationDB).filter(RegulationDB.code == code).first()
|
|
|
|
def get_all(
|
|
self,
|
|
regulation_type: Optional[RegulationTypeEnum] = None,
|
|
is_active: Optional[bool] = True
|
|
) -> List[RegulationDB]:
|
|
"""Get all regulations with optional filters."""
|
|
query = self.db.query(RegulationDB)
|
|
if regulation_type:
|
|
query = query.filter(RegulationDB.regulation_type == regulation_type)
|
|
if is_active is not None:
|
|
query = query.filter(RegulationDB.is_active == is_active)
|
|
return query.order_by(RegulationDB.code).all()
|
|
|
|
def update(self, regulation_id: str, **kwargs) -> Optional[RegulationDB]:
|
|
"""Update a regulation."""
|
|
regulation = self.get_by_id(regulation_id)
|
|
if not regulation:
|
|
return None
|
|
for key, value in kwargs.items():
|
|
if hasattr(regulation, key):
|
|
setattr(regulation, key, value)
|
|
regulation.updated_at = datetime.now(timezone.utc)
|
|
self.db.commit()
|
|
self.db.refresh(regulation)
|
|
return regulation
|
|
|
|
def delete(self, regulation_id: str) -> bool:
|
|
"""Delete a regulation."""
|
|
regulation = self.get_by_id(regulation_id)
|
|
if not regulation:
|
|
return False
|
|
self.db.delete(regulation)
|
|
self.db.commit()
|
|
return True
|
|
|
|
def get_active(self) -> List[RegulationDB]:
|
|
"""Get all active regulations."""
|
|
return self.get_all(is_active=True)
|
|
|
|
def count(self) -> int:
|
|
"""Count all regulations."""
|
|
return self.db.query(func.count(RegulationDB.id)).scalar() or 0
|
|
|
|
|
|
class RequirementRepository:
|
|
"""Repository for requirements."""
|
|
|
|
def __init__(self, db: DBSession):
|
|
self.db = db
|
|
|
|
def create(
|
|
self,
|
|
regulation_id: str,
|
|
article: str,
|
|
title: str,
|
|
paragraph: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
requirement_text: Optional[str] = None,
|
|
breakpilot_interpretation: Optional[str] = None,
|
|
is_applicable: bool = True,
|
|
priority: int = 2,
|
|
) -> RequirementDB:
|
|
"""Create a new requirement."""
|
|
requirement = RequirementDB(
|
|
id=str(uuid.uuid4()),
|
|
regulation_id=regulation_id,
|
|
article=article,
|
|
paragraph=paragraph,
|
|
title=title,
|
|
description=description,
|
|
requirement_text=requirement_text,
|
|
breakpilot_interpretation=breakpilot_interpretation,
|
|
is_applicable=is_applicable,
|
|
priority=priority,
|
|
)
|
|
self.db.add(requirement)
|
|
self.db.commit()
|
|
self.db.refresh(requirement)
|
|
return requirement
|
|
|
|
def get_by_id(self, requirement_id: str) -> Optional[RequirementDB]:
|
|
"""Get requirement by ID with eager-loaded relationships."""
|
|
return (
|
|
self.db.query(RequirementDB)
|
|
.options(
|
|
selectinload(RequirementDB.control_mappings).selectinload(ControlMappingDB.control),
|
|
joinedload(RequirementDB.regulation)
|
|
)
|
|
.filter(RequirementDB.id == requirement_id)
|
|
.first()
|
|
)
|
|
|
|
def get_by_regulation(
|
|
self,
|
|
regulation_id: str,
|
|
is_applicable: Optional[bool] = None
|
|
) -> List[RequirementDB]:
|
|
"""Get all requirements for a regulation with eager-loaded controls."""
|
|
query = (
|
|
self.db.query(RequirementDB)
|
|
.options(
|
|
selectinload(RequirementDB.control_mappings).selectinload(ControlMappingDB.control),
|
|
joinedload(RequirementDB.regulation)
|
|
)
|
|
.filter(RequirementDB.regulation_id == regulation_id)
|
|
)
|
|
if is_applicable is not None:
|
|
query = query.filter(RequirementDB.is_applicable == is_applicable)
|
|
return query.order_by(RequirementDB.article, RequirementDB.paragraph).all()
|
|
|
|
def get_by_regulation_code(self, code: str) -> List[RequirementDB]:
|
|
"""Get requirements by regulation code with eager-loaded relationships."""
|
|
return (
|
|
self.db.query(RequirementDB)
|
|
.options(
|
|
selectinload(RequirementDB.control_mappings).selectinload(ControlMappingDB.control),
|
|
joinedload(RequirementDB.regulation)
|
|
)
|
|
.join(RegulationDB)
|
|
.filter(RegulationDB.code == code)
|
|
.order_by(RequirementDB.article, RequirementDB.paragraph)
|
|
.all()
|
|
)
|
|
|
|
def get_all(self, is_applicable: Optional[bool] = None) -> List[RequirementDB]:
|
|
"""Get all requirements with optional filter and eager-loading."""
|
|
query = (
|
|
self.db.query(RequirementDB)
|
|
.options(
|
|
selectinload(RequirementDB.control_mappings).selectinload(ControlMappingDB.control),
|
|
joinedload(RequirementDB.regulation)
|
|
)
|
|
)
|
|
if is_applicable is not None:
|
|
query = query.filter(RequirementDB.is_applicable == is_applicable)
|
|
return query.order_by(RequirementDB.article, RequirementDB.paragraph).all()
|
|
|
|
def get_paginated(
|
|
self,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
regulation_code: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
is_applicable: Optional[bool] = None,
|
|
search: Optional[str] = None,
|
|
) -> Tuple[List[RequirementDB], int]:
|
|
"""
|
|
Get paginated requirements with eager-loaded relationships.
|
|
Returns tuple of (items, total_count).
|
|
"""
|
|
query = (
|
|
self.db.query(RequirementDB)
|
|
.options(
|
|
selectinload(RequirementDB.control_mappings).selectinload(ControlMappingDB.control),
|
|
joinedload(RequirementDB.regulation)
|
|
)
|
|
)
|
|
|
|
# Filters
|
|
if regulation_code:
|
|
query = query.join(RegulationDB).filter(RegulationDB.code == regulation_code)
|
|
if status:
|
|
query = query.filter(RequirementDB.implementation_status == status)
|
|
if is_applicable is not None:
|
|
query = query.filter(RequirementDB.is_applicable == is_applicable)
|
|
if search:
|
|
search_term = f"%{search}%"
|
|
query = query.filter(
|
|
or_(
|
|
RequirementDB.title.ilike(search_term),
|
|
RequirementDB.description.ilike(search_term),
|
|
RequirementDB.article.ilike(search_term),
|
|
)
|
|
)
|
|
|
|
# Count before pagination
|
|
total = query.count()
|
|
|
|
# Apply pagination and ordering
|
|
items = (
|
|
query
|
|
.order_by(RequirementDB.priority.desc(), RequirementDB.article, RequirementDB.paragraph)
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
.all()
|
|
)
|
|
|
|
return items, total
|
|
|
|
def delete(self, requirement_id: str) -> bool:
|
|
"""Delete a requirement."""
|
|
requirement = self.db.query(RequirementDB).filter(RequirementDB.id == requirement_id).first()
|
|
if not requirement:
|
|
return False
|
|
self.db.delete(requirement)
|
|
self.db.commit()
|
|
return True
|
|
|
|
def count(self) -> int:
|
|
"""Count all requirements."""
|
|
return self.db.query(func.count(RequirementDB.id)).scalar() or 0
|
|
|