50 lines
1.5 KiB
Python
50 lines
1.5 KiB
Python
"""Shared timezone-aware UTC helpers for runtime persistence and comparisons."""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
UTC = timezone.utc
|
|
UTC_MIN = datetime.min.replace(tzinfo=UTC)
|
|
|
|
|
|
def ensure_utc(value: datetime) -> datetime:
|
|
"""Normalize a datetime to timezone-aware UTC.
|
|
|
|
IMPORTANT: legacy persisted timestamps in Doctor were historically naive UTC.
|
|
Treating naive values as UTC preserves backward compatibility across upgrades.
|
|
"""
|
|
if value.tzinfo is None:
|
|
return value.replace(tzinfo=UTC)
|
|
return value.astimezone(UTC)
|
|
|
|
|
|
|
|
def utc_now() -> datetime:
|
|
"""Return the current timezone-aware UTC datetime."""
|
|
return datetime.now(UTC)
|
|
|
|
|
|
|
|
def utc_isoformat(value: Optional[datetime] = None) -> str:
|
|
"""Serialize a datetime as ISO-8601 UTC with a trailing `Z`."""
|
|
normalized = ensure_utc(value or utc_now())
|
|
return normalized.isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
|
|
def utc_filename_timestamp(value: Optional[datetime] = None) -> str:
|
|
"""Stable UTC timestamp for backup/migration filenames."""
|
|
return ensure_utc(value or utc_now()).strftime("%Y%m%d-%H%M%S")
|
|
|
|
|
|
|
|
def parse_utc_timestamp(value: Optional[str]) -> Optional[datetime]:
|
|
"""Parse legacy/new persisted timestamps into aware UTC datetimes."""
|
|
if not value:
|
|
return None
|
|
try:
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except (ValueError, TypeError, AttributeError):
|
|
return None
|
|
return ensure_utc(parsed)
|