1"""This module identifies timezones.
2
3Normally, timezones have ids.
4This is a way to access the ids if you have a
5datetime.tzinfo object.
6"""
7
8from __future__ import annotations
9
10from collections import defaultdict
11from pathlib import Path
12from typing import TYPE_CHECKING, Optional
13
14from dateutil.tz import tz
15
16from icalendar.timezone import equivalent_timezone_ids_result
17
18if TYPE_CHECKING:
19 from datetime import datetime, tzinfo
20
21DATEUTIL_UTC = tz.gettz("UTC")
22DATEUTIL_UTC_PATH: Optional[str] = getattr(DATEUTIL_UTC, "_filename", None)
23DATEUTIL_ZONEINFO_PATH = (
24 None if DATEUTIL_UTC_PATH is None else Path(DATEUTIL_UTC_PATH).parent
25)
26
27
28def tzids_from_tzinfo(tzinfo: Optional[tzinfo]) -> tuple[str]:
29 """Get several timezone ids if we can identify the timezone.
30
31 >>> import zoneinfo
32 >>> from icalendar.timezone.tzid import tzids_from_tzinfo
33 >>> tzids_from_tzinfo(zoneinfo.ZoneInfo("Arctic/Longyearbyen"))
34 ('Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Berlin', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
35 >>> from dateutil.tz import gettz
36 >>> tzids_from_tzinfo(gettz("Europe/Berlin"))
37 ('Europe/Berlin', 'Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
38
39 """ # The example might need to change if you recreate the lookup tree # noqa: E501
40 if tzinfo is None:
41 return ()
42 if hasattr(tzinfo, "zone"):
43 return get_equivalent_tzids(tzinfo.zone) # pytz implementation
44 if hasattr(tzinfo, "key"):
45 return get_equivalent_tzids(tzinfo.key) # ZoneInfo implementation
46 if isinstance(tzinfo, tz._tzicalvtz): # noqa: SLF001
47 return get_equivalent_tzids(tzinfo._tzid) # noqa: SLF001
48 if isinstance(tzinfo, tz.tzstr):
49 return get_equivalent_tzids(tzinfo._s) # noqa: SLF001
50 if hasattr(tzinfo, "_filename"): # dateutil.tz.tzfile # noqa: SIM102
51 if DATEUTIL_ZONEINFO_PATH is not None:
52 # tzfile('/usr/share/zoneinfo/Europe/Berlin')
53 path = tzinfo._filename # noqa: SLF001
54 if path.startswith(str(DATEUTIL_ZONEINFO_PATH)):
55 tzid = str(Path(path).relative_to(DATEUTIL_ZONEINFO_PATH))
56 return get_equivalent_tzids(tzid)
57 return get_equivalent_tzids(path)
58 if isinstance(tzinfo, tz.tzutc):
59 return get_equivalent_tzids("UTC")
60 return ()
61
62
63def tzid_from_tzinfo(tzinfo: Optional[tzinfo]) -> Optional[str]:
64 """Retrieve the timezone id from the tzinfo object.
65
66 Some timezones are equivalent.
67 Thus, we might return one ID that is equivelant to others.
68 """
69 tzids = tzids_from_tzinfo(tzinfo)
70 if "UTC" in tzids:
71 return "UTC"
72 if not tzids:
73 return None
74 return tzids[0]
75
76
77def tzid_from_dt(dt: datetime) -> Optional[str]:
78 """Retrieve the timezone id from the datetime object."""
79 tzid = tzid_from_tzinfo(dt.tzinfo)
80 if tzid is None:
81 return dt.tzname()
82 return tzid
83
84
85_EQUIVALENT_IDS: dict[str, set[str]] = defaultdict(set)
86
87
88def _add_equivalent_ids(value: tuple | dict | set):
89 """This adds equivalent ids/
90
91 As soon as one timezone implementation used claims their equivalence,
92 they are considered equivalent.
93 Have a look at icalendar.timezone.equivalent_timezone_ids.
94 """
95 if isinstance(value, set):
96 for tzid in value:
97 _EQUIVALENT_IDS[tzid].update(value)
98 elif isinstance(value, tuple):
99 _add_equivalent_ids(value[1])
100 elif isinstance(value, dict):
101 for value2 in value.values():
102 _add_equivalent_ids(value2)
103 else:
104 raise TypeError(
105 f"Expected tuple, dict or set, not {value.__class__.__name__}: {value!r}"
106 )
107
108
109_add_equivalent_ids(equivalent_timezone_ids_result.lookup)
110
111
112def get_equivalent_tzids(tzid: str) -> tuple[str]:
113 """This returns the tzids which are equivalent to this one."""
114 ids = _EQUIVALENT_IDS.get(tzid, set())
115 return (tzid,) + tuple(sorted(ids - {tzid}))
116
117
118__all__ = ["tzid_from_dt", "tzid_from_tzinfo", "tzids_from_tzinfo"]