dycw-utilities 0.151.12__py3-none-any.whl → 0.153.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
utilities/period.py DELETED
@@ -1,370 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import datetime as dt
4
- from dataclasses import dataclass
5
- from typing import (
6
- TYPE_CHECKING,
7
- Any,
8
- Self,
9
- TypedDict,
10
- TypeVar,
11
- assert_never,
12
- overload,
13
- override,
14
- )
15
- from zoneinfo import ZoneInfo
16
-
17
- from whenever import Date, DateDelta, PlainDateTime, Time, TimeDelta, ZonedDateTime
18
-
19
- from utilities.dataclasses import replace_non_sentinel
20
- from utilities.functions import get_class_name
21
- from utilities.sentinel import Sentinel, sentinel
22
- from utilities.whenever import format_compact
23
- from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
24
-
25
- if TYPE_CHECKING:
26
- from utilities.types import TimeZoneLike
27
-
28
- _TDate_co = TypeVar("_TDate_co", bound=Date | dt.date, covariant=True)
29
- _TTime_co = TypeVar("_TTime_co", bound=Time | dt.time, covariant=True)
30
- _TDateTime_co = TypeVar(
31
- "_TDateTime_co", bound=ZonedDateTime | dt.datetime, covariant=True
32
- )
33
-
34
-
35
- class PeriodDict[T: Date | Time | ZonedDateTime | dt.date | dt.time | dt.datetime](
36
- TypedDict
37
- ):
38
- start: T
39
- end: T
40
-
41
-
42
- @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
43
- class DatePeriod:
44
- """A period of dates."""
45
-
46
- start: Date
47
- end: Date
48
-
49
- def __post_init__(self) -> None:
50
- if self.start > self.end:
51
- raise _PeriodInvalidError(start=self.start, end=self.end)
52
-
53
- def __add__(self, other: DateDelta, /) -> Self:
54
- """Offset the period."""
55
- return self.replace(start=self.start + other, end=self.end + other)
56
-
57
- def __contains__(self, other: Date, /) -> bool:
58
- """Check if a date/datetime lies in the period."""
59
- return self.start <= other <= self.end
60
-
61
- @override
62
- def __repr__(self) -> str:
63
- cls = get_class_name(self)
64
- return f"{cls}({self.start}, {self.end})"
65
-
66
- def __sub__(self, other: DateDelta, /) -> Self:
67
- """Offset the period."""
68
- return self.replace(start=self.start - other, end=self.end - other)
69
-
70
- def at(
71
- self, obj: Time | tuple[Time, Time], /, *, time_zone: TimeZoneLike = UTC
72
- ) -> ZonedDateTimePeriod:
73
- """Combine a date with a time to create a datetime."""
74
- match obj:
75
- case Time() as time:
76
- start = end = time
77
- case Time() as start, Time() as end:
78
- ...
79
- case never:
80
- assert_never(never)
81
- tz = ensure_time_zone(time_zone).key
82
- return ZonedDateTimePeriod(
83
- self.start.at(start).assume_tz(tz), self.end.at(end).assume_tz(tz)
84
- )
85
-
86
- @property
87
- def delta(self) -> DateDelta:
88
- """The delta of the period."""
89
- return self.end - self.start
90
-
91
- def format_compact(self) -> str:
92
- """Format the period in a compact fashion."""
93
- fc, start, end = format_compact, self.start, self.end
94
- if self.start == self.end:
95
- return f"{fc(start)}="
96
- if self.start.year_month() == self.end.year_month():
97
- return f"{fc(start)}-{fc(end, fmt='%d')}"
98
- if self.start.year == self.end.year:
99
- return f"{fc(start)}-{fc(end, fmt='%m%d')}"
100
- return f"{fc(start)}-{fc(end)}"
101
-
102
- @classmethod
103
- def from_dict(cls, mapping: PeriodDict[_TDate_co], /) -> Self:
104
- """Convert the dictionary to a period."""
105
- match mapping["start"]:
106
- case Date() as start:
107
- ...
108
- case dt.date() as py_date:
109
- start = Date.from_py_date(py_date)
110
- case never:
111
- assert_never(never)
112
- match mapping["end"]:
113
- case Date() as end:
114
- ...
115
- case dt.date() as py_date:
116
- end = Date.from_py_date(py_date)
117
- case never:
118
- assert_never(never)
119
- return cls(start=start, end=end)
120
-
121
- def replace(
122
- self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
123
- ) -> Self:
124
- """Replace elements of the period."""
125
- return replace_non_sentinel(self, start=start, end=end)
126
-
127
- def to_dict(self) -> PeriodDict[Date]:
128
- """Convert the period to a dictionary."""
129
- return PeriodDict(start=self.start, end=self.end)
130
-
131
-
132
- @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
133
- class TimePeriod:
134
- """A period of times."""
135
-
136
- start: Time
137
- end: Time
138
-
139
- @override
140
- def __repr__(self) -> str:
141
- cls = get_class_name(self)
142
- return f"{cls}({self.start}, {self.end})"
143
-
144
- def at(
145
- self, obj: Date | tuple[Date, Date], /, *, time_zone: TimeZoneLike = UTC
146
- ) -> ZonedDateTimePeriod:
147
- """Combine a date with a time to create a datetime."""
148
- match obj:
149
- case Date() as date:
150
- start = end = date
151
- case Date() as start, Date() as end:
152
- ...
153
- case never:
154
- assert_never(never)
155
- return DatePeriod(start, end).at((self.start, self.end), time_zone=time_zone)
156
-
157
- @classmethod
158
- def from_dict(cls, mapping: PeriodDict[_TTime_co], /) -> Self:
159
- """Convert the dictionary to a period."""
160
- match mapping["start"]:
161
- case Time() as start:
162
- ...
163
- case dt.time() as py_time:
164
- start = Time.from_py_time(py_time)
165
- case never:
166
- assert_never(never)
167
- match mapping["end"]:
168
- case Time() as end:
169
- ...
170
- case dt.time() as py_time:
171
- end = Time.from_py_time(py_time)
172
- case never:
173
- assert_never(never)
174
- return cls(start=start, end=end)
175
-
176
- def replace(
177
- self, *, start: Time | Sentinel = sentinel, end: Time | Sentinel = sentinel
178
- ) -> Self:
179
- """Replace elements of the period."""
180
- return replace_non_sentinel(self, start=start, end=end)
181
-
182
- def to_dict(self) -> PeriodDict[Time]:
183
- """Convert the period to a dictionary."""
184
- return PeriodDict(start=self.start, end=self.end)
185
-
186
-
187
- @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
188
- class ZonedDateTimePeriod:
189
- """A period of time."""
190
-
191
- start: ZonedDateTime
192
- end: ZonedDateTime
193
-
194
- def __post_init__(self) -> None:
195
- if self.start > self.end:
196
- raise _PeriodInvalidError(start=self.start, end=self.end)
197
- if self.start.tz != self.end.tz:
198
- raise _PeriodTimeZoneError(
199
- start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
200
- )
201
-
202
- def __add__(self, other: TimeDelta, /) -> Self:
203
- """Offset the period."""
204
- return self.replace(start=self.start + other, end=self.end + other)
205
-
206
- def __contains__(self, other: ZonedDateTime, /) -> bool:
207
- """Check if a date/datetime lies in the period."""
208
- return self.start <= other <= self.end
209
-
210
- @override
211
- def __repr__(self) -> str:
212
- cls = get_class_name(self)
213
- return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
214
-
215
- def __sub__(self, other: TimeDelta, /) -> Self:
216
- """Offset the period."""
217
- return self.replace(start=self.start - other, end=self.end - other)
218
-
219
- @property
220
- def delta(self) -> TimeDelta:
221
- """The duration of the period."""
222
- return self.end - self.start
223
-
224
- @overload
225
- def exact_eq(self, period: ZonedDateTimePeriod, /) -> bool: ...
226
- @overload
227
- def exact_eq(self, start: ZonedDateTime, end: ZonedDateTime, /) -> bool: ...
228
- @overload
229
- def exact_eq(
230
- self, start: PlainDateTime, end: PlainDateTime, time_zone: ZoneInfo, /
231
- ) -> bool: ...
232
- def exact_eq(self, *args: Any) -> bool:
233
- """Check if a period is exactly equal to another."""
234
- if (len(args) == 1) and isinstance(args[0], ZonedDateTimePeriod):
235
- return self.start.exact_eq(args[0].start) and self.end.exact_eq(args[0].end)
236
- if (
237
- (len(args) == 2)
238
- and isinstance(args[0], ZonedDateTime)
239
- and isinstance(args[1], ZonedDateTime)
240
- ):
241
- return self.exact_eq(ZonedDateTimePeriod(args[0], args[1]))
242
- if (
243
- (len(args) == 3)
244
- and isinstance(args[0], PlainDateTime)
245
- and isinstance(args[1], PlainDateTime)
246
- and isinstance(args[2], ZoneInfo)
247
- ):
248
- return self.exact_eq(
249
- ZonedDateTimePeriod(
250
- args[0].assume_tz(args[2].key), args[1].assume_tz(args[2].key)
251
- )
252
- )
253
- raise _PeriodExactEqArgumentsError(args=args)
254
-
255
- def format_compact(self) -> str:
256
- """Format the period in a compact fashion."""
257
- fc, start, end = format_compact, self.start, self.end
258
- if start == end:
259
- if end.second != 0:
260
- return f"{fc(start)}="
261
- if end.minute != 0:
262
- return f"{fc(start, fmt='%Y%m%dT%H%M')}="
263
- return f"{fc(start, fmt='%Y%m%dT%H')}="
264
- if start.date() == end.date():
265
- if end.second != 0:
266
- return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M%S')}"
267
- if end.minute != 0:
268
- return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M')}"
269
- return f"{fc(start.to_plain())}-{fc(end, fmt='%H')}"
270
- if start.date().year_month() == end.date().year_month():
271
- if end.second != 0:
272
- return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M%S')}"
273
- if end.minute != 0:
274
- return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M')}"
275
- return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H')}"
276
- if start.year == end.year:
277
- if end.second != 0:
278
- return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M%S')}"
279
- if end.minute != 0:
280
- return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M')}"
281
- return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H')}"
282
- if end.second != 0:
283
- return f"{fc(start.to_plain())}-{fc(end)}"
284
- if end.minute != 0:
285
- return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H%M')}"
286
- return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H')}"
287
-
288
- @classmethod
289
- def from_dict(cls, mapping: PeriodDict[_TDateTime_co], /) -> Self:
290
- """Convert the dictionary to a period."""
291
- match mapping["start"]:
292
- case ZonedDateTime() as start:
293
- ...
294
- case dt.date() as py_datetime:
295
- start = ZonedDateTime.from_py_datetime(py_datetime)
296
- case never:
297
- assert_never(never)
298
- match mapping["end"]:
299
- case ZonedDateTime() as end:
300
- ...
301
- case dt.date() as py_datetime:
302
- end = ZonedDateTime.from_py_datetime(py_datetime)
303
- case never:
304
- assert_never(never)
305
- return cls(start=start, end=end)
306
-
307
- def replace(
308
- self,
309
- *,
310
- start: ZonedDateTime | Sentinel = sentinel,
311
- end: ZonedDateTime | Sentinel = sentinel,
312
- ) -> Self:
313
- """Replace elements of the period."""
314
- return replace_non_sentinel(self, start=start, end=end)
315
-
316
- @property
317
- def time_zone(self) -> ZoneInfo:
318
- """The time zone of the period."""
319
- return ZoneInfo(self.start.tz)
320
-
321
- def to_dict(self) -> PeriodDict[ZonedDateTime]:
322
- """Convert the period to a dictionary."""
323
- return PeriodDict(start=self.start, end=self.end)
324
-
325
- def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
326
- """Convert the time zone."""
327
- tz = get_time_zone_name(time_zone)
328
- return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
329
-
330
-
331
- @dataclass(kw_only=True, slots=True)
332
- class PeriodError(Exception): ...
333
-
334
-
335
- @dataclass(kw_only=True, slots=True)
336
- class _PeriodInvalidError[T: Date | ZonedDateTime](PeriodError):
337
- start: T
338
- end: T
339
-
340
- @override
341
- def __str__(self) -> str:
342
- return f"Invalid period; got {self.start} > {self.end}"
343
-
344
-
345
- @dataclass(kw_only=True, slots=True)
346
- class _PeriodTimeZoneError(PeriodError):
347
- start: ZoneInfo
348
- end: ZoneInfo
349
-
350
- @override
351
- def __str__(self) -> str:
352
- return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
353
-
354
-
355
- @dataclass(kw_only=True, slots=True)
356
- class _PeriodExactEqArgumentsError(PeriodError):
357
- args: tuple[Any, ...]
358
-
359
- @override
360
- def __str__(self) -> str:
361
- return f"Invalid arguments; got {self.args}"
362
-
363
-
364
- __all__ = [
365
- "DatePeriod",
366
- "PeriodDict",
367
- "PeriodError",
368
- "TimePeriod",
369
- "ZonedDateTimePeriod",
370
- ]