"""Retention minimization for governed provider-report results.""" from __future__ import annotations from datetime import UTC, datetime from sqlalchemy.orm import Session from govoplan_reporting.backend.db.models import ReportingProviderExecution class ReportingRetentionService: def apply_retention( self, session: object, *, dry_run: bool, now: datetime, limit: int = 500, ) -> dict[str, int]: if not isinstance(session, Session): raise TypeError("Reporting retention requires a SQLAlchemy Session") observed_at = _aware(now) rows = ( session.query(ReportingProviderExecution) .filter( ReportingProviderExecution.expires_at.is_not(None), ReportingProviderExecution.expires_at <= observed_at, ReportingProviderExecution.retention_redacted_at.is_(None), ) .order_by( ReportingProviderExecution.expires_at.asc(), ReportingProviderExecution.id.asc(), ) .limit(max(1, min(int(limit), 5_000))) .all() ) counts = { "eligible": len(rows), "redacted": 0, "remaining_in_batch": 0, } if dry_run: return counts for row in rows: # Keep immutable request/output hashes and provenance as audit # evidence while removing the retained report detail itself. row.result_payload = {} row.retention_redacted_at = observed_at counts["redacted"] += 1 session.flush() counts["remaining_in_batch"] = ( session.query(ReportingProviderExecution.id) .filter( ReportingProviderExecution.expires_at.is_not(None), ReportingProviderExecution.expires_at <= observed_at, ReportingProviderExecution.retention_redacted_at.is_(None), ) .limit(1) .count() ) return counts def _aware(value: datetime) -> datetime: return value if value.tzinfo is not None else value.replace(tzinfo=UTC) __all__ = ["ReportingRetentionService"]