dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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.
Potentially problematic release.
This version of dycw-utilities might be problematic. Click here for more details.
- dycw_utilities-0.175.31.dist-info/METADATA +34 -0
- dycw_utilities-0.175.31.dist-info/RECORD +103 -0
- dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
- {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +113 -64
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +381 -0
- utilities/enum.py +2 -2
- utilities/errors.py +1 -1
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +12 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +74 -85
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +4 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +296 -174
- utilities/pottery.py +8 -73
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +155 -46
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +8 -3
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1947 -0
- utilities/tempfile.py +95 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +2 -2
- utilities/traceback.py +46 -36
- utilities/types.py +62 -23
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +661 -151
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.148.5.dist-info/METADATA +0 -41
- dycw_utilities-0.148.5.dist-info/RECORD +0 -95
- dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
- dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/typed_settings.py +0 -144
utilities/eventkit.py
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from functools import wraps
|
|
5
|
-
from inspect import iscoroutinefunction
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Self, assert_never, cast, override
|
|
7
|
-
|
|
8
|
-
from eventkit import (
|
|
9
|
-
Constant,
|
|
10
|
-
Count,
|
|
11
|
-
DropWhile,
|
|
12
|
-
Enumerate,
|
|
13
|
-
Event,
|
|
14
|
-
Filter,
|
|
15
|
-
Fork,
|
|
16
|
-
Iterate,
|
|
17
|
-
Map,
|
|
18
|
-
Pack,
|
|
19
|
-
Partial,
|
|
20
|
-
PartialRight,
|
|
21
|
-
Pluck,
|
|
22
|
-
Skip,
|
|
23
|
-
Star,
|
|
24
|
-
Take,
|
|
25
|
-
TakeUntil,
|
|
26
|
-
TakeWhile,
|
|
27
|
-
Timestamp,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
from utilities.functions import apply_decorators
|
|
31
|
-
from utilities.iterables import always_iterable
|
|
32
|
-
from utilities.logging import get_logger
|
|
33
|
-
|
|
34
|
-
if TYPE_CHECKING:
|
|
35
|
-
from collections.abc import Callable
|
|
36
|
-
|
|
37
|
-
from utilities.types import Coro, LoggerOrName, MaybeCoro, MaybeIterable, TypeLike
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
##
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def add_listener[E: Event, F: Callable](
|
|
44
|
-
event: E,
|
|
45
|
-
listener: Callable[..., MaybeCoro[None]],
|
|
46
|
-
/,
|
|
47
|
-
*,
|
|
48
|
-
error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
|
|
49
|
-
ignore: TypeLike[BaseException] | None = None,
|
|
50
|
-
logger: LoggerOrName | None = None,
|
|
51
|
-
decorators: MaybeIterable[Callable[[F], F]] | None = None,
|
|
52
|
-
done: Callable[..., MaybeCoro[None]] | None = None,
|
|
53
|
-
keep_ref: bool = False,
|
|
54
|
-
) -> E:
|
|
55
|
-
"""Connect a listener to an event."""
|
|
56
|
-
lifted = lift_listener(
|
|
57
|
-
listener,
|
|
58
|
-
event,
|
|
59
|
-
error=error,
|
|
60
|
-
ignore=ignore,
|
|
61
|
-
logger=logger,
|
|
62
|
-
decorators=decorators,
|
|
63
|
-
)
|
|
64
|
-
return cast("E", event.connect(lifted, done=done, keep_ref=keep_ref))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
##
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@dataclass(repr=False, kw_only=True)
|
|
71
|
-
class LiftedEvent[F: Callable[..., MaybeCoro[None]]]:
|
|
72
|
-
"""A lifted version of `Event`."""
|
|
73
|
-
|
|
74
|
-
event: Event
|
|
75
|
-
|
|
76
|
-
def name(self) -> str:
|
|
77
|
-
return self.event.name() # pragma: no cover
|
|
78
|
-
|
|
79
|
-
def done(self) -> bool:
|
|
80
|
-
return self.event.done() # pragma: no cover
|
|
81
|
-
|
|
82
|
-
def set_done(self) -> None:
|
|
83
|
-
self.event.set_done() # pragma: no cover
|
|
84
|
-
|
|
85
|
-
def value(self) -> Any:
|
|
86
|
-
return self.event.value() # pragma: no cover
|
|
87
|
-
|
|
88
|
-
def connect[F2: Callable](
|
|
89
|
-
self,
|
|
90
|
-
listener: F,
|
|
91
|
-
/,
|
|
92
|
-
*,
|
|
93
|
-
error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
|
|
94
|
-
ignore: TypeLike[BaseException] | None = None,
|
|
95
|
-
logger: LoggerOrName | None = None,
|
|
96
|
-
decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
|
|
97
|
-
done: Callable[..., MaybeCoro[None]] | None = None,
|
|
98
|
-
keep_ref: bool = False,
|
|
99
|
-
) -> Event:
|
|
100
|
-
return add_listener(
|
|
101
|
-
self.event,
|
|
102
|
-
listener,
|
|
103
|
-
error=error,
|
|
104
|
-
ignore=ignore,
|
|
105
|
-
logger=logger,
|
|
106
|
-
decorators=decorators,
|
|
107
|
-
done=done,
|
|
108
|
-
keep_ref=keep_ref,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
def disconnect(
|
|
112
|
-
self, listener: Any, /, *, error: Any = None, done: Any = None
|
|
113
|
-
) -> Any:
|
|
114
|
-
return self.event.disconnect( # pragma: no cover
|
|
115
|
-
listener, error=error, done=done
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
def disconnect_obj(self, obj: Any, /) -> None:
|
|
119
|
-
self.event.disconnect_obj(obj) # pragma: no cover
|
|
120
|
-
|
|
121
|
-
def emit(self, *args: Any) -> None:
|
|
122
|
-
self.event.emit(*args) # pragma: no cover
|
|
123
|
-
|
|
124
|
-
def emit_threadsafe(self, *args: Any) -> None:
|
|
125
|
-
self.event.emit_threadsafe(*args) # pragma: no cover
|
|
126
|
-
|
|
127
|
-
def clear(self) -> None:
|
|
128
|
-
self.event.clear() # pragma: no cover
|
|
129
|
-
|
|
130
|
-
def run(self) -> list[Any]:
|
|
131
|
-
return self.event.run() # pragma: no cover
|
|
132
|
-
|
|
133
|
-
def pipe(self, *targets: Event) -> Event:
|
|
134
|
-
return self.event.pipe(*targets) # pragma: no cover
|
|
135
|
-
|
|
136
|
-
def fork(self, *targets: Event) -> Fork:
|
|
137
|
-
return self.event.fork(*targets) # pragma: no cover
|
|
138
|
-
|
|
139
|
-
def set_source(self, source: Any, /) -> None:
|
|
140
|
-
self.event.set_source(source) # pragma: no cover
|
|
141
|
-
|
|
142
|
-
def _onFinalize(self, ref: Any) -> None: # noqa: N802
|
|
143
|
-
self.event._onFinalize(ref) # noqa: SLF001 # pragma: no cover
|
|
144
|
-
|
|
145
|
-
async def aiter(self, *, skip_to_last: bool = False, tuples: bool = False) -> Any:
|
|
146
|
-
async for i in self.event.aiter( # pragma: no cover
|
|
147
|
-
skip_to_last=skip_to_last, tuples=tuples
|
|
148
|
-
):
|
|
149
|
-
yield i
|
|
150
|
-
|
|
151
|
-
__iadd__ = connect
|
|
152
|
-
__isub__ = disconnect
|
|
153
|
-
__call__ = emit
|
|
154
|
-
__or__ = pipe
|
|
155
|
-
|
|
156
|
-
@override
|
|
157
|
-
def __repr__(self) -> str:
|
|
158
|
-
return self.event.__repr__() # pragma: no cover
|
|
159
|
-
|
|
160
|
-
def __len__(self) -> int:
|
|
161
|
-
return self.event.__len__() # pragma: no cover
|
|
162
|
-
|
|
163
|
-
def __bool__(self) -> bool:
|
|
164
|
-
return self.event.__bool__() # pragma: no cover
|
|
165
|
-
|
|
166
|
-
def __getitem__(self, fork_targets: Any, /) -> Fork:
|
|
167
|
-
return self.event.__getitem__(fork_targets) # pragma: no cover
|
|
168
|
-
|
|
169
|
-
def __await__(self) -> Any:
|
|
170
|
-
return self.event.__await__() # pragma: no cover
|
|
171
|
-
|
|
172
|
-
__aiter__ = aiter
|
|
173
|
-
|
|
174
|
-
def __contains__(self, c: Any, /) -> bool:
|
|
175
|
-
return self.event.__contains__(c) # pragma: no cover
|
|
176
|
-
|
|
177
|
-
@override
|
|
178
|
-
def __reduce__(self) -> Any:
|
|
179
|
-
return self.event.__reduce__() # pragma: no cover
|
|
180
|
-
|
|
181
|
-
def filter(self, *, predicate: Any = bool) -> Filter:
|
|
182
|
-
return self.event.filter(predicate=predicate) # pragma: no cover
|
|
183
|
-
|
|
184
|
-
def skip(self, *, count: int = 1) -> Skip:
|
|
185
|
-
return self.event.skip(count=count) # pragma: no cover
|
|
186
|
-
|
|
187
|
-
def take(self, *, count: int = 1) -> Take:
|
|
188
|
-
return self.event.take(count=count) # pragma: no cover
|
|
189
|
-
|
|
190
|
-
def takewhile(self, *, predicate: Any = bool) -> TakeWhile:
|
|
191
|
-
return self.event.takewhile(predicate=predicate) # pragma: no cover
|
|
192
|
-
|
|
193
|
-
def dropwhile(self, *, predicate: Any = lambda x: not x) -> DropWhile: # pyright: ignore[reportUnknownLambdaType]
|
|
194
|
-
return self.event.dropwhile(predicate=predicate) # pragma: no cover
|
|
195
|
-
|
|
196
|
-
def takeuntil(self, notifier: Event, /) -> TakeUntil:
|
|
197
|
-
return self.event.takeuntil(notifier) # pragma: no cover
|
|
198
|
-
|
|
199
|
-
def constant(self, constant: Any, /) -> Constant:
|
|
200
|
-
return self.event.constant(constant) # pragma: no cover
|
|
201
|
-
|
|
202
|
-
def iterate(self, it: Any, /) -> Iterate:
|
|
203
|
-
return self.event.iterate(it) # pragma: no cover
|
|
204
|
-
|
|
205
|
-
def count(self, *, start: int = 0, step: int = 1) -> Count:
|
|
206
|
-
return self.event.count(start=start, step=step) # pragma: no cover
|
|
207
|
-
|
|
208
|
-
def enumerate(self, *, start: int = 0, step: int = 1) -> Enumerate:
|
|
209
|
-
return self.event.enumerate(start=start, step=step) # pragma: no cover
|
|
210
|
-
|
|
211
|
-
def timestamp(self) -> Timestamp:
|
|
212
|
-
return self.event.timestamp() # pragma: no cover
|
|
213
|
-
|
|
214
|
-
def partial(self, *left_args: Any) -> Partial:
|
|
215
|
-
return self.event.partial(*left_args) # pragma: no cover
|
|
216
|
-
|
|
217
|
-
def partial_right(self, *right_args: Any) -> PartialRight:
|
|
218
|
-
return self.event.partial_right(*right_args) # pragma: no cover
|
|
219
|
-
|
|
220
|
-
def star(self) -> Star:
|
|
221
|
-
return self.event.star() # pragma: no cover
|
|
222
|
-
|
|
223
|
-
def pack(self) -> Pack:
|
|
224
|
-
return self.event.pack() # pragma: no cover
|
|
225
|
-
|
|
226
|
-
def pluck(self, *selections: int | str) -> Pluck:
|
|
227
|
-
return self.event.pluck(*selections) # pragma: no cover
|
|
228
|
-
|
|
229
|
-
def map(
|
|
230
|
-
self,
|
|
231
|
-
func: Any,
|
|
232
|
-
/,
|
|
233
|
-
*,
|
|
234
|
-
timeout: float | None = None,
|
|
235
|
-
ordered: bool = True,
|
|
236
|
-
task_limit: int | None = None,
|
|
237
|
-
) -> Map:
|
|
238
|
-
return self.event.map( # pragma: no cover
|
|
239
|
-
func, timeout=timeout, ordered=ordered, task_limit=task_limit
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
##
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class TypedEvent[F: Callable[..., MaybeCoro[None]]](Event):
|
|
247
|
-
"""A typed version of `Event`."""
|
|
248
|
-
|
|
249
|
-
@override
|
|
250
|
-
def connect[F2: Callable](
|
|
251
|
-
self,
|
|
252
|
-
listener: F,
|
|
253
|
-
error: Callable[[Self, BaseException], MaybeCoro[None]] | None = None,
|
|
254
|
-
done: Callable[[Self], MaybeCoro[None]] | None = None,
|
|
255
|
-
keep_ref: bool = False,
|
|
256
|
-
*,
|
|
257
|
-
ignore: TypeLike[BaseException] | None = None,
|
|
258
|
-
logger: LoggerOrName | None = None,
|
|
259
|
-
decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
|
|
260
|
-
) -> Self:
|
|
261
|
-
lifted = lift_listener(
|
|
262
|
-
listener,
|
|
263
|
-
self,
|
|
264
|
-
error=cast(
|
|
265
|
-
"Callable[[Event, BaseException], MaybeCoro[None]] | None", error
|
|
266
|
-
),
|
|
267
|
-
ignore=ignore,
|
|
268
|
-
logger=logger,
|
|
269
|
-
decorators=decorators,
|
|
270
|
-
)
|
|
271
|
-
return cast(
|
|
272
|
-
"Self", super().connect(lifted, error=error, done=done, keep_ref=keep_ref)
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
##
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
|
|
280
|
-
listener: F1,
|
|
281
|
-
event: Event,
|
|
282
|
-
/,
|
|
283
|
-
*,
|
|
284
|
-
error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
|
|
285
|
-
ignore: TypeLike[BaseException] | None = None,
|
|
286
|
-
logger: LoggerOrName | None = None,
|
|
287
|
-
decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
|
|
288
|
-
) -> F1:
|
|
289
|
-
match error, bool(iscoroutinefunction(listener)):
|
|
290
|
-
case None, False:
|
|
291
|
-
listener_typed = cast("Callable[..., None]", listener)
|
|
292
|
-
|
|
293
|
-
@wraps(listener)
|
|
294
|
-
def listener_no_error_sync(*args: Any, **kwargs: Any) -> None:
|
|
295
|
-
try:
|
|
296
|
-
listener_typed(*args, **kwargs)
|
|
297
|
-
except Exception as exc: # noqa: BLE001
|
|
298
|
-
if (ignore is not None) and isinstance(exc, ignore):
|
|
299
|
-
return
|
|
300
|
-
get_logger(logger=logger).exception("")
|
|
301
|
-
|
|
302
|
-
lifted = listener_no_error_sync
|
|
303
|
-
|
|
304
|
-
case None, True:
|
|
305
|
-
listener_typed = cast("Callable[..., Coro[None]]", listener)
|
|
306
|
-
|
|
307
|
-
@wraps(listener)
|
|
308
|
-
async def listener_no_error_async(*args: Any, **kwargs: Any) -> None:
|
|
309
|
-
try:
|
|
310
|
-
await listener_typed(*args, **kwargs)
|
|
311
|
-
except Exception as exc: # noqa: BLE001
|
|
312
|
-
if (ignore is not None) and isinstance(exc, ignore):
|
|
313
|
-
return
|
|
314
|
-
get_logger(logger=logger).exception("")
|
|
315
|
-
|
|
316
|
-
lifted = listener_no_error_async
|
|
317
|
-
case _, _:
|
|
318
|
-
match bool(iscoroutinefunction(listener)), bool(iscoroutinefunction(error)):
|
|
319
|
-
case False, False:
|
|
320
|
-
listener_typed = cast("Callable[..., None]", listener)
|
|
321
|
-
error_typed = cast("Callable[[Event, Exception], None]", error)
|
|
322
|
-
|
|
323
|
-
@wraps(listener)
|
|
324
|
-
def listener_have_error_sync(*args: Any, **kwargs: Any) -> None:
|
|
325
|
-
try:
|
|
326
|
-
listener_typed(*args, **kwargs)
|
|
327
|
-
except Exception as exc: # noqa: BLE001
|
|
328
|
-
if (ignore is not None) and isinstance(exc, ignore):
|
|
329
|
-
return
|
|
330
|
-
error_typed(event, exc)
|
|
331
|
-
|
|
332
|
-
lifted = listener_have_error_sync
|
|
333
|
-
case False, True:
|
|
334
|
-
listener_typed = cast("Callable[..., None]", listener)
|
|
335
|
-
error_typed = cast(
|
|
336
|
-
"Callable[[Event, Exception], Coro[None]]", error
|
|
337
|
-
)
|
|
338
|
-
raise LiftListenerError(listener=listener_typed, error=error_typed)
|
|
339
|
-
case True, _:
|
|
340
|
-
listener_typed = cast("Callable[..., Coro[None]]", listener)
|
|
341
|
-
|
|
342
|
-
@wraps(listener)
|
|
343
|
-
async def listener_have_error_async(
|
|
344
|
-
*args: Any, **kwargs: Any
|
|
345
|
-
) -> None:
|
|
346
|
-
try:
|
|
347
|
-
await listener_typed(*args, **kwargs)
|
|
348
|
-
except Exception as exc: # noqa: BLE001
|
|
349
|
-
if (ignore is not None) and isinstance(exc, ignore):
|
|
350
|
-
return None
|
|
351
|
-
if iscoroutinefunction(error):
|
|
352
|
-
error_typed = cast(
|
|
353
|
-
"Callable[[Event, Exception], Coro[None]]", error
|
|
354
|
-
)
|
|
355
|
-
return await error_typed(event, exc)
|
|
356
|
-
error_typed = cast(
|
|
357
|
-
"Callable[[Event, Exception], None]", error
|
|
358
|
-
)
|
|
359
|
-
error_typed(event, exc)
|
|
360
|
-
|
|
361
|
-
lifted = listener_have_error_async
|
|
362
|
-
case _ as never:
|
|
363
|
-
assert_never(never)
|
|
364
|
-
case _ as never:
|
|
365
|
-
assert_never(never)
|
|
366
|
-
|
|
367
|
-
if decorators is not None:
|
|
368
|
-
lifted = apply_decorators(lifted, *always_iterable(decorators))
|
|
369
|
-
return cast("F1", lifted)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
@dataclass(kw_only=True, slots=True)
|
|
373
|
-
class LiftListenerError(Exception):
|
|
374
|
-
listener: Callable[..., None]
|
|
375
|
-
error: Callable[[Event, Exception], Coro[None]]
|
|
376
|
-
|
|
377
|
-
@override
|
|
378
|
-
def __str__(self) -> str:
|
|
379
|
-
return f"Synchronous listener {self.listener} cannot be paired with an asynchronous error handler {self.error}"
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
__all__ = [
|
|
383
|
-
"LiftListenerError",
|
|
384
|
-
"LiftedEvent",
|
|
385
|
-
"TypedEvent",
|
|
386
|
-
"add_listener",
|
|
387
|
-
"lift_listener",
|
|
388
|
-
]
|
utilities/period.py
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Self, TypedDict, overload, override
|
|
5
|
-
from zoneinfo import ZoneInfo
|
|
6
|
-
|
|
7
|
-
from whenever import Date, DateDelta, PlainDateTime, TimeDelta, ZonedDateTime
|
|
8
|
-
|
|
9
|
-
from utilities.dataclasses import replace_non_sentinel
|
|
10
|
-
from utilities.functions import get_class_name
|
|
11
|
-
from utilities.sentinel import Sentinel, sentinel
|
|
12
|
-
from utilities.whenever import format_compact
|
|
13
|
-
from utilities.zoneinfo import get_time_zone_name
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from utilities.types import TimeZoneLike
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class _PeriodAsDict[T: (Date, ZonedDateTime)](TypedDict):
|
|
20
|
-
start: T
|
|
21
|
-
end: T
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
25
|
-
class DatePeriod:
|
|
26
|
-
"""A period of dates."""
|
|
27
|
-
|
|
28
|
-
start: Date
|
|
29
|
-
end: Date
|
|
30
|
-
|
|
31
|
-
def __post_init__(self) -> None:
|
|
32
|
-
if self.start > self.end:
|
|
33
|
-
raise _PeriodInvalidError(start=self.start, end=self.end)
|
|
34
|
-
|
|
35
|
-
def __add__(self, other: DateDelta, /) -> Self:
|
|
36
|
-
"""Offset the period."""
|
|
37
|
-
return self.replace(start=self.start + other, end=self.end + other)
|
|
38
|
-
|
|
39
|
-
def __contains__(self, other: Date, /) -> bool:
|
|
40
|
-
"""Check if a date/datetime lies in the period."""
|
|
41
|
-
return self.start <= other <= self.end
|
|
42
|
-
|
|
43
|
-
@override
|
|
44
|
-
def __repr__(self) -> str:
|
|
45
|
-
cls = get_class_name(self)
|
|
46
|
-
return f"{cls}({self.start}, {self.end})"
|
|
47
|
-
|
|
48
|
-
def __sub__(self, other: DateDelta, /) -> Self:
|
|
49
|
-
"""Offset the period."""
|
|
50
|
-
return self.replace(start=self.start - other, end=self.end - other)
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def delta(self) -> DateDelta:
|
|
54
|
-
"""The delta of the period."""
|
|
55
|
-
return self.end - self.start
|
|
56
|
-
|
|
57
|
-
def format_compact(self) -> str:
|
|
58
|
-
"""Format the period in a compact fashion."""
|
|
59
|
-
fc, start, end = format_compact, self.start, self.end
|
|
60
|
-
if self.start == self.end:
|
|
61
|
-
return f"{fc(start)}="
|
|
62
|
-
if self.start.year_month() == self.end.year_month():
|
|
63
|
-
return f"{fc(start)}-{fc(end, fmt='%d')}"
|
|
64
|
-
if self.start.year == self.end.year:
|
|
65
|
-
return f"{fc(start)}-{fc(end, fmt='%m%d')}"
|
|
66
|
-
return f"{fc(start)}-{fc(end)}"
|
|
67
|
-
|
|
68
|
-
def replace(
|
|
69
|
-
self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
|
|
70
|
-
) -> Self:
|
|
71
|
-
"""Replace elements of the period."""
|
|
72
|
-
return replace_non_sentinel(self, start=start, end=end)
|
|
73
|
-
|
|
74
|
-
def to_dict(self) -> _PeriodAsDict[Date]:
|
|
75
|
-
"""Convert the period to a dictionary."""
|
|
76
|
-
return _PeriodAsDict(start=self.start, end=self.end)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
80
|
-
class ZonedDateTimePeriod:
|
|
81
|
-
"""A period of time."""
|
|
82
|
-
|
|
83
|
-
start: ZonedDateTime
|
|
84
|
-
end: ZonedDateTime
|
|
85
|
-
|
|
86
|
-
def __post_init__(self) -> None:
|
|
87
|
-
if self.start > self.end:
|
|
88
|
-
raise _PeriodInvalidError(start=self.start, end=self.end)
|
|
89
|
-
if self.start.tz != self.end.tz:
|
|
90
|
-
raise _PeriodTimeZoneError(
|
|
91
|
-
start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
def __add__(self, other: TimeDelta, /) -> Self:
|
|
95
|
-
"""Offset the period."""
|
|
96
|
-
return self.replace(start=self.start + other, end=self.end + other)
|
|
97
|
-
|
|
98
|
-
def __contains__(self, other: ZonedDateTime, /) -> bool:
|
|
99
|
-
"""Check if a date/datetime lies in the period."""
|
|
100
|
-
return self.start <= other <= self.end
|
|
101
|
-
|
|
102
|
-
@override
|
|
103
|
-
def __repr__(self) -> str:
|
|
104
|
-
cls = get_class_name(self)
|
|
105
|
-
return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
|
|
106
|
-
|
|
107
|
-
def __sub__(self, other: TimeDelta, /) -> Self:
|
|
108
|
-
"""Offset the period."""
|
|
109
|
-
return self.replace(start=self.start - other, end=self.end - other)
|
|
110
|
-
|
|
111
|
-
@property
|
|
112
|
-
def delta(self) -> TimeDelta:
|
|
113
|
-
"""The duration of the period."""
|
|
114
|
-
return self.end - self.start
|
|
115
|
-
|
|
116
|
-
@overload
|
|
117
|
-
def exact_eq(self, period: ZonedDateTimePeriod, /) -> bool: ...
|
|
118
|
-
@overload
|
|
119
|
-
def exact_eq(self, start: ZonedDateTime, end: ZonedDateTime, /) -> bool: ...
|
|
120
|
-
@overload
|
|
121
|
-
def exact_eq(
|
|
122
|
-
self, start: PlainDateTime, end: PlainDateTime, time_zone: ZoneInfo, /
|
|
123
|
-
) -> bool: ...
|
|
124
|
-
def exact_eq(self, *args: Any) -> bool:
|
|
125
|
-
"""Check if a period is exactly equal to another."""
|
|
126
|
-
if (len(args) == 1) and isinstance(args[0], ZonedDateTimePeriod):
|
|
127
|
-
return self.start.exact_eq(args[0].start) and self.end.exact_eq(args[0].end)
|
|
128
|
-
if (
|
|
129
|
-
(len(args) == 2)
|
|
130
|
-
and isinstance(args[0], ZonedDateTime)
|
|
131
|
-
and isinstance(args[1], ZonedDateTime)
|
|
132
|
-
):
|
|
133
|
-
return self.exact_eq(ZonedDateTimePeriod(args[0], args[1]))
|
|
134
|
-
if (
|
|
135
|
-
(len(args) == 3)
|
|
136
|
-
and isinstance(args[0], PlainDateTime)
|
|
137
|
-
and isinstance(args[1], PlainDateTime)
|
|
138
|
-
and isinstance(args[2], ZoneInfo)
|
|
139
|
-
):
|
|
140
|
-
return self.exact_eq(
|
|
141
|
-
ZonedDateTimePeriod(
|
|
142
|
-
args[0].assume_tz(args[2].key), args[1].assume_tz(args[2].key)
|
|
143
|
-
)
|
|
144
|
-
)
|
|
145
|
-
raise _PeriodExactEqArgumentsError(args=args)
|
|
146
|
-
|
|
147
|
-
def format_compact(self) -> str:
|
|
148
|
-
"""Format the period in a compact fashion."""
|
|
149
|
-
fc, start, end = format_compact, self.start, self.end
|
|
150
|
-
if start == end:
|
|
151
|
-
if end.second != 0:
|
|
152
|
-
return f"{fc(start)}="
|
|
153
|
-
if end.minute != 0:
|
|
154
|
-
return f"{fc(start, fmt='%Y%m%dT%H%M')}="
|
|
155
|
-
return f"{fc(start, fmt='%Y%m%dT%H')}="
|
|
156
|
-
if start.date() == end.date():
|
|
157
|
-
if end.second != 0:
|
|
158
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M%S')}"
|
|
159
|
-
if end.minute != 0:
|
|
160
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M')}"
|
|
161
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%H')}"
|
|
162
|
-
if start.date().year_month() == end.date().year_month():
|
|
163
|
-
if end.second != 0:
|
|
164
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M%S')}"
|
|
165
|
-
if end.minute != 0:
|
|
166
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M')}"
|
|
167
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H')}"
|
|
168
|
-
if start.year == end.year:
|
|
169
|
-
if end.second != 0:
|
|
170
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M%S')}"
|
|
171
|
-
if end.minute != 0:
|
|
172
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M')}"
|
|
173
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H')}"
|
|
174
|
-
if end.second != 0:
|
|
175
|
-
return f"{fc(start.to_plain())}-{fc(end)}"
|
|
176
|
-
if end.minute != 0:
|
|
177
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H%M')}"
|
|
178
|
-
return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H')}"
|
|
179
|
-
|
|
180
|
-
def replace(
|
|
181
|
-
self,
|
|
182
|
-
*,
|
|
183
|
-
start: ZonedDateTime | Sentinel = sentinel,
|
|
184
|
-
end: ZonedDateTime | Sentinel = sentinel,
|
|
185
|
-
) -> Self:
|
|
186
|
-
"""Replace elements of the period."""
|
|
187
|
-
return replace_non_sentinel(self, start=start, end=end)
|
|
188
|
-
|
|
189
|
-
@property
|
|
190
|
-
def time_zone(self) -> ZoneInfo:
|
|
191
|
-
"""The time zone of the period."""
|
|
192
|
-
return ZoneInfo(self.start.tz)
|
|
193
|
-
|
|
194
|
-
def to_dict(self) -> _PeriodAsDict[ZonedDateTime]:
|
|
195
|
-
"""Convert the period to a dictionary."""
|
|
196
|
-
return _PeriodAsDict(start=self.start, end=self.end)
|
|
197
|
-
|
|
198
|
-
def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
|
|
199
|
-
"""Convert the time zone."""
|
|
200
|
-
tz = get_time_zone_name(time_zone)
|
|
201
|
-
return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
@dataclass(kw_only=True, slots=True)
|
|
205
|
-
class PeriodError(Exception): ...
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@dataclass(kw_only=True, slots=True)
|
|
209
|
-
class _PeriodInvalidError[T: (Date, ZonedDateTime)](PeriodError):
|
|
210
|
-
start: T
|
|
211
|
-
end: T
|
|
212
|
-
|
|
213
|
-
@override
|
|
214
|
-
def __str__(self) -> str:
|
|
215
|
-
return f"Invalid period; got {self.start} > {self.end}"
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
@dataclass(kw_only=True, slots=True)
|
|
219
|
-
class _PeriodTimeZoneError(PeriodError):
|
|
220
|
-
start: ZoneInfo
|
|
221
|
-
end: ZoneInfo
|
|
222
|
-
|
|
223
|
-
@override
|
|
224
|
-
def __str__(self) -> str:
|
|
225
|
-
return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
@dataclass(kw_only=True, slots=True)
|
|
229
|
-
class _PeriodExactEqArgumentsError(PeriodError):
|
|
230
|
-
args: tuple[Any, ...]
|
|
231
|
-
|
|
232
|
-
@override
|
|
233
|
-
def __str__(self) -> str:
|
|
234
|
-
return f"Invalid arguments; got {self.args}"
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
__all__ = ["DatePeriod", "PeriodError", "ZonedDateTimePeriod"]
|