1from __future__ import annotations
2
3from typing import TYPE_CHECKING, Optional, Union
4
5from icalendar.tools import to_datetime
6
7from .windows_to_olson import WINDOWS_TO_OLSON
8
9if TYPE_CHECKING:
10 import datetime
11
12 from dateutil.rrule import rrule
13
14 from icalendar import prop
15 from icalendar.cal import Timezone
16
17 from .provider import TZProvider
18
19DEFAULT_TIMEZONE_PROVIDER = "zoneinfo"
20
21
22class TZP:
23 """This is the timezone provider proxy.
24
25 If you would like to have another timezone implementation,
26 you can create a new one and pass it to this proxy.
27 All of icalendar will then use this timezone implementation.
28 """
29
30 def __init__(self, provider: Union[str, TZProvider] = DEFAULT_TIMEZONE_PROVIDER):
31 """Create a new timezone implementation proxy."""
32 self.use(provider)
33
34 def use_pytz(self) -> None:
35 """Use pytz as the timezone provider."""
36 from .pytz import PYTZ
37
38 self._use(PYTZ())
39
40 def use_zoneinfo(self) -> None:
41 """Use zoneinfo as the timezone provider."""
42 from .zoneinfo import ZONEINFO
43
44 self._use(ZONEINFO())
45
46 def _use(self, provider: TZProvider) -> None:
47 """Use a timezone implementation."""
48 self.__tz_cache = {}
49 self.__provider = provider
50
51 def use(self, provider: Union[str, TZProvider]):
52 """Switch to a different timezone provider."""
53 if isinstance(provider, str):
54 use_provider = getattr(self, f"use_{provider}", None)
55 if use_provider is None:
56 raise ValueError(
57 f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'."
58 )
59 use_provider()
60 else:
61 self._use(provider)
62
63 def use_default(self):
64 """Use the default timezone provider."""
65 self.use(DEFAULT_TIMEZONE_PROVIDER)
66
67 def localize_utc(self, dt: datetime.date) -> datetime.datetime:
68 """Return the datetime in UTC.
69
70 If the datetime has no timezone, set UTC as its timezone.
71 """
72 return self.__provider.localize_utc(to_datetime(dt))
73
74 def localize(
75 self, dt: datetime.date, tz: Union[datetime.tzinfo, str, None]
76 ) -> datetime.datetime:
77 """Localize a datetime to a timezone."""
78 if isinstance(tz, str):
79 tz = self.timezone(tz)
80 if tz is None:
81 return dt.replace(tzinfo=None)
82 return self.__provider.localize(to_datetime(dt), tz)
83
84 def cache_timezone_component(self, timezone_component: Timezone.Timezone) -> None:
85 """Cache the timezone that is created from a timezone component
86 if it is not already known.
87
88 This can influence the result from timezone(): Once cached, the
89 custom timezone is returned from timezone().
90 """
91 _unclean_id = timezone_component["TZID"]
92 _id = self.clean_timezone_id(_unclean_id)
93 if (
94 not self.__provider.knows_timezone_id(_id)
95 and not self.__provider.knows_timezone_id(_unclean_id)
96 and _id not in self.__tz_cache
97 ):
98 self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False)
99
100 def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
101 """Make sure the until value works."""
102 self.__provider.fix_rrule_until(rrule, ical_rrule)
103
104 def create_timezone(self, timezone_component: Timezone.Timezone) -> datetime.tzinfo:
105 """Create a timezone from a timezone component.
106
107 This component will not be cached.
108 """
109 return self.__provider.create_timezone(timezone_component)
110
111 def clean_timezone_id(self, tzid: str) -> str:
112 """Return a clean version of the timezone id.
113
114 Timezone ids can be a bit unclean, starting with a / for example.
115 Internally, we should use this to identify timezones.
116 """
117 return tzid.strip("/")
118
119 def timezone(self, tz_id: str) -> Optional[datetime.tzinfo]:
120 """Return a timezone with an id or None if we cannot find it."""
121 _unclean_id = tz_id
122 tz_id = self.clean_timezone_id(tz_id)
123 tz = self.__provider.timezone(tz_id)
124 if tz is not None:
125 return tz
126 if tz_id in WINDOWS_TO_OLSON:
127 tz = self.__provider.timezone(WINDOWS_TO_OLSON[tz_id])
128 return tz or self.__provider.timezone(_unclean_id) or self.__tz_cache.get(tz_id)
129
130 def uses_pytz(self) -> bool:
131 """Whether we use pytz at all."""
132 return self.__provider.uses_pytz()
133
134 def uses_zoneinfo(self) -> bool:
135 """Whether we use zoneinfo."""
136 return self.__provider.uses_zoneinfo()
137
138 @property
139 def name(self) -> str:
140 """The name of the timezone component used."""
141 return self.__provider.name
142
143 def __repr__(self) -> str:
144 return f"{self.__class__.__name__}({self.name!r})"
145
146
147__all__ = ["TZP"]