dycw-utilities 0.114.0__py3-none-any.whl → 0.114.2__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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.114.0
3
+ Version: 0.114.2
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
7
7
  Requires-Dist: typing-extensions<4.14,>=4.13.1
8
8
  Provides-Extra: test
9
- Requires-Dist: hypothesis<6.132,>=6.131.15; extra == 'test'
9
+ Requires-Dist: hypothesis<6.132,>=6.131.17; extra == 'test'
10
10
  Requires-Dist: pytest-asyncio<0.27,>=0.26.0; extra == 'test'
11
11
  Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
12
12
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
@@ -38,7 +38,7 @@ Provides-Extra: zzz-test-cachetools
38
38
  Requires-Dist: cachetools<5.6,>=5.5.2; extra == 'zzz-test-cachetools'
39
39
  Provides-Extra: zzz-test-click
40
40
  Requires-Dist: click<8.3,>=8.2.0; extra == 'zzz-test-click'
41
- Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-click'
41
+ Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-click'
42
42
  Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-click'
43
43
  Provides-Extra: zzz-test-contextlib
44
44
  Provides-Extra: zzz-test-contextvars
@@ -80,12 +80,12 @@ Provides-Extra: zzz-test-hypothesis
80
80
  Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-hypothesis'
81
81
  Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
82
82
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
83
- Requires-Dist: hypothesis<6.132,>=6.131.15; extra == 'zzz-test-hypothesis'
83
+ Requires-Dist: hypothesis<6.132,>=6.131.17; extra == 'zzz-test-hypothesis'
84
84
  Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
85
85
  Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-hypothesis'
86
86
  Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
87
- Requires-Dist: redis<6.1,>=6.0.0; extra == 'zzz-test-hypothesis'
88
- Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-hypothesis'
87
+ Requires-Dist: redis<6.2,>=6.1.0; extra == 'zzz-test-hypothesis'
88
+ Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-hypothesis'
89
89
  Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-hypothesis'
90
90
  Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-hypothesis'
91
91
  Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-hypothesis'
@@ -164,7 +164,7 @@ Provides-Extra: zzz-test-re
164
164
  Provides-Extra: zzz-test-redis
165
165
  Requires-Dist: orjson<3.11,>=3.10.15; extra == 'zzz-test-redis'
166
166
  Requires-Dist: polars-lts-cpu<1.30,>=1.29.0; extra == 'zzz-test-redis'
167
- Requires-Dist: redis<6.1,>=6.0.0; extra == 'zzz-test-redis'
167
+ Requires-Dist: redis<6.2,>=6.1.0; extra == 'zzz-test-redis'
168
168
  Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-redis'
169
169
  Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-redis'
170
170
  Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-redis'
@@ -184,7 +184,7 @@ Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-sqlalchemy'
184
184
  Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-sqlalchemy'
185
185
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-sqlalchemy'
186
186
  Requires-Dist: nest-asyncio<1.7,>=1.6.0; extra == 'zzz-test-sqlalchemy'
187
- Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-sqlalchemy'
187
+ Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-sqlalchemy'
188
188
  Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-sqlalchemy'
189
189
  Provides-Extra: zzz-test-sqlalchemy-polars
190
190
  Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-sqlalchemy-polars'
@@ -192,7 +192,7 @@ Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-sqlalchemy-polars'
192
192
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-sqlalchemy-polars'
193
193
  Requires-Dist: nest-asyncio<1.7,>=1.6.0; extra == 'zzz-test-sqlalchemy-polars'
194
194
  Requires-Dist: polars-lts-cpu<1.30,>=1.29.0; extra == 'zzz-test-sqlalchemy-polars'
195
- Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-sqlalchemy-polars'
195
+ Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-sqlalchemy-polars'
196
196
  Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-sqlalchemy-polars'
197
197
  Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-sqlalchemy-polars'
198
198
  Provides-Extra: zzz-test-streamlit
@@ -1,7 +1,7 @@
1
- utilities/__init__.py,sha256=qECk5Uaq4SsKcMpFjNUaChs2N78j3evztQRVgKylt4g,60
1
+ utilities/__init__.py,sha256=oAmd7ttFz157b1dwFyTjWf9pMUSBunxAZPRwd4iDr68,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=a4riQIfTYCe4oxXUbbuP7YB3tIlzmYxYDC9XygdsrQc,17637
4
+ utilities/asyncio.py,sha256=XVvmVNXKhP246JawvQCpFh4bMOl_NYm7QM60e-zrSFQ,20850
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
@@ -87,7 +87,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
87
  utilities/whenever.py,sha256=iLRP_-8CZtBpHKbGZGu-kjSMg1ZubJ-VSmgSy7Eudxw,17787
