Files
govoplan-reporting/src/govoplan_reporting/backend/retention.py
T

70 lines
2.2 KiB
Python

"""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"]