Files
remnawave-bedolaga-telegram…/app/utils/timezone.py
T
Fringg eb18994b7d fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
2026-02-17 04:45:40 +03:00

67 lines
1.9 KiB
Python

"""Timezone utilities for consistent local time handling."""
from __future__ import annotations
from datetime import UTC, datetime
from functools import lru_cache
from zoneinfo import ZoneInfo
import structlog
from app.config import settings
logger = structlog.get_logger(__name__)
@lru_cache(maxsize=1)
def get_local_timezone() -> ZoneInfo:
"""Return the configured local timezone.
Falls back to UTC if the configured timezone cannot be loaded. The
fallback is logged once and cached for subsequent calls.
"""
tz_name = settings.TIMEZONE
try:
return ZoneInfo(tz_name)
except Exception as exc: # pragma: no cover - defensive branch
logger.warning("⚠️ Не удалось загрузить временную зону '': . Используем UTC.", tz_name=tz_name, exc=exc)
return ZoneInfo('UTC')
def panel_datetime_to_utc(dt: datetime) -> datetime:
"""Convert a panel datetime to aware UTC.
Panel API returns local time with a misleading UTC offset (+00:00 / Z).
This strips the offset, interprets the raw value as panel-local time,
then converts to aware UTC for database storage.
"""
naive = dt
localized = naive.replace(tzinfo=get_local_timezone())
return localized.astimezone(ZoneInfo('UTC'))
def to_local_datetime(dt: datetime | None) -> datetime | None:
"""Convert a datetime value to the configured local timezone."""
if dt is None:
return None
aware_dt = dt if dt.tzinfo is not None else dt.replace(tzinfo=UTC)
return aware_dt.astimezone(get_local_timezone())
def format_local_datetime(
dt: datetime | None,
fmt: str = '%Y-%m-%d %H:%M:%S %Z',
na_placeholder: str = 'N/A',
) -> str:
"""Format a datetime value in the configured local timezone."""
localized = to_local_datetime(dt)
if localized is None:
return na_placeholder
return localized.strftime(fmt)