import contextvars from datetime import date, datetime, timedelta, timezone, tzinfo from zoneinfo import ZoneInfo # Costa Rica has no DST; a fixed offset is exact year-round. This is the # FALLBACK when the request doesn't declare a timezone (n8n posts, tests). CR_TZ = timezone(timedelta(hours=-6)) # Per-request client timezone, set by the agent middleware from the # X-Client-Timezone header (IANA name sent by the browser). Falls back to # Costa Rica so headless callers keep the owner's local semantics. _client_tz: contextvars.ContextVar[tzinfo] = contextvars.ContextVar( "client_tz", default=CR_TZ ) def set_client_timezone(name: str | None) -> contextvars.Token | None: """Bind the request's IANA timezone; ignores unknown/absent names. Proxies that see the header twice merge the values into a comma list ("Zone, Zone") — take the first entry. """ if not name: return None try: return _client_tz.set(ZoneInfo(name.split(",")[0].strip())) except (KeyError, ValueError): return None def reset_client_timezone(token: contextvars.Token | None) -> None: if token is not None: _client_tz.reset(token) def client_tz() -> tzinfo: return _client_tz.get() def today_client() -> date: """Current date where the USER is (request timezone, CR fallback). The server clock is UTC — at 6 pm in Costa Rica it is already "tomorrow" in UTC. Anything user-facing that means "today" (agent answers, date defaults) must come from here, never date.today(). """ return datetime.now(client_tz()).date() def today_cr() -> date: """Current date in Costa Rica (UTC-6) — timezone-independent fallback.""" return datetime.now(CR_TZ).date() def utcnow() -> datetime: """Naive UTC now, via the non-deprecated API. Every datetime column in the schema is TIMESTAMP WITHOUT TIME ZONE, so the whole app speaks naive-UTC. Replacing datetime.utcnow() call sites with this helper removes the Python 3.12 deprecation without changing any stored value or comparison. When the columns move to TIMESTAMPTZ (Phase 2), this becomes `datetime.now(timezone.utc)` and the type change happens in exactly one place. """ return datetime.now(timezone.utc).replace(tzinfo=None)