"""Время для БД и API: колонки DateTime в проекте — naive; договорённость — UTC.""" from __future__ import annotations from datetime import date, datetime, timezone from typing import Union from zoneinfo import ZoneInfo _DEFAULT_DISPLAY_TZ = "Europe/Moscow" def zoneinfo_or_utc(name: str) -> Union[ZoneInfo, timezone]: """IANA timezone; on Windows without tzdata falls back to UTC for «UTC».""" key = (name or "").strip() if not key or key.upper() in {"UTC", "ETC/UTC", "GMT"}: return timezone.utc try: return ZoneInfo(key) except Exception: if key.upper() in {"UTC", "ETC/UTC", "GMT"}: return timezone.utc raise def utc_now_naive() -> datetime: """Текущий момент в UTC без tzinfo (как DateTime в SQLite/SQLAlchemy здесь).""" return datetime.now(timezone.utc).replace(tzinfo=None) def utc_now_iso() -> str: """ISO 8601 с явным UTC (+00:00) для JSON и логов.""" return datetime.now(timezone.utc).isoformat() def naive_utc_to_iso(dt: datetime | None) -> str: """Сериализация naive UTC из БД в ISO с суффиксом +00:00.""" if dt is None: return utc_now_iso() if dt.tzinfo is not None: return dt.astimezone(timezone.utc).isoformat() return dt.replace(tzinfo=timezone.utc).isoformat() def display_timezone_name() -> str: """IANA-имя зоны для календаря/«сегодня» в UI (из конфига приложения).""" try: from flask import current_app, has_request_context if has_request_context(): name = current_app.config.get("WESP_DISPLAY_TIMEZONE") if name: return str(name) except Exception: pass return _DEFAULT_DISPLAY_TZ def display_calendar_today() -> date: """Сегодняшняя дата в зоне WESP_DISPLAY_TIMEZONE (для диапазонов графиков и т.п.).""" tz_name = display_timezone_name() try: tz = zoneinfo_or_utc(tz_name) return datetime.now(tz).date() except Exception: return datetime.now(timezone.utc).date()