dycw-utilities 0.115.1__py3-none-any.whl → 0.116.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.115.1
3
+ Version: 0.116.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,7 +1,7 @@
1
- utilities/__init__.py,sha256=TbTtbGuM9AadB_WkxBUd94oWTm59iHPdjdluFVXZ-HI,60
1
+ utilities/__init__.py,sha256=pLugaU63qZmC8Jq_ToL0O5damDhX5lVpukCAQ0DN0UA,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=rDsYSQUhl2YZQKtkbeCNK4Mrbh5heVaqWLvLn2ppMJg,21534
4
+ utilities/asyncio.py,sha256=NmAyhbOabek5LRagPcgTcSjjAZGK1yu5tLErWDdv5V0,23353
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
@@ -14,7 +14,7 @@ utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
14
  utilities/dataclasses.py,sha256=iiC1wpGXWhaocIikzwBt8bbLWyImoUlOlcDZJGejaIg,33011
15
15
  utilities/datetime.py,sha256=PcN-4_sSPX1zbpdzBQRdo08pubCuGHyigxkV6SUnvlo,38733
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
- utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
17
+ utilities/errors.py,sha256=gxsaa7eq7jbYl41Of40-ivjXqJB5gt4QAcJ0smZZMJE,829
18
18
  utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
19
19
  utilities/fastapi.py,sha256=y-35at3005jzlNx2wJoiSvB1Ch5bMo30wgU_so3IDdI,2467
20
20
  utilities/fpdf2.py,sha256=y1NGXR5chWqLXWpewGV3hlRGMr_5yV1lVRkPBhPEgJI,1843
@@ -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.115.1.dist-info/METADATA,sha256=Bqi_67m9vtvYTiPadk63p01uSHmZlqyxM7_6YEuoo3A,12943
91
- dycw_utilities-0.115.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.115.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.115.1.dist-info/RECORD,,
90
+ dycw_utilities-0.116.0.dist-info/METADATA,sha256=yAhNM7tqxJvQ36peel-0LjBpoJ-0nS9jT20DPZ9yBn4,12943
91
+ dycw_utilities-0.116.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.116.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.116.0.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.115.1"
3
+ __version__ = "0.116.0"
utilities/asyncio.py CHANGED
@@ -30,6 +30,7 @@ from subprocess import PIPE
30
30
  from sys import stderr, stdout
