GitHub Action
deploy: worker release from GitHub
8ff1b66
"""
Product-level analysis freshness rules.
"""
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import Any, cast
from app.core.config import settings
class FreshnessStatus(str, Enum):
"""Product freshness state for an existing analysis."""
FRESH = "fresh"
STALE_BY_AGE = "stale_by_age"
STALE_BY_PATCH = "stale_by_patch"
def _as_utc_datetime(value: Any) -> datetime | None:
if value is None:
return None
if isinstance(value, datetime):
return value if value.tzinfo is not None else value.replace(tzinfo=timezone.utc)
if isinstance(value, str):
parsed = datetime.fromisoformat(value)
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=timezone.utc)
return None
def get_analysis_reference_at(document: dict[str, Any]) -> datetime | None:
"""Return the best available execution timestamp for freshness checks."""
raw = document.get("results")
results: dict[str, Any] = cast(dict[str, Any], raw) if isinstance(raw, dict) else {}
return (
_as_utc_datetime(results.get("analysis_date"))
or _as_utc_datetime(document.get("analyzed_at"))
or _as_utc_datetime(document.get("cached_at"))
)
def evaluate_freshness(
document: dict[str, Any],
current_patch_at: datetime | None,
) -> FreshnessStatus:
"""
Evaluate analysis freshness using product rules:
patch recency first, then max age.
"""
analysis_at = get_analysis_reference_at(document)
if analysis_at is None:
return FreshnessStatus.STALE_BY_AGE
if current_patch_at is not None and analysis_at < current_patch_at:
return FreshnessStatus.STALE_BY_PATCH
age_days = (datetime.now(timezone.utc) - analysis_at).days
if age_days > settings.analysis_freshness_max_age_days:
return FreshnessStatus.STALE_BY_AGE
return FreshnessStatus.FRESH
def get_staleness_reason(status: FreshnessStatus) -> str | None:
if status == FreshnessStatus.STALE_BY_AGE:
return "STALE_REASON_AGE"
if status == FreshnessStatus.STALE_BY_PATCH:
return "STALE_REASON_PATCH"
return None