mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Make iot time timezone aware (#1147)
Also makes on_since for iot devices use device time. Changes the return value for device.timezone to be tzinfo instead of a dict.
This commit is contained in:
parent
8bb2cca7cf
commit
9641edcbc0
@ -40,7 +40,7 @@ async def state(ctx, dev: Device):
|
||||
echo(f"Port: {dev.port}")
|
||||
echo(f"Device state: {dev.is_on}")
|
||||
|
||||
echo(f"Time: {dev.time} (tz: {dev.timezone}")
|
||||
echo(f"Time: {dev.time} (tz: {dev.timezone})")
|
||||
echo(f"Hardware: {dev.hw_info['hw_ver']}")
|
||||
echo(f"Software: {dev.hw_info['sw_ver']}")
|
||||
echo(f"MAC (rssi): {dev.mac} ({dev.rssi})")
|
||||
|
@ -109,7 +109,7 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, tzinfo
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from warnings import warn
|
||||
|
||||
@ -377,7 +377,7 @@ class Device(ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def timezone(self) -> dict:
|
||||
def timezone(self) -> tzinfo:
|
||||
"""Return the timezone and time_difference."""
|
||||
|
||||
@property
|
||||
|
@ -18,7 +18,7 @@ import functools
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from ..device import Device, WifiNetwork
|
||||
@ -299,7 +299,7 @@ class IotDevice(Device):
|
||||
|
||||
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
|
||||
for module in self._modules.values():
|
||||
module._post_update_hook()
|
||||
await module._post_update_hook()
|
||||
|
||||
if not self._features:
|
||||
await self._initialize_features()
|
||||
@ -464,7 +464,7 @@ class IotDevice(Device):
|
||||
|
||||
@property
|
||||
@requires_update
|
||||
def timezone(self) -> dict:
|
||||
def timezone(self) -> tzinfo:
|
||||
"""Return the current timezone."""
|
||||
return self.modules[Module.IotTime].timezone
|
||||
|
||||
@ -606,9 +606,7 @@ class IotDevice(Device):
|
||||
|
||||
on_time = self._sys_info["on_time"]
|
||||
|
||||
time = datetime.now(timezone.utc).astimezone().replace(microsecond=0)
|
||||
|
||||
on_since = time - timedelta(seconds=on_time)
|
||||
on_since = self.time - timedelta(seconds=on_time)
|
||||
if not self._on_since or timedelta(
|
||||
seconds=0
|
||||
) < on_since - self._on_since > timedelta(seconds=5):
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from ..device_type import DeviceType
|
||||
@ -373,7 +373,7 @@ class IotStripPlug(IotPlug):
|
||||
"""
|
||||
await self._modular_update({})
|
||||
for module in self._modules.values():
|
||||
module._post_update_hook()
|
||||
await module._post_update_hook()
|
||||
|
||||
if not self._features:
|
||||
await self._initialize_features()
|
||||
@ -445,7 +445,7 @@ class IotStripPlug(IotPlug):
|
||||
info = self._get_child_info()
|
||||
on_time = info["on_time"]
|
||||
|
||||
time = datetime.now(timezone.utc).astimezone().replace(microsecond=0)
|
||||
time = self._parent.time
|
||||
|
||||
on_since = time - timedelta(seconds=on_time)
|
||||
if not self._on_since or timedelta(
|
||||
|
178
kasa/iot/iottimezone.py
Normal file
178
kasa/iot/iottimezone.py
Normal file
@ -0,0 +1,178 @@
|
||||
"""Module for io device timezone lookups."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, tzinfo
|
||||
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_timezone(index: int) -> tzinfo:
|
||||
"""Get the timezone from the index."""
|
||||
if index > 109:
|
||||
_LOGGER.error(
|
||||
"Unexpected index %s not configured as a timezone, defaulting to UTC", index
|
||||
)
|
||||
return await _CachedZoneInfo.get_cached_zone_info("Etc/UTC")
|
||||
|
||||
name = TIMEZONE_INDEX[index]
|
||||
return await _CachedZoneInfo.get_cached_zone_info(name)
|
||||
|
||||
|
||||
async def get_timezone_index(name: str) -> int:
|
||||
"""Return the iot firmware index for a valid IANA timezone key."""
|
||||
rev = {val: key for key, val in TIMEZONE_INDEX.items()}
|
||||
if name in rev:
|
||||
return rev[name]
|
||||
|
||||
# Try to find a supported timezone matching dst true/false
|
||||
zone = await _CachedZoneInfo.get_cached_zone_info(name)
|
||||
now = datetime.now()
|
||||
winter = datetime(now.year, 1, 1, 12)
|
||||
summer = datetime(now.year, 7, 1, 12)
|
||||
for i in range(110):
|
||||
configured_zone = await get_timezone(i)
|
||||
if zone.utcoffset(winter) == configured_zone.utcoffset(
|
||||
winter
|
||||
) and zone.utcoffset(summer) == configured_zone.utcoffset(summer):
|
||||
return i
|
||||
raise ValueError("Device does not support timezone %s", name)
|
||||
|
||||
|
||||
class _CachedZoneInfo(ZoneInfo):
|
||||
"""Cache zone info objects."""
|
||||
|
||||
_cache: dict[str, ZoneInfo] = {}
|
||||
|
||||
@classmethod
|
||||
async def get_cached_zone_info(cls, time_zone_str: str) -> ZoneInfo:
|
||||
"""Get a cached zone info object."""
|
||||
if cached := cls._cache.get(time_zone_str):
|
||||
return cached
|
||||
loop = asyncio.get_running_loop()
|
||||
zinfo = await loop.run_in_executor(None, _get_zone_info, time_zone_str)
|
||||
cls._cache[time_zone_str] = zinfo
|
||||
return zinfo
|
||||
|
||||
|
||||
def _get_zone_info(time_zone_str: str) -> ZoneInfo:
|
||||
"""Get a time zone object for the given time zone string."""
|
||||
return ZoneInfo(time_zone_str)
|
||||
|
||||
|
||||
TIMEZONE_INDEX = {
|
||||
0: "Etc/GMT+12",
|
||||
1: "Pacific/Samoa",
|
||||
2: "US/Hawaii",
|
||||
3: "US/Alaska",
|
||||
4: "Mexico/BajaNorte",
|
||||
5: "Etc/GMT+8",
|
||||
6: "PST8PDT",
|
||||
7: "US/Arizona",
|
||||
8: "America/Mazatlan",
|
||||
9: "MST",
|
||||
10: "MST7MDT",
|
||||
11: "Mexico/General",
|
||||
12: "Etc/GMT+6",
|
||||
13: "CST6CDT",
|
||||
14: "America/Monterrey",
|
||||
15: "Canada/Saskatchewan",
|
||||
16: "America/Bogota",
|
||||
17: "Etc/GMT+5",
|
||||
18: "EST",
|
||||
19: "America/Indiana/Indianapolis",
|
||||
20: "America/Caracas",
|
||||
21: "America/Asuncion",
|
||||
22: "Etc/GMT+4",
|
||||
23: "Canada/Atlantic",
|
||||
24: "America/Cuiaba",
|
||||
25: "Brazil/West",
|
||||
26: "America/Santiago",
|
||||
27: "Canada/Newfoundland",
|
||||
28: "America/Sao_Paulo",
|
||||
29: "America/Argentina/Buenos_Aires",
|
||||
30: "America/Cayenne",
|
||||
31: "America/Miquelon",
|
||||
32: "America/Montevideo",
|
||||
33: "Chile/Continental",
|
||||
34: "Etc/GMT+2",
|
||||
35: "Atlantic/Azores",
|
||||
36: "Atlantic/Cape_Verde",
|
||||
37: "Africa/Casablanca",
|
||||
38: "UCT",
|
||||
39: "GB",
|
||||
40: "Africa/Monrovia",
|
||||
41: "Europe/Amsterdam",
|
||||
42: "Europe/Belgrade",
|
||||
43: "Europe/Brussels",
|
||||
44: "Europe/Sarajevo",
|
||||
45: "Africa/Lagos",
|
||||
46: "Africa/Windhoek",
|
||||
47: "Asia/Amman",
|
||||
48: "Europe/Athens",
|
||||
49: "Asia/Beirut",
|
||||
50: "Africa/Cairo",
|
||||
51: "Asia/Damascus",
|
||||
52: "EET",
|
||||
53: "Africa/Harare",
|
||||
54: "Europe/Helsinki",
|
||||
55: "Asia/Istanbul",
|
||||
56: "Asia/Jerusalem",
|
||||
57: "Europe/Kaliningrad",
|
||||
58: "Africa/Tripoli",
|
||||
59: "Asia/Baghdad",
|
||||
60: "Asia/Kuwait",
|
||||
61: "Europe/Minsk",
|
||||
62: "Europe/Moscow",
|
||||
63: "Africa/Nairobi",
|
||||
64: "Asia/Tehran",
|
||||
65: "Asia/Muscat",
|
||||
66: "Asia/Baku",
|
||||
67: "Europe/Samara",
|
||||
68: "Indian/Mauritius",
|
||||
69: "Asia/Tbilisi",
|
||||
70: "Asia/Yerevan",
|
||||
71: "Asia/Kabul",
|
||||
72: "Asia/Ashgabat",
|
||||
73: "Asia/Yekaterinburg",
|
||||
74: "Asia/Karachi",
|
||||
75: "Asia/Kolkata",
|
||||
76: "Asia/Colombo",
|
||||
77: "Asia/Kathmandu",
|
||||
78: "Asia/Almaty",
|
||||
79: "Asia/Dhaka",
|
||||
80: "Asia/Novosibirsk",
|
||||
81: "Asia/Rangoon",
|
||||
82: "Asia/Bangkok",
|
||||
83: "Asia/Krasnoyarsk",
|
||||
84: "Asia/Chongqing",
|
||||
85: "Asia/Irkutsk",
|
||||
86: "Asia/Singapore",
|
||||
87: "Australia/Perth",
|
||||
88: "Asia/Taipei",
|
||||
89: "Asia/Ulaanbaatar",
|
||||
90: "Asia/Tokyo",
|
||||
91: "Asia/Seoul",
|
||||
92: "Asia/Yakutsk",
|
||||
93: "Australia/Adelaide",
|
||||
94: "Australia/Darwin",
|
||||
95: "Australia/Brisbane",
|
||||
96: "Australia/Canberra",
|
||||
97: "Pacific/Guam",
|
||||
98: "Australia/Hobart",
|
||||
99: "Antarctica/DumontDUrville",
|
||||
100: "Asia/Magadan",
|
||||
101: "Asia/Srednekolymsk",
|
||||
102: "Etc/GMT-11",
|
||||
103: "Asia/Anadyr",
|
||||
104: "Pacific/Auckland",
|
||||
105: "Etc/GMT-12",
|
||||
106: "Pacific/Fiji",
|
||||
107: "Etc/GMT-13",
|
||||
108: "Pacific/Apia",
|
||||
109: "Etc/GMT-14",
|
||||
}
|
@ -12,7 +12,7 @@ from .usage import Usage
|
||||
class Emeter(Usage, EnergyInterface):
|
||||
"""Emeter module."""
|
||||
|
||||
def _post_update_hook(self) -> None:
|
||||
async def _post_update_hook(self) -> None:
|
||||
self._supported = EnergyInterface.ModuleFeature.PERIODIC_STATS
|
||||
if (
|
||||
"voltage_mv" in self.data["get_realtime"]
|
||||
|
@ -239,7 +239,7 @@ class Light(IotModule, LightInterface):
|
||||
"""Return the current light state."""
|
||||
return self._light_state
|
||||
|
||||
def _post_update_hook(self) -> None:
|
||||
async def _post_update_hook(self) -> None:
|
||||
if self._device.is_on is False:
|
||||
state = LightState(light_on=False)
|
||||
else:
|
||||
|
@ -41,7 +41,7 @@ class LightPreset(IotModule, LightPresetInterface):
|
||||
_presets: dict[str, IotLightPreset]
|
||||
_preset_list: list[str]
|
||||
|
||||
def _post_update_hook(self):
|
||||
async def _post_update_hook(self):
|
||||
"""Update the internal presets."""
|
||||
self._presets = {
|
||||
f"Light preset {index+1}": IotLightPreset(**vals)
|
||||
|
@ -1,14 +1,19 @@
|
||||
"""Provides the current time and timezone information."""
|
||||
|
||||
from datetime import datetime
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone, tzinfo
|
||||
|
||||
from ...exceptions import KasaException
|
||||
from ..iotmodule import IotModule, merge
|
||||
from ..iottimezone import get_timezone
|
||||
|
||||
|
||||
class Time(IotModule):
|
||||
"""Implements the timezone settings."""
|
||||
|
||||
_timezone: tzinfo = timezone.utc
|
||||
|
||||
def query(self):
|
||||
"""Request time and timezone."""
|
||||
q = self.query_for_command("get_time")
|
||||
@ -16,11 +21,16 @@ class Time(IotModule):
|
||||
merge(q, self.query_for_command("get_timezone"))
|
||||
return q
|
||||
|
||||
async def _post_update_hook(self):
|
||||
"""Perform actions after a device update."""
|
||||
if res := self.data.get("get_timezone"):
|
||||
self._timezone = await get_timezone(res.get("index"))
|
||||
|
||||
@property
|
||||
def time(self) -> datetime:
|
||||
"""Return current device time."""
|
||||
res = self.data["get_time"]
|
||||
return datetime(
|
||||
time = datetime(
|
||||
res["year"],
|
||||
res["month"],
|
||||
res["mday"],
|
||||
@ -28,12 +38,12 @@ class Time(IotModule):
|
||||
res["min"],
|
||||
res["sec"],
|
||||
)
|
||||
return time.astimezone(self.timezone)
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
def timezone(self) -> tzinfo:
|
||||
"""Return current timezone."""
|
||||
res = self.data["get_timezone"]
|
||||
return res
|
||||
return self._timezone
|
||||
|
||||
async def get_time(self):
|
||||
"""Return current device time."""
|
||||
|
@ -155,7 +155,7 @@ class Module(ABC):
|
||||
children's modules.
|
||||
"""
|
||||
|
||||
def _post_update_hook(self): # noqa: B027
|
||||
async def _post_update_hook(self): # noqa: B027
|
||||
"""Perform actions after a device update.
|
||||
|
||||
This can be implemented if a module needs to perform actions each time
|
||||
|
@ -10,7 +10,7 @@ class DeviceModule(SmartModule):
|
||||
|
||||
REQUIRED_COMPONENT = "device"
|
||||
|
||||
def _post_update_hook(self):
|
||||
async def _post_update_hook(self):
|
||||
"""Perform actions after a device update.
|
||||
|
||||
Overrides the default behaviour to disable a module if the query returns
|
||||
|
@ -152,7 +152,7 @@ class Light(SmartModule, LightInterface):
|
||||
"""Return the current light state."""
|
||||
return self._light_state
|
||||
|
||||
def _post_update_hook(self) -> None:
|
||||
async def _post_update_hook(self) -> None:
|
||||
if self._device.is_on is False:
|
||||
state = LightState(light_on=False)
|
||||
else:
|
||||
|
@ -28,7 +28,7 @@ class LightEffect(SmartModule, SmartLightEffect):
|
||||
_effect_list: list[str]
|
||||
_scenes_names_to_id: dict[str, str]
|
||||
|
||||
def _post_update_hook(self) -> None:
|
||||
async def _post_update_hook(self) -> None:
|
||||
"""Update internal effect state."""
|
||||
# Copy the effects so scene name updates do not update the underlying dict.
|
||||
effects = copy.deepcopy(
|
||||
|
@ -34,7 +34,7 @@ class LightPreset(SmartModule, LightPresetInterface):
|
||||
self._state_in_sysinfo = self.SYS_INFO_STATE_KEY in device.sys_info
|
||||
self._brightness_only: bool = False
|
||||
|
||||
def _post_update_hook(self):
|
||||
async def _post_update_hook(self):
|
||||
"""Update the internal presets."""
|
||||
index = 0
|
||||
self._presets = {}
|
||||
|
@ -90,7 +90,7 @@ class LightTransition(SmartModule):
|
||||
)
|
||||
)
|
||||
|
||||
def _post_update_hook(self) -> None:
|
||||
async def _post_update_hook(self) -> None:
|
||||
"""Update the states."""
|
||||
# Assumes any device with state in sysinfo supports on and off and
|
||||
# has maximum values for both.
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
from time import mktime
|
||||
from typing import cast
|
||||
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
@ -31,18 +33,27 @@ class Time(SmartModule):
|
||||
)
|
||||
|
||||
@property
|
||||
def time(self) -> datetime:
|
||||
"""Return device's current datetime."""
|
||||
def timezone(self) -> tzinfo:
|
||||
"""Return current timezone."""
|
||||
td = timedelta(minutes=cast(float, self.data.get("time_diff")))
|
||||
if self.data.get("region"):
|
||||
tz = timezone(td, str(self.data.get("region")))
|
||||
if region := self.data.get("region"):
|
||||
try:
|
||||
# Zoneinfo will return a DST aware object
|
||||
tz: tzinfo = ZoneInfo(region)
|
||||
except ZoneInfoNotFoundError:
|
||||
tz = timezone(td, region)
|
||||
else:
|
||||
# in case the device returns a blank region this will result in the
|
||||
# tzname being a UTC offset
|
||||
tz = timezone(td)
|
||||
return tz
|
||||
|
||||
@property
|
||||
def time(self) -> datetime:
|
||||
"""Return device's current datetime."""
|
||||
return datetime.fromtimestamp(
|
||||
cast(float, self.data.get("timestamp")),
|
||||
tz=tz,
|
||||
tz=self.timezone,
|
||||
)
|
||||
|
||||
async def set_time(self, dt: datetime):
|
||||
|
@ -61,7 +61,7 @@ class SmartChildDevice(SmartDevice):
|
||||
self._last_update = await self.protocol.query(req)
|
||||
|
||||
for module in self.modules.values():
|
||||
self._handle_module_post_update(
|
||||
await self._handle_module_post_update(
|
||||
module, now, had_query=module in module_queries
|
||||
)
|
||||
self._last_update_time = now
|
||||
|
@ -6,8 +6,8 @@ import base64
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, cast
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from ..aestransport import AesTransport
|
||||
from ..device import Device, WifiNetwork
|
||||
@ -168,7 +168,7 @@ class SmartDevice(Device):
|
||||
await self._initialize_modules()
|
||||
# Run post update for the cloud module
|
||||
if cloud_mod := self.modules.get(Module.Cloud):
|
||||
self._handle_module_post_update(cloud_mod, now, had_query=True)
|
||||
await self._handle_module_post_update(cloud_mod, now, had_query=True)
|
||||
|
||||
resp = await self._modular_update(first_update, now)
|
||||
|
||||
@ -195,7 +195,7 @@ class SmartDevice(Device):
|
||||
updated = self._last_update if first_update else resp
|
||||
_LOGGER.debug("Update completed %s: %s", self.host, list(updated.keys()))
|
||||
|
||||
def _handle_module_post_update(
|
||||
async def _handle_module_post_update(
|
||||
self, module: SmartModule, update_time: float, had_query: bool
|
||||
):
|
||||
if module.disabled:
|
||||
@ -203,7 +203,7 @@ class SmartDevice(Device):
|
||||
if had_query:
|
||||
module._last_update_time = update_time
|
||||
try:
|
||||
module._post_update_hook()
|
||||
await module._post_update_hook()
|
||||
module._set_error(None)
|
||||
except Exception as ex:
|
||||
# Only set the error if a query happened.
|
||||
@ -260,7 +260,7 @@ class SmartDevice(Device):
|
||||
|
||||
# Call handle update for modules that want to update internal data
|
||||
for module in self._modules.values():
|
||||
self._handle_module_post_update(
|
||||
await self._handle_module_post_update(
|
||||
module, update_time, had_query=module in module_queries
|
||||
)
|
||||
|
||||
@ -516,10 +516,11 @@ class SmartDevice(Device):
|
||||
return self._on_since
|
||||
|
||||
@property
|
||||
def timezone(self) -> dict:
|
||||
def timezone(self) -> tzinfo:
|
||||
"""Return the timezone and time_difference."""
|
||||
ti = self.time
|
||||
return {"timezone": ti.tzname()}
|
||||
if TYPE_CHECKING:
|
||||
assert self.time.tzinfo
|
||||
return self.time.tzinfo
|
||||
|
||||
@property
|
||||
def hw_info(self) -> dict:
|
||||
|
@ -121,7 +121,7 @@ class SmartModule(Module):
|
||||
"""Name of the module."""
|
||||
return getattr(self, "NAME", self.__class__.__name__)
|
||||
|
||||
def _post_update_hook(self): # noqa: B027
|
||||
async def _post_update_hook(self): # noqa: B027
|
||||
"""Perform actions after a device update.
|
||||
|
||||
Any modules overriding this should ensure that self.data is
|
||||
|
@ -10,10 +10,16 @@ from contextlib import AbstractContextManager
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
import zoneinfo
|
||||
|
||||
import kasa
|
||||
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
|
||||
from kasa.iot import IotDevice
|
||||
from kasa.iot.iottimezone import (
|
||||
TIMEZONE_INDEX,
|
||||
get_timezone,
|
||||
get_timezone_index,
|
||||
)
|
||||
from kasa.iot.modules import IotLightPreset
|
||||
from kasa.smart import SmartChildDevice, SmartDevice
|
||||
|
||||
@ -299,3 +305,29 @@ async def test_device_type_aliases():
|
||||
)
|
||||
assert isinstance(dev.config, DeviceConfig)
|
||||
assert DeviceType.Dimmer == Device.Type.Dimmer
|
||||
|
||||
|
||||
async def test_device_timezones():
|
||||
"""Test the timezone data is good."""
|
||||
# Check all indexes return a zoneinfo
|
||||
for i in range(110):
|
||||
tz = await get_timezone(i)
|
||||
assert tz
|
||||
assert tz != zoneinfo.ZoneInfo("Etc/UTC"), f"{i} is default Etc/UTC"
|
||||
|
||||
# Check an unexpected index returns a UTC default.
|
||||
tz = await get_timezone(110)
|
||||
assert tz == zoneinfo.ZoneInfo("Etc/UTC")
|
||||
|
||||
# Get an index from a timezone
|
||||
for index, zone in TIMEZONE_INDEX.items():
|
||||
found_index = await get_timezone_index(zone)
|
||||
assert found_index == index
|
||||
|
||||
# Try a timezone not hardcoded finds another match
|
||||
index = await get_timezone_index("Asia/Katmandu")
|
||||
assert index == 77
|
||||
|
||||
# Try a timezone not hardcoded no match
|
||||
with pytest.raises(zoneinfo.ZoneInfoNotFoundError):
|
||||
await get_timezone_index("Foo/bar")
|
||||
|
@ -13,6 +13,7 @@ dependencies = [
|
||||
"async-timeout>=3.0.0",
|
||||
"aiohttp>=3",
|
||||
"typing-extensions>=4.12.2,<5.0",
|
||||
"tzdata>=2024.2 ; platform_system == 'Windows'",
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
|
17
uv.lock
17
uv.lock
@ -1,8 +1,10 @@
|
||||
version = 1
|
||||
requires-python = ">=3.9, <4.0"
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.13'",
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version < '3.13' and platform_system == 'Windows'",
|
||||
"python_full_version < '3.13' and platform_system != 'Windows'",
|
||||
"python_full_version >= '3.13' and platform_system == 'Windows'",
|
||||
"python_full_version >= '3.13' and platform_system != 'Windows'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1351,6 +1353,7 @@ dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "tzdata", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@ -1407,6 +1410,7 @@ requires-dist = [
|
||||
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = "~=2.0" },
|
||||
{ name = "sphinxcontrib-programoutput", marker = "extra == 'docs'", specifier = "~=0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.12.2,<5.0" },
|
||||
{ name = "tzdata", marker = "platform_system == 'Windows'", specifier = ">=2024.2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@ -1693,6 +1697,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2024.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
|
Loading…
Reference in New Issue
Block a user