"""NormalizedDateTime module.""" from datetime import date, datetime from typing import Any from sqlalchemy.engine import Dialect from sqlalchemy.types import DateTime as SADateTime, TypeDecorator from abacus_common_logic.constants.constants import SYSTEM_TIMEZONE _FORMATS = [ '%Y/%m/%d', '%m/%d/%Y', '%d-%m-%Y', '%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', ] class NormalizedDateTime(TypeDecorator): """Normalize inputs to a datetime.""" impl = SADateTime(timezone=True) cache_ok = True def process_bind_param(self, value: Any, dialect: Dialect) -> datetime | None: """Process params.""" return to_datetime(value) def process_result_value(self, value: Any, dialect: Dialect) -> datetime | None: """Process results.""" return to_datetime(value) def to_datetime(value: Any) -> datetime | None: """Normalize inputs to a datetime.""" # None / '' -> None if value is None or value == '': return None # datetime if isinstance(value, datetime): return _to_naive_datetime(value) # date -> datetime at midnight if isinstance(value, date): return datetime(value.year, value.month, value.day) # Epoch seconds if isinstance(value, (int, float)): return datetime.fromtimestamp(value) # bytes -> decode (utf-8) if isinstance(value, (bytes, bytearray)): value = value.decode('utf-8', errors='strict') # Convert to string s = str(value).strip() if not s: return None # Handle trailing 'Z' (UTC) if s.endswith(('Z', 'z')): s = s[:-1] + '+00:00' # ISO 8601 try: return _to_naive_datetime(datetime.fromisoformat(s)) except ValueError: pass # ISO date-only ('YYYY-MM-DD') try: d = date.fromisoformat(s) return datetime(d.year, d.month, d.day) except ValueError: pass # Try fallback formats for fmt in _FORMATS: try: return _to_naive_datetime(datetime.strptime(s, fmt)) except ValueError: continue raise TypeError(f'Unsupported datetime value: {value!r}') def _to_naive_datetime(dt: datetime) -> datetime: if dt.tzinfo is None: return dt return dt.astimezone(SYSTEM_TIMEZONE).replace(tzinfo=None)