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