""" 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 EvidenceRepository: """Repository for evidence.""" def __init__(self, db: DBSession): self.db = db def create( self, control_id: str, evidence_type: str, title: str, description: Optional[str] = None, artifact_path: Optional[str] = None, artifact_url: Optional[str] = None, artifact_hash: Optional[str] = None, file_size_bytes: Optional[int] = None, mime_type: Optional[str] = None, valid_until: Optional[datetime] = None, source: str = "manual", ci_job_id: Optional[str] = None, uploaded_by: Optional[str] = None, ) -> EvidenceDB: """Create evidence record.""" # Get control UUID control = self.db.query(ControlDB).filter(ControlDB.control_id == control_id).first() if not control: raise ValueError(f"Control {control_id} not found") evidence = EvidenceDB( id=str(uuid.uuid4()), control_id=control.id, evidence_type=evidence_type, title=title, description=description, artifact_path=artifact_path, artifact_url=artifact_url, artifact_hash=artifact_hash, file_size_bytes=file_size_bytes, mime_type=mime_type, valid_until=valid_until, source=source, ci_job_id=ci_job_id, uploaded_by=uploaded_by, ) self.db.add(evidence) self.db.commit() self.db.refresh(evidence) return evidence def get_by_id(self, evidence_id: str) -> Optional[EvidenceDB]: """Get evidence by ID.""" return self.db.query(EvidenceDB).filter(EvidenceDB.id == evidence_id).first() def get_by_control( self, control_id: str, status: Optional[EvidenceStatusEnum] = None ) -> List[EvidenceDB]: """Get all evidence for a control.""" control = self.db.query(ControlDB).filter(ControlDB.control_id == control_id).first() if not control: return [] query = self.db.query(EvidenceDB).filter(EvidenceDB.control_id == control.id) if status: query = query.filter(EvidenceDB.status == status) return query.order_by(EvidenceDB.collected_at.desc()).all() def get_all( self, evidence_type: Optional[str] = None, status: Optional[EvidenceStatusEnum] = None, limit: int = 100, ) -> List[EvidenceDB]: """Get all evidence with filters.""" query = self.db.query(EvidenceDB) if evidence_type: query = query.filter(EvidenceDB.evidence_type == evidence_type) if status: query = query.filter(EvidenceDB.status == status) return query.order_by(EvidenceDB.collected_at.desc()).limit(limit).all() def update_status(self, evidence_id: str, status: EvidenceStatusEnum) -> Optional[EvidenceDB]: """Update evidence status.""" evidence = self.get_by_id(evidence_id) if not evidence: return None evidence.status = status evidence.updated_at = datetime.now(timezone.utc) self.db.commit() self.db.refresh(evidence) return evidence def get_statistics(self) -> Dict[str, Any]: """Get evidence statistics.""" total = self.db.query(func.count(EvidenceDB.id)).scalar() by_type = dict( self.db.query(EvidenceDB.evidence_type, func.count(EvidenceDB.id)) .group_by(EvidenceDB.evidence_type) .all() ) by_status = dict( self.db.query(EvidenceDB.status, func.count(EvidenceDB.id)) .group_by(EvidenceDB.status) .all() ) valid = by_status.get(EvidenceStatusEnum.VALID, 0) coverage = (valid / total * 100) if total > 0 else 0 return { "total": total, "by_type": by_type, "by_status": {str(k.value) if k else "none": v for k, v in by_status.items()}, "coverage_percent": round(coverage, 1), }