Files
phase-memory/src/phase_memory/utils.py

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)