70 lines
2.2 KiB
Python
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"]
|