Fix various warnings and deprecations (#23641)

* Migrate to fastapi lifespan for startup events

The `on_event` style decorator is deprecated

https://fastapi.tiangolo.com/advanced/events/

* Migrate to pydantic TypeAdapter

https://pydantic.dev/docs/validation/2.12/get-started/migration/#introduction-of-typeadapter

* Replace pytz with native tz-aware objects and zoneinfo library

Supported from Python 3.9, see https://peps.python.org/pep-0615/. Also
https://blog.ganssle.io/articles/2019/11/utcnow.html.

Also provide tzdata as new dependency to supplement systems without local
tzdata information.

* Provide lookup for normalized timezone names

This provides a helper to look up the correct timezone name independent
of the casing of the given timezone name.

* Test timezone naming normalization and error handling
This commit is contained in:
Martin Weinelt
2026-10-11 17:49:33 -05:00
committed by GitHub
parent 186a730790
commit 135a95d8f2
16 changed files with 115 additions and 68 deletions
+21 -6
View File
@@ -3,17 +3,32 @@
import datetime
import logging
import math
from zoneinfo import ZoneInfoNotFoundError
from typing import Final
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones
import pytz
from tzlocal import get_localzone
logger = logging.getLogger(__name__)
TIMEZONE_CASEFOLD_INDEX: Final = {
timezone.casefold(): timezone for timezone in available_timezones()
}
def get_normalized_tz_name(tz_name: str) -> str:
"""Return the canonical name for a case-insensitive IANA timezone."""
tz = TIMEZONE_CASEFOLD_INDEX.get(tz_name.casefold())
if tz:
return tz
raise ValueError(f"Unknown timezone: {tz_name}")
def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]:
seconds_offset = (
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
datetime.datetime.now(ZoneInfo(get_normalized_tz_name(tz_name)))
.utcoffset()
.total_seconds()
)
hours_offset = int(seconds_offset / 60 / 60)
minutes_offset = int(seconds_offset / 60 - hours_offset * 60)
@@ -26,7 +41,7 @@ def get_tomorrow_at_time(hour: int) -> datetime.datetime:
"""Returns the datetime of the following day at 2am."""
try:
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
except ZoneInfoNotFoundError:
except (ValueError, ZoneInfoNotFoundError):
tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1)
logger.warning(
"Using utc for maintenance due to missing or incorrect timezone set"
@@ -84,8 +99,8 @@ def get_dst_transitions(
continuous periods with the same UTC offset
"""
try:
tz = pytz.timezone(tz_name)
except pytz.UnknownTimeZoneError:
tz = ZoneInfo(get_normalized_tz_name(tz_name))
except (ValueError, ZoneInfoNotFoundError):
# If timezone is invalid, return single period with no offset
return [(start_time, end_time, 0)]