generated from coulomb/repo-seed
59 lines
1.6 KiB
Python
59 lines
1.6 KiB
Python
"""Small deterministic helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import is_dataclass, asdict
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
import hashlib
|
|
import json
|
|
from typing import Any
|
|
|
|
|
|
def utc_now_iso() -> str:
|
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
|
|
|
|
def stable_digest(value: Any, *, length: int = 12) -> str:
|
|
encoded = json.dumps(to_plain(value), sort_keys=True, separators=(",", ":")).encode()
|
|
return hashlib.sha256(encoded).hexdigest()[:length]
|
|
|
|
|
|
def compact_dict(mapping: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
key: to_plain(value)
|
|
for key, value in mapping.items()
|
|
if value not in (None, "", [], {}, ())
|
|
}
|
|
|
|
|
|
def to_plain(value: Any) -> Any:
|
|
if isinstance(value, Enum):
|
|
return value.value
|
|
if is_dataclass(value):
|
|
return to_plain(asdict(value))
|
|
if isinstance(value, dict):
|
|
return {
|
|
str(key): to_plain(item)
|
|
for key, item in value.items()
|
|
if item not in (None, "", [], {}, ())
|
|
}
|
|
if isinstance(value, (list, tuple, set)):
|
|
return [to_plain(item) for item in value]
|
|
return value
|
|
|
|
|
|
def parse_iso_datetime(value: str | None) -> datetime | None:
|
|
if not value:
|
|
return None
|
|
raw = value.strip()
|
|
if raw.endswith("Z"):
|
|
raw = raw[:-1] + "+00:00"
|
|
try:
|
|
parsed = datetime.fromisoformat(raw)
|
|
except ValueError:
|
|
return None
|
|
if parsed.tzinfo is None:
|
|
return parsed.replace(tzinfo=timezone.utc)
|
|
return parsed.astimezone(timezone.utc)
|