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.
- {dycw_utilities-0.151.12.dist-info → dycw_utilities-0.153.0.dist-info}/METADATA +2 -2
- {dycw_utilities-0.151.12.dist-info → dycw_utilities-0.153.0.dist-info}/RECORD +28 -29
- utilities/__init__.py +1 -1
- utilities/asyncio.py +12 -11
- utilities/cryptography.py +3 -3
- utilities/eventkit.py +8 -8
- utilities/functions.py +1 -33
- utilities/iterables.py +1 -12
- utilities/logging.py +23 -24
- utilities/pathlib.py +34 -34
- utilities/postgres.py +12 -12
- utilities/pottery.py +5 -5
- utilities/pyinstrument.py +3 -3
- utilities/pytest.py +8 -8
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +1 -1
- utilities/random.py +8 -6
- utilities/redis.py +2 -2
- utilities/text.py +37 -2
- utilities/traceback.py +15 -18
- utilities/typed_settings.py +3 -3
- utilities/types.py +29 -35
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +431 -37
- utilities/period.py +0 -370
- {dycw_utilities-0.151.12.dist-info → dycw_utilities-0.153.0.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.151.12.dist-info → dycw_utilities-0.153.0.dist-info}/entry_points.txt +0 -0
- {dycw_utilities-0.151.12.dist-info → dycw_utilities-0.153.0.dist-info}/licenses/LICENSE +0 -0
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
|
-
]
|
File without changes
|
File without changes
|
File without changes
|