88
88
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
89
  utilities/zoneinfo.py,sha256=-Xm57PMMwDTYpxJdkiJG13wnbwK--I7XItBh5WVhD-o,1874
90
- dycw_utilities-0.114.0.dist-info/METADATA,sha256=plrwlVumL52G3Ix8B_ywj9b6hpqMXiheJfgTbbD5P_c,12943
91
- dycw_utilities-0.114.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.114.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.114.0.dist-info/RECORD,,
90
+ dycw_utilities-0.114.2.dist-info/METADATA,sha256=mbXuybydflJ95yffJ4FaYeqq1tiATKR6DrtDd_5v02c,12943
91
+ dycw_utilities-0.114.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.114.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.114.2.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.114.0"
3
+ __version__ = "0.114.2"
utilities/asyncio.py CHANGED
@@ -16,7 +16,7 @@ from asyncio import (
16
16
  sleep,
17
17
  timeout,
18
18
  )
19
- from collections.abc import Callable, Iterable, Mapping
19
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping
20
20
  from contextlib import (
21
21
  AsyncExitStack,
22
22
  _AsyncGeneratorContextManager,
@@ -30,6 +30,7 @@ from sys import stderr, stdout
30
30
  from typing import (
31
31
  TYPE_CHECKING,
32
32
  Generic,
33
+ NoReturn,
33
34
  Self,
34
35
  TextIO,
35
36
  TypeVar,
@@ -43,7 +44,6 @@ from utilities.errors import ImpossibleCaseError
43
44
  from utilities.functions import ensure_int, ensure_not_none
44
45
  from utilities.sentinel import Sentinel, sentinel
45
46
  from utilities.types import (
46
- Coroutine1,
47
47
  MaybeCallableEvent,
48
48
  MaybeType,
49
49
  THashable,
@@ -328,61 +328,155 @@ class ExceptionProcessor(QueueProcessor[Exception | type[Exception]]):
328
328
  class InfiniteLooper(ABC, Generic[THashable]):
329
329
  """An infinite loop which can throw exceptions by setting events."""
330
330
 
331
- events: Mapping[THashable, Event] = field(
331
+ _events: Mapping[THashable, Event] = field(
332
332
  default_factory=dict, init=False, repr=False
333
333
  )
334
334
  sleep_core: Duration = SECOND
335
335
  sleep_restart: Duration = MINUTE
336
336
 
337
337
  def __post_init__(self) -> None:
338
- self._reset_events()
339
-
340
- async def __call__(self) -> Coroutine1[None]:
338
+ self._events = {
339
+ event: Event() for event, _ in self._yield_events_and_exceptions()
340
+ }
341
+
342
+ async def __call__(self) -> None:
343
+ """Create a coroutine to run the looper."""
344
+ loopers = list(self._yield_loopers())
345
+ if len(loopers) == 0:
346
+ return await self._run_looper()
347
+ return await self._run_multiple_loopers(*loopers)
348
+
349
+ async def _run_looper(self) -> None:
350
+ """Run the looper by itself."""
341
351
  while True:
342
352
  try:
343
353
  self._reset_events()
344
354
  try:
345
- await self.initialize()
355
+ await self._initialize()
346
356
  except Exception as error: # noqa: BLE001
347
- self.error_upon_initialize(error)
357
+ self._error_upon_initialize(error)
348
358
  await sleep_dur(duration=self.sleep_restart)
349
359
  else:
350
360
  while True:
351
361
  try:
352
362
  event = next(
353
363
  key
354
- for (key, value) in self.events.items()
364
+ for (key, value) in self._events.items()
355
365
  if value.is_set()
356
366
  )
357
367
  except StopIteration:
358
- await self.core()
368
+ await self._core()
359
369
  await sleep_dur(duration=self.sleep_core)
360
370
  else:
361
- raise self.events_and_exceptions[event]
371
+ self._raise_error(event)
372
+ except InfiniteLooperError:
373
+ raise
362
374
  except Exception as error: # noqa: BLE001
363
- self.error_upon_core(error)
375
+ self._error_upon_core(error)
364
376
  await sleep_dur(duration=self.sleep_restart)
365
377
 
366
- @property
367
- @abstractmethod
368
- def events_and_exceptions(self) -> Mapping[THashable, MaybeType[BaseException]]:
369
- """A mapping of events to exceptions."""
378
+ async def _run_multiple_loopers(self, *loopers: InfiniteLooper) -> None:
379
+ """Run multiple loopers."""
380
+ while True:
381
+ self._reset_events()
382
+ try:
383
+ async with TaskGroup() as tg:
384
+ _ = tg.create_task(self._run_looper())
385
+ _ = [tg.create_task(looper()) for looper in loopers]
386
+ except Exception as error: # noqa: BLE001
387
+ self._error_upon_core(error) # pragma: no cover
388
+ await sleep_dur(duration=self.sleep_restart) # pragma: no cover
370
389
 
371
- async def initialize(self) -> None:
390
+ async def _initialize(self) -> None:
372
391
  """Initialize the loop."""
373
392
 
374
- async def core(self) -> None:
375
- """Run the core."""
393
+ async def _core(self) -> None:
394
+ """Run the core part of the loop."""
376
395
 
377
- def error_upon_initialize(self, error: Exception, /) -> None:
396
+ def _error_upon_initialize(self, error: Exception, /) -> None:
397
+ """Handle any errors upon initializing the looper."""
378
398
  _ = error
379
399
 
380
- def error_upon_core(self, error: Exception, /) -> None:
400
+ def _error_upon_core(self, error: Exception, /) -> None:
401
+ """Handle any errors upon running the core function."""
381
402
  _ = error
382
403
 
404
+ def _raise_error(self, event: THashable, /) -> NoReturn:
405
+ """Raise the error corresponding to given event."""
406
+ mapping = dict(self._yield_events_and_exceptions())
407
+ error = mapping.get(event, InfiniteLooperError)
408
+ raise error
409
+
383
410
  def _reset_events(self) -> None:
384
411
  """Reset the events."""
385
- self.events = {event: Event() for event in self.events_and_exceptions}
412
+ self._events = {
413
+ event: Event() for event, _ in self._yield_events_and_exceptions()
414
+ }
415
+
416
+ def _set_event(self, event: THashable, /) -> None:
417
+ """Set the given event."""
418
+ try:
419
+ event_obj = self._events[event]
420
+ except KeyError:
421
+ raise InfiniteLooperError(event=event) from None
422
+ event_obj.set()
423
+
424
+ def _yield_events_and_exceptions(
425
+ self,
426
+ ) -> Iterator[tuple[THashable, MaybeType[BaseException]]]:
427
+ """Yield the events & exceptions."""
428
+ yield from []
429
+
430
+ def _yield_loopers(self) -> Iterator[InfiniteLooper]:
431
+ """Yield any other infinite loopers which must also be run."""
432
+ yield from []
433
+
434
+
435
+ @dataclass(kw_only=True, slots=True)
436
+ class InfiniteLooperError(Exception):
437
+ event: Hashable
438
+
439
+ @override
440
+ def __str__(self) -> str:
441
+ return f"No event {self.event!r} found"
442
+
443
+
444
+ ##
445
+
446
+
447
+ @dataclass(kw_only=True)
448
+ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
449
+ """An infinite loop which processes a queue."""
450
+
451
+ queue_type: type[Queue[_T]] = field(default=Queue, repr=False)
452
+ _queue: Queue[_T] = field(init=False)
453
+ _current: Queue[_T] = field(init=False)
454
+
455
+ @override
456
+ def __post_init__(self) -> None:
457
+ super().__post_init__()
458
+ self._queue = self.queue_type()
459
+ self._current = self.queue_type()
460
+
461
+ @override
462
+ async def _core(self) -> None:
463
+ """Run the core part of the loop."""
464
+ items = await get_items(self._queue)
465
+ _ = get_items_nowait(self._current)
466
+ put_items_nowait(items, self._current)
467
+ try:
468
+ await self._process_items(*items)
469
+ except Exception:
470
+ put_items_nowait(items, self._queue)
471
+ raise
472
+
473
+ @abstractmethod
474
+ async def _process_items(self, *items: _T) -> None:
475
+ """Process the items."""
476
+
477
+ def put_items_nowait(self, *items: _T) -> None:
478
+ """Put items into the queue."""
479
+ put_items_nowait(items, self._queue)
386
480
 
387
481
 
388
482
  ##
@@ -590,6 +684,7 @@ __all__ = [
590
684
  "AsyncService",
591
685
  "EnhancedTaskGroup",
592
686
  "ExceptionProcessor",
687
+ "InfiniteLooperError",
593
688
  "QueueProcessor",
594
689
  "StreamCommandOutput",
595
690
  "UniquePriorityQueue",