dycw-utilities 0.121.1__py3-none-any.whl → 0.122.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.121.1.dist-info → dycw_utilities-0.122.0.dist-info}/METADATA +1 -1
- {dycw_utilities-0.121.1.dist-info → dycw_utilities-0.122.0.dist-info}/RECORD +9 -9
- utilities/__init__.py +1 -1
- utilities/asyncio.py +132 -29
- utilities/fastapi.py +4 -0
- utilities/redis.py +1 -1
- utilities/sqlalchemy.py +1 -1
- {dycw_utilities-0.121.1.dist-info → dycw_utilities-0.122.0.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.121.1.dist-info → dycw_utilities-0.122.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=V19RNamTpOs4KxZ_wh7w8SsT-d-C24iIQHK07Pu3opA,60
|
2
2
|
utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
|
3
3
|
utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
|
4
|
-
utilities/asyncio.py,sha256=
|
4
|
+
utilities/asyncio.py,sha256=sVCGCGl5vGo1MztLcd46DiuWBR6XQW1CsGDyDdaevYg,22766
|
5
5
|
utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
|
6
6
|
utilities/atools.py,sha256=IYMuFSFGSKyuQmqD6v5IUtDlz8PPw0Sr87Cub_gRU3M,1168
|
7
7
|
utilities/cachetools.py,sha256=C1zqOg7BYz0IfQFK8e3qaDDgEZxDpo47F15RTfJM37Q,2910
|
@@ -16,7 +16,7 @@ utilities/datetime.py,sha256=uYoaOi_C1YtNXGfTN9xlTrW62Re2b1_4Skuv14_MeYQ,38985
|
|
16
16
|
utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
|
17
17
|
utilities/errors.py,sha256=gxsaa7eq7jbYl41Of40-ivjXqJB5gt4QAcJ0smZZMJE,829
|
18
18
|
utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
|
19
|
-
utilities/fastapi.py,sha256=
|
19
|
+
utilities/fastapi.py,sha256=LG1-Q8RDi7wsyVN6v74qptPYX8WGXPkFOQFniMvtzjc,2439
|
20
20
|
utilities/fpdf2.py,sha256=y1NGXR5chWqLXWpewGV3hlRGMr_5yV1lVRkPBhPEgJI,1843
|
21
21
|
utilities/functions.py,sha256=jgt592voaHNtX56qX0SRvFveVCRmSIxCZmqvpLZCnY8,27305
|
22
22
|
utilities/functools.py,sha256=WrpHt7NLNWSUn9A1Q_ZIWlNaYZOEI4IFKyBG9HO3BC4,1643
|
@@ -59,7 +59,7 @@ utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq
|
|
59
59
|
utilities/python_dotenv.py,sha256=iWcnpXbH7S6RoXHiLlGgyuH6udCupAcPd_gQ0eAenQ0,3190
|
60
60
|
utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
|
61
61
|
utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
|
62
|
-
utilities/redis.py,sha256=
|
62
|
+
utilities/redis.py,sha256=8ELnXiISVHY1m9elJWhekhLXg6NQSaNIIPvZ_EaGw3s,26624
|
63
63
|
utilities/reprlib.py,sha256=Re9bk3n-kC__9DxQmRlevqFA86pE6TtVfWjUgpbVOv0,1849
|
64
64
|
utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
|
65
65
|
utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
|
@@ -67,7 +67,7 @@ utilities/sentinel.py,sha256=3jIwgpMekWgDAxPDA_hXMP2St43cPhciKN3LWiZ7kv0,1248
|
|
67
67
|
utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
|
68
68
|
utilities/slack_sdk.py,sha256=A2f7-DYOngRoUP6ZdLIaUQ6Lfzgru5Xp3U3k5JfEkQE,3301
|
69
69
|
utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
|
70
|
-
utilities/sqlalchemy.py,sha256=
|
70
|
+
utilities/sqlalchemy.py,sha256=yCUCDhg0wFOCdEh6wwBD7Ma979OksLpz4arck1s653s,35447
|
71
71
|
utilities/sqlalchemy_polars.py,sha256=wjJpoUo-yO9E2ujpG_06vV5r2OdvBiQ4yvV6wKCa2Tk,15605
|
72
72
|
utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
|
73
73
|
utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
|
@@ -88,7 +88,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
|
|
88
88
|
utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
|
89
89
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
90
90
|
utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
|
91
|
-
dycw_utilities-0.
|
92
|
-
dycw_utilities-0.
|
93
|
-
dycw_utilities-0.
|
94
|
-
dycw_utilities-0.
|
91
|
+
dycw_utilities-0.122.0.dist-info/METADATA,sha256=VUsd4omUw1y-str8uasKFbLZUMA2Sr3HpSaq8f8coh0,12943
|
92
|
+
dycw_utilities-0.122.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
93
|
+
dycw_utilities-0.122.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
94
|
+
dycw_utilities-0.122.0.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/asyncio.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import datetime as dt
|
4
4
|
from abc import ABC, abstractmethod
|
5
5
|
from asyncio import (
|
6
|
+
CancelledError,
|
6
7
|
Event,
|
7
8
|
PriorityQueue,
|
8
9
|
Queue,
|
@@ -12,11 +13,17 @@ from asyncio import (
|
|
12
13
|
Task,
|
13
14
|
TaskGroup,
|
14
15
|
create_subprocess_shell,
|
16
|
+
create_task,
|
15
17
|
sleep,
|
16
18
|
timeout,
|
17
19
|
)
|
18
20
|
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping
|
19
|
-
from contextlib import
|
21
|
+
from contextlib import (
|
22
|
+
AsyncExitStack,
|
23
|
+
_AsyncGeneratorContextManager,
|
24
|
+
asynccontextmanager,
|
25
|
+
suppress,
|
26
|
+
)
|
20
27
|
from dataclasses import dataclass, field
|
21
28
|
from io import StringIO
|
22
29
|
from logging import getLogger
|
@@ -27,6 +34,7 @@ from typing import (
|
|
27
34
|
Any,
|
28
35
|
Generic,
|
29
36
|
NoReturn,
|
37
|
+
Self,
|
30
38
|
TextIO,
|
31
39
|
TypeVar,
|
32
40
|
assert_never,
|
@@ -42,7 +50,7 @@ from utilities.datetime import (
|
|
42
50
|
get_now,
|
43
51
|
round_datetime,
|
44
52
|
)
|
45
|
-
from utilities.errors import repr_error
|
53
|
+
from utilities.errors import ImpossibleCaseError, repr_error
|
46
54
|
from utilities.functions import ensure_int, ensure_not_none, get_class_name
|
47
55
|
from utilities.reprlib import get_repr
|
48
56
|
from utilities.sentinel import Sentinel, sentinel
|
@@ -60,6 +68,7 @@ if TYPE_CHECKING:
|
|
60
68
|
from asyncio.subprocess import Process
|
61
69
|
from collections.abc import AsyncIterator, Sequence
|
62
70
|
from contextvars import Context
|
71
|
+
from types import TracebackType
|
63
72
|
|
64
73
|
from utilities.types import Duration
|
65
74
|
|
@@ -125,26 +134,94 @@ class EnhancedTaskGroup(TaskGroup):
|
|
125
134
|
class InfiniteLooper(ABC, Generic[THashable]):
|
126
135
|
"""An infinite loop which can throw exceptions by setting events."""
|
127
136
|
|
128
|
-
sleep_core: DurationOrEveryDuration = SECOND
|
129
|
-
sleep_restart: DurationOrEveryDuration = MINUTE
|
130
|
-
|
137
|
+
sleep_core: DurationOrEveryDuration = field(default=SECOND, repr=False)
|
138
|
+
sleep_restart: DurationOrEveryDuration = field(default=MINUTE, repr=False)
|
139
|
+
duration: Duration | None = field(default=None, repr=False)
|
140
|
+
logger: str | None = field(default=None, repr=False)
|
141
|
+
_await_upon_aenter: bool = field(default=True, init=False, repr=False)
|
142
|
+
_depth: int = field(default=0, init=False, repr=False)
|
131
143
|
_events: Mapping[THashable | None, Event] = field(
|
132
144
|
default_factory=dict, init=False, repr=False, hash=False
|
133
145
|
)
|
146
|
+
_stack: AsyncExitStack = field(
|
147
|
+
default_factory=AsyncExitStack, init=False, repr=False
|
148
|
+
)
|
149
|
+
_task: Task[None] | None = field(default=None, init=False, repr=False)
|
134
150
|
|
135
151
|
def __post_init__(self) -> None:
|
136
152
|
self._events = {
|
137
153
|
event: Event() for event, _ in self._yield_events_and_exceptions()
|
138
154
|
}
|
139
155
|
|
140
|
-
async def
|
141
|
-
"""
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
156
|
+
async def __aenter__(self) -> Self:
|
157
|
+
"""Context manager entry."""
|
158
|
+
if (self._task is None) and (self._depth == 0):
|
159
|
+
_ = await self._stack.__aenter__()
|
160
|
+
self._task = create_task(self._run_looper())
|
161
|
+
if self._await_upon_aenter:
|
162
|
+
with suppress(CancelledError):
|
163
|
+
await self._task
|
164
|
+
elif (self._task is not None) and (self._depth >= 1):
|
165
|
+
...
|
166
|
+
else:
|
167
|
+
raise ImpossibleCaseError( # pragma: no cover
|
168
|
+
case=[f"{self._task=}", f"{self._depth=}"]
|
169
|
+
)
|
170
|
+
self._depth += 1
|
171
|
+
return self
|
172
|
+
|
173
|
+
async def __aexit__(
|
174
|
+
self,
|
175
|
+
exc_type: type[BaseException] | None = None,
|
176
|
+
exc_value: BaseException | None = None,
|
177
|
+
traceback: TracebackType | None = None,
|
178
|
+
) -> None:
|
179
|
+
"""Context manager exit."""
|
180
|
+
_ = (exc_type, exc_value, traceback)
|
181
|
+
if (self._task is None) or (self._depth == 0):
|
182
|
+
raise ImpossibleCaseError( # pragma: no cover
|
183
|
+
case=[f"{self._task=}", f"{self._depth=}"]
|
184
|
+
)
|
185
|
+
self._depth -= 1
|
186
|
+
if self._depth == 0:
|
187
|
+
_ = await self._stack.__aexit__(exc_type, exc_value, traceback)
|
188
|
+
with suppress(CancelledError):
|
189
|
+
await self._task
|
190
|
+
self._task = None
|
191
|
+
with suppress(Exception):
|
192
|
+
await self._teardown()
|
193
|
+
|
194
|
+
async def stop(self) -> None:
|
195
|
+
"""Stop the service."""
|
196
|
+
if self._task is None:
|
197
|
+
raise ImpossibleCaseError(case=[f"{self._task=}"]) # pragma: no cover
|
198
|
+
with suppress(CancelledError):
|
199
|
+
_ = self._task.cancel()
|
146
200
|
|
147
201
|
async def _run_looper(self) -> None:
|
202
|
+
"""Run the looper."""
|
203
|
+
match self.duration:
|
204
|
+
case None:
|
205
|
+
await self._run_looper_without_timeout()
|
206
|
+
case int() | float() | dt.timedelta() as duration:
|
207
|
+
try:
|
208
|
+
async with timeout_dur(duration=duration):
|
209
|
+
return await self._run_looper_without_timeout()
|
210
|
+
except TimeoutError:
|
211
|
+
await self.stop()
|
212
|
+
case _ as never:
|
213
|
+
assert_never(never)
|
214
|
+
return None
|
215
|
+
|
216
|
+
async def _run_looper_without_timeout(self) -> None:
|
217
|
+
"""Run the looper without a timeout."""
|
218
|
+
coroutines = list(self._yield_coroutines())
|
219
|
+
loopers = list(self._yield_loopers())
|
220
|
+
if (len(coroutines) == 0) and (len(loopers) == 0):
|
221
|
+
return await self._run_looper_by_itself()
|
222
|
+
return await self._run_looper_with_others(coroutines, loopers)
|
223
|
+
|
224
|
+
async def _run_looper_by_itself(self) -> None:
|
148
225
|
"""Run the looper by itself."""
|
149
226
|
while True:
|
150
227
|
try:
|
@@ -171,20 +248,31 @@ class InfiniteLooper(ABC, Generic[THashable]):
|
|
171
248
|
raise
|
172
249
|
except Exception as error: # noqa: BLE001
|
173
250
|
self._error_upon_core(error)
|
174
|
-
|
251
|
+
try:
|
252
|
+
await self._teardown()
|
253
|
+
except Exception as error: # noqa: BLE001
|
254
|
+
self._error_upon_teardown(error)
|
255
|
+
finally:
|
256
|
+
await self._run_sleep(self.sleep_restart)
|
175
257
|
|
176
|
-
async def
|
177
|
-
self,
|
258
|
+
async def _run_looper_with_others(
|
259
|
+
self,
|
260
|
+
coroutines: Iterable[Callable[[], Coroutine1[None]]],
|
261
|
+
loopers: Iterable[InfiniteLooper[Any]],
|
262
|
+
/,
|
178
263
|
) -> None:
|
179
264
|
"""Run multiple loopers."""
|
180
265
|
while True:
|
181
266
|
self._reset_events()
|
182
267
|
try:
|
183
|
-
async with TaskGroup() as tg:
|
184
|
-
_ = tg.create_task(self.
|
268
|
+
async with TaskGroup() as tg, AsyncExitStack() as stack:
|
269
|
+
_ = tg.create_task(self._run_looper_by_itself())
|
185
270
|
_ = [tg.create_task(c()) for c in coroutines]
|
271
|
+
_ = [
|
272
|
+
tg.create_task(stack.enter_async_context(lo)) for lo in loopers
|
273
|
+
]
|
186
274
|
except ExceptionGroup as error:
|
187
|
-
self.
|
275
|
+
self._error_group_upon_others(error)
|
188
276
|
await self._run_sleep(self.sleep_restart)
|
189
277
|
|
190
278
|
async def _initialize(self) -> None:
|
@@ -193,6 +281,9 @@ class InfiniteLooper(ABC, Generic[THashable]):
|
|
193
281
|
async def _core(self) -> None:
|
194
282
|
"""Run the core part of the loop."""
|
195
283
|
|
284
|
+
async def _teardown(self) -> None:
|
285
|
+
"""Tear down the loop."""
|
286
|
+
|
196
287
|
def _error_upon_initialize(self, error: Exception, /) -> None:
|
197
288
|
"""Handle any errors upon initializing the looper."""
|
198
289
|
if self.logger is not None:
|
@@ -213,7 +304,17 @@ class InfiniteLooper(ABC, Generic[THashable]):
|
|
213
304
|
self._sleep_restart_desc,
|
214
305
|
)
|
215
306
|
|
216
|
-
def
|
307
|
+
def _error_upon_teardown(self, error: Exception, /) -> None:
|
308
|
+
"""Handle any errors upon tearing down the looper."""
|
309
|
+
if self.logger is not None:
|
310
|
+
getLogger(name=self.logger).error(
|
311
|
+
"%r encountered %r whilst tearing down; sleeping %s...",
|
312
|
+
get_class_name(self),
|
313
|
+
repr_error(error),
|
314
|
+
self._sleep_restart_desc,
|
315
|
+
)
|
316
|
+
|
317
|
+
def _error_group_upon_others(self, group: ExceptionGroup, /) -> None:
|
217
318
|
"""Handle any errors upon running the core function."""
|
218
319
|
if self.logger is not None:
|
219
320
|
errors = group.exceptions
|
@@ -269,15 +370,19 @@ class InfiniteLooper(ABC, Generic[THashable]):
|
|
269
370
|
raise _InfiniteLooperNoSuchEventError(looper=self, event=event) from None
|
270
371
|
event_obj.set()
|
271
372
|
|
373
|
+
def _yield_events_and_exceptions(
|
374
|
+
self,
|
375
|
+
) -> Iterator[tuple[THashable | None, MaybeType[Exception]]]:
|
376
|
+
"""Yield the events & exceptions."""
|
377
|
+
yield (None, _InfiniteLooperDefaultEventError(looper=self))
|
378
|
+
|
272
379
|
def _yield_coroutines(self) -> Iterator[Callable[[], Coroutine1[None]]]:
|
273
380
|
"""Yield any other coroutines which must also be run."""
|
274
381
|
yield from []
|
275
382
|
|
276
|
-
def
|
277
|
-
|
278
|
-
|
279
|
-
"""Yield the events & exceptions."""
|
280
|
-
yield (None, _InfiniteLooperDefaultEventError)
|
383
|
+
def _yield_loopers(self) -> Iterator[InfiniteLooper[Any]]:
|
384
|
+
"""Yield any other loopers which must also be run."""
|
385
|
+
yield from []
|
281
386
|
|
282
387
|
|
283
388
|
@dataclass(kw_only=True, slots=True)
|
@@ -309,6 +414,7 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
309
414
|
"""An infinite loop which processes a queue."""
|
310
415
|
|
311
416
|
queue_type: type[Queue[_T]] = field(default=Queue, repr=False)
|
417
|
+
_await_upon_aenter: bool = field(default=False, init=False, repr=False)
|
312
418
|
_queue: Queue[_T] = field(init=False)
|
313
419
|
|
314
420
|
@override
|
@@ -346,6 +452,7 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
346
452
|
"""Run until the queue is empty."""
|
347
453
|
while not self.empty():
|
348
454
|
await self._process_items(*get_items_nowait(self._queue))
|
455
|
+
await self.stop()
|
349
456
|
|
350
457
|
@override
|
351
458
|
def _error_upon_core(self, error: Exception, /) -> None:
|
@@ -353,12 +460,12 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
353
460
|
if self.logger is not None:
|
354
461
|
if isinstance(error, InfiniteQueueLooperError):
|
355
462
|
getLogger(name=self.logger).error(
|
356
|
-
"%r encountered %s whilst processing %d item(s) %s; sleeping
|
463
|
+
"%r encountered %s whilst processing %d item(s) %s; sleeping %s...",
|
357
464
|
get_class_name(self),
|
358
465
|
repr_error(error.error),
|
359
466
|
len(error.items),
|
360
467
|
get_repr(error.items),
|
361
|
-
self.
|
468
|
+
self._sleep_restart_desc,
|
362
469
|
)
|
363
470
|
else:
|
364
471
|
super()._error_upon_core(error) # pragma: no cover
|
@@ -370,10 +477,6 @@ class InfiniteQueueLooperError(Exception, Generic[_T]):
|
|
370
477
|
items: Sequence[_T]
|
371
478
|
error: Exception
|
372
479
|
|
373
|
-
@override
|
374
|
-
def __str__(self) -> str:
|
375
|
-
return f"{get_class_name(self.looper)!r} encountered {repr_error(self.error)} whilst processing {len(self.items)} item(s): {get_repr(self.items)}"
|
376
|
-
|
377
480
|
|
378
481
|
##
|
379
482
|
|
utilities/fastapi.py
CHANGED
utilities/redis.py
CHANGED
@@ -611,7 +611,7 @@ class Publisher(InfiniteQueueLooper[None, tuple[str, EncodableT]]):
|
|
611
611
|
@override
|
612
612
|
def _yield_events_and_exceptions(
|
613
613
|
self,
|
614
|
-
) -> Iterator[tuple[None, MaybeType[
|
614
|
+
) -> Iterator[tuple[None, MaybeType[Exception]]]:
|
615
615
|
yield (None, PublisherError) # skipif-ci-and-not-linux
|
616
616
|
|
617
617
|
|
utilities/sqlalchemy.py
CHANGED
@@ -640,7 +640,7 @@ class Upserter(InfiniteQueueLooper[None, _InsertItem]):
|
|
640
640
|
@override
|
641
641
|
def _yield_events_and_exceptions(
|
642
642
|
self,
|
643
|
-
) -> Iterator[tuple[None, MaybeType[
|
643
|
+
) -> Iterator[tuple[None, MaybeType[Exception]]]:
|
644
644
|
yield (None, UpserterError)
|
645
645
|
|
646
646
|
|
File without changes
|
File without changes
|