31
31
  from typing import (
32
32
  TYPE_CHECKING,
33
+ Any,
33
34
  Generic,
34
35
  NoReturn,
35
36
  Self,
@@ -41,8 +42,9 @@ from typing import (
41
42
  )
42
43
 
43
44
  from utilities.datetime import MILLISECOND, MINUTE, SECOND, datetime_duration_to_float
44
- from utilities.errors import ImpossibleCaseError
45
+ from utilities.errors import ImpossibleCaseError, repr_error
45
46
  from utilities.functions import ensure_int, ensure_not_none, get_class_name
47
+ from utilities.reprlib import get_repr
46
48
  from utilities.sentinel import Sentinel, sentinel
47
49
  from utilities.types import (
48
50
  Coroutine1,
@@ -379,17 +381,19 @@ class InfiniteLooper(ABC, Generic[THashable]):
379
381
  self._error_upon_core(error)
380
382
  await sleep_dur(duration=self.sleep_restart)
381
383
 
382
- async def _run_looper_with_coroutines(self, *coroutines: Coroutine1) -> None:
384
+ async def _run_looper_with_coroutines(
385
+ self, *coroutines: Callable[[], Coroutine1[None]]
386
+ ) -> None:
383
387
  """Run multiple loopers."""
384
388
  while True:
385
389
  self._reset_events()
386
390
  try:
387
391
  async with TaskGroup() as tg:
388
392
  _ = tg.create_task(self._run_looper())
389
- _ = list(map(tg.create_task, coroutines))
390
- except Exception as error: # noqa: BLE001
391
- self._error_upon_core(error) # pragma: no cover
392
- await sleep_dur(duration=self.sleep_restart) # pragma: no cover
393
+ _ = [tg.create_task(c()) for c in coroutines]
394
+ except ExceptionGroup as error:
395
+ self._error_group_upon_coroutines(error)
396
+ await sleep_dur(duration=self.sleep_restart)
393
397
 
394
398
  async def _initialize(self) -> None:
395
399
  """Initialize the loop."""
@@ -401,9 +405,9 @@ class InfiniteLooper(ABC, Generic[THashable]):
401
405
  """Handle any errors upon initializing the looper."""
402
406
  if self.logger is not None:
403
407
  getLogger(name=self.logger).error(
404
- "Error initializing %r due to %r; sleeping for %s...",
408
+ "%r encountered %r whilst initializing; sleeping for %s...",
405
409
  get_class_name(self),
406
- repr(error),
410
+ repr_error(error),
407
411
  self.sleep_restart,
408
412
  )
409
413
 
@@ -411,12 +415,25 @@ class InfiniteLooper(ABC, Generic[THashable]):
411
415
  """Handle any errors upon running the core function."""
412
416
  if self.logger is not None:
413
417
  getLogger(name=self.logger).error(
414
- "Error running %r due to %r; sleeping for %s...",
418
+ "%r encountered %r; sleeping for %s...",
415
419
  get_class_name(self),
416
- repr(error),
420
+ repr_error(error),
417
421
  self.sleep_restart,
418
422
  )
419
423
 
424
+ def _error_group_upon_coroutines(self, group: ExceptionGroup, /) -> None:
425
+ """Handle any errors upon running the core function."""
426
+ if self.logger is not None:
427
+ errors = group.exceptions
428
+ n = len(errors)
429
+ msgs = [f"{get_class_name(self)!r} encountered {n} error(s):"]
430
+ msgs.extend(
431
+ f"- Error #{i}/{n}: {repr_error(e)}"
432
+ for i, e in enumerate(errors, start=1)
433
+ )
434
+ msgs.append(f"Sleeping for {self.sleep_restart}...")
435
+ getLogger(name=self.logger).error("\n".join(msgs))
436
+
420
437
  def _raise_error(self, event: THashable, /) -> NoReturn:
421
438
  """Raise the error corresponding to given event."""
422
439
  mapping = dict(self._yield_events_and_exceptions())
@@ -434,10 +451,10 @@ class InfiniteLooper(ABC, Generic[THashable]):
434
451
  try:
435
452
  event_obj = self._events[event]
436
453
  except KeyError:
437
- raise InfiniteLooperError(event=event) from None
454
+ raise InfiniteLooperError(looper=self, event=event) from None
438
455
  event_obj.set()
439
456
 
440
- def _yield_coroutines(self) -> Iterator[Coroutine1[None]]:
457
+ def _yield_coroutines(self) -> Iterator[Callable[[], Coroutine1[None]]]:
441
458
  """Yield any other coroutines which must also be run."""
442
459
  yield from []
443
460
 
@@ -450,11 +467,12 @@ class InfiniteLooper(ABC, Generic[THashable]):
450
467
 
451
468
  @dataclass(kw_only=True, slots=True)
452
469
  class InfiniteLooperError(Exception):
470
+ looper: InfiniteLooper[Any]
453
471
  event: Hashable
454
472
 
455
473
  @override
456
474
  def __str__(self) -> str:
457
- return f"No event {self.event!r} found"
475
+ return f"{get_class_name(self.looper)!r} does not have an event {self.event!r}"
458
476
 
459
477
 
460
478
  ##
@@ -466,25 +484,24 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
466
484
 
467
485
  queue_type: type[Queue[_T]] = field(default=Queue, repr=False)
468
486
  _queue: Queue[_T] = field(init=False)
469
- _current: Queue[_T] = field(init=False)
470
487
 
471
488
  @override
472
489
  def __post_init__(self) -> None:
473
490
  super().__post_init__()
474
491
  self._queue = self.queue_type()
475
- self._current = self.queue_type()
476
492
 
477
493
  @override
478
494
  async def _core(self) -> None:
479
495
  """Run the core part of the loop."""
480
- items = await get_items(self._queue)
481
- _ = get_items_nowait(self._current)
482
- put_items_nowait(items, self._current)
496
+ items = get_items_nowait(self._queue)
497
+ if len(items) == 0:
498
+ return
483
499
  try:
484
500
  await self._process_items(*items)
485
- except Exception:
486
- put_items_nowait(items, self._queue)
487
- raise
501
+ except Exception as error: # noqa: BLE001
502
+ raise InfiniteQueueLooperError(
503
+ looper=self, items=items, error=error
504
+ ) from None
488
505
 
489
506
  @abstractmethod
490
507
  async def _process_items(self, *items: _T) -> None:
@@ -494,6 +511,33 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
494
511
  """Put items into the queue."""
495
512
  put_items_nowait(items, self._queue)
496
513
 
514
+ @override
515
+ def _error_upon_core(self, error: Exception, /) -> None:
516
+ """Handle any errors upon running the core function."""
517
+ if self.logger is not None:
518
+ if isinstance(error, InfiniteQueueLooperError):
519
+ getLogger(name=self.logger).error(
520
+ "%r encountered %s whilst processing %d item(s) %s; sleeping for %s...",
521
+ get_class_name(self),
522
+ repr_error(error.error),
523
+ len(error.items),
524
+ get_repr(error.items),
525
+ self.sleep_restart,
526
+ )
527
+ else:
528
+ super()._error_upon_core(error) # pragma: no cover
529
+
530
+
531
+ @dataclass(kw_only=True, slots=True)
532
+ class InfiniteQueueLooperError(Exception, Generic[_T]):
533
+ looper: InfiniteQueueLooper[Any, Any]
534
+ items: Sequence[_T]
535
+ error: Exception
536
+
537
+ @override
538
+ def __str__(self) -> str:
539
+ return f"{get_class_name(self.looper)!r} encountered {repr_error(self.error)} whilst processing {len(self.items)} item(s): {get_repr(self.items)}"
540
+
497
541
 
498
542
  ##
499
543
 
@@ -703,6 +747,7 @@ __all__ = [
703
747
  "InfiniteLooper",
704
748
  "InfiniteLooperError",
705
749
  "InfiniteQueueLooper",
750
+ "InfiniteQueueLooperError",
706
751
  "QueueProcessor",
707
752
  "StreamCommandOutput",
708
753
  "UniquePriorityQueue",
utilities/errors.py CHANGED
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import override
4
+ from typing import TYPE_CHECKING, assert_never, override
5
+
6
+ if TYPE_CHECKING:
7
+ from utilities.types import MaybeType
5
8
 
6
9
 
7
10
  @dataclass(kw_only=True, slots=True)
@@ -14,4 +17,18 @@ class ImpossibleCaseError(Exception):
14
17
  return f"Case must be possible: {desc}."
15
18
 
16
19
 
17
- __all__ = ["ImpossibleCaseError"]
20
+ ##
21
+
22
+
23
+ def repr_error(error: MaybeType[Exception], /) -> str:
24
+ """Get a string representation of an error."""
25
+ match error:
26
+ case Exception() as error_obj:
27
+ return f"{error_obj.__class__.__name__}({error_obj})"
28
+ case type() as error_cls:
29
+ return error_cls.__name__
30
+ case _ as never:
31
+ assert_never(never)
32
+
33
+
34
+ __all__ = ["ImpossibleCaseError", "repr_error"]