""" core/tz.py - Timezone-aware date/time helpers The frontend sends: X-Timezone-Name – IANA timezone (e.g. "America/Chicago"), preferred X-Timezone-Offset – minutes from UTC (JS getTimezoneOffset sign), fallback For background tasks (no request context) the scheduler reads the stored timezone_name / timezone_offset from user_preferences. """ from datetime import datetime, date, timezone, timedelta from zoneinfo import ZoneInfo import core.postgres as postgres # ── Request-context helpers (used by Flask route handlers) ──────────── def _get_request_tz(): """Return a tzinfo from the current Flask request headers. Prefers X-Timezone-Name (IANA), falls back to X-Timezone-Offset.""" try: import flask name = flask.request.headers.get("X-Timezone-Name") if name: try: return ZoneInfo(name) except (KeyError, Exception): pass offset = int(flask.request.headers.get("X-Timezone-Offset", 0)) return timezone(timedelta(minutes=-offset)) except (ValueError, TypeError, RuntimeError): return timezone.utc def _get_offset_minutes(): """Read the timezone offset header from the current Flask request. Returns 0 if outside a request context or header is absent.""" try: import flask return int(flask.request.headers.get("X-Timezone-Offset", 0)) except (ValueError, TypeError, RuntimeError): return 0 def _offset_to_tz(offset_minutes): """Convert JS-style offset (positive = behind UTC) to a timezone object.""" return timezone(timedelta(minutes=-offset_minutes)) def user_now(offset_minutes=None): """Current datetime in the user's timezone (request-context). If offset_minutes is provided, uses that directly. Otherwise reads request headers (prefers IANA name over offset).""" if offset_minutes is not None: tz = _offset_to_tz(offset_minutes) else: tz = _get_request_tz() return datetime.now(tz) def user_today(offset_minutes=None): """Current date in the user's timezone.""" return user_now(offset_minutes).date() # ── Stored-preference helpers (used by scheduler / background jobs) ─── def tz_for_user(user_uuid): """Return a tzinfo for *user_uuid* from stored preferences. Priority: timezone_name (IANA) > timezone_offset (minutes) > UTC.""" prefs = postgres.select_one("user_preferences", {"user_uuid": user_uuid}) if prefs: name = prefs.get("timezone_name") if name: try: return ZoneInfo(name) except (KeyError, Exception): pass offset = prefs.get("timezone_offset") if offset is not None: return timezone(timedelta(minutes=-offset)) return timezone.utc def user_now_for(user_uuid): """Current datetime in a user's timezone using their stored preferences.""" return datetime.now(tz_for_user(user_uuid)) def user_today_for(user_uuid): """Current date in a user's timezone using their stored preferences.""" return user_now_for(user_uuid).date()