dycw-utilities 0.125.11__py3-none-any.whl → 0.125.13__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.125.11.dist-info → dycw_utilities-0.125.13.dist-info}/METADATA +5 -5
- {dycw_utilities-0.125.11.dist-info → dycw_utilities-0.125.13.dist-info}/RECORD +6 -6
- utilities/__init__.py +1 -1
- utilities/asyncio.py +473 -9
- {dycw_utilities-0.125.11.dist-info → dycw_utilities-0.125.13.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.125.11.dist-info → dycw_utilities-0.125.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dycw-utilities
|
3
|
-
Version: 0.125.
|
3
|
+
Version: 0.125.13
|
4
4
|
Author-email: Derek Wan <d.wan@icloud.com>
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.12
|
@@ -25,7 +25,7 @@ Requires-Dist: altair<5.6,>=5.5.0; extra == 'zzz-test-altair'
|
|
25
25
|
Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-altair'
|
26
26
|
Requires-Dist: img2pdf<0.7,>=0.6.0; extra == 'zzz-test-altair'
|
27
27
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-altair'
|
28
|
-
Requires-Dist: vl-convert-python<1.
|
28
|
+
Requires-Dist: vl-convert-python<1.9,>=1.8.0; extra == 'zzz-test-altair'
|
29
29
|
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-altair'
|
30
30
|
Provides-Extra: zzz-test-asyncio
|
31
31
|
Provides-Extra: zzz-test-atomicwrites
|
@@ -82,7 +82,7 @@ Requires-Dist: hypothesis<6.132,>=6.131.30; extra == 'zzz-test-hypothesis'
|
|
82
82
|
Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
|
83
83
|
Requires-Dist: numpy<2.3,>=2.2.6; extra == 'zzz-test-hypothesis'
|
84
84
|
Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
|
85
|
-
Requires-Dist: redis<6.
|
85
|
+
Requires-Dist: redis<6.3,>=6.2.0; extra == 'zzz-test-hypothesis'
|
86
86
|
Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-hypothesis'
|
87
87
|
Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-hypothesis'
|
88
88
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-hypothesis'
|
@@ -162,7 +162,7 @@ Provides-Extra: zzz-test-re
|
|
162
162
|
Provides-Extra: zzz-test-redis
|
163
163
|
Requires-Dist: orjson<3.11,>=3.10.15; extra == 'zzz-test-redis'
|
164
164
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-redis'
|
165
|
-
Requires-Dist: redis<6.
|
165
|
+
Requires-Dist: redis<6.3,>=6.2.0; extra == 'zzz-test-redis'
|
166
166
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-redis'
|
167
167
|
Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-redis'
|
168
168
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-redis'
|
@@ -174,7 +174,7 @@ Requires-Dist: scipy<1.16,>=1.15.3; extra == 'zzz-test-scipy'
|
|
174
174
|
Provides-Extra: zzz-test-sentinel
|
175
175
|
Provides-Extra: zzz-test-shelve
|
176
176
|
Provides-Extra: zzz-test-slack-sdk
|
177
|
-
Requires-Dist: aiohttp<3.12.
|
177
|
+
Requires-Dist: aiohttp<3.12.5,>=3.12.4; extra == 'zzz-test-slack-sdk'
|
178
178
|
Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
|
179
179
|
Provides-Extra: zzz-test-socket
|
180
180
|
Provides-Extra: zzz-test-sqlalchemy
|
@@ -1,6 +1,6 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=7ZYwhNHFuejRkKzg0MeSvh2I14YxTE-mRj6ie69FmMU,61
|
2
2
|
utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
|
3
|
-
utilities/asyncio.py,sha256=
|
3
|
+
utilities/asyncio.py,sha256=uUVIpmBMP-tAYPnE2Yi7ZyTno7VYImjNp7uA85MQhys,47865
|
4
4
|
utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
|
5
5
|
utilities/atools.py,sha256=IYMuFSFGSKyuQmqD6v5IUtDlz8PPw0Sr87Cub_gRU3M,1168
|
6
6
|
utilities/cachetools.py,sha256=C1zqOg7BYz0IfQFK8e3qaDDgEZxDpo47F15RTfJM37Q,2910
|
@@ -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.125.
|
92
|
-
dycw_utilities-0.125.
|
93
|
-
dycw_utilities-0.125.
|
94
|
-
dycw_utilities-0.125.
|
91
|
+
dycw_utilities-0.125.13.dist-info/METADATA,sha256=hQ21eoRHxkp3q3Y7wQ3F9fXew2_S1sTHV3hd68qX8e4,12852
|
92
|
+
dycw_utilities-0.125.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
93
|
+
dycw_utilities-0.125.13.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
94
|
+
dycw_utilities-0.125.13.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/asyncio.py
CHANGED
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
|
|
5
5
|
from asyncio import (
|
6
6
|
CancelledError,
|
7
7
|
Event,
|
8
|
+
Lock,
|
8
9
|
PriorityQueue,
|
9
10
|
Queue,
|
10
11
|
QueueEmpty,
|
@@ -29,7 +30,7 @@ from contextlib import (
|
|
29
30
|
from dataclasses import dataclass, field
|
30
31
|
from io import StringIO
|
31
32
|
from itertools import chain
|
32
|
-
from logging import getLogger
|
33
|
+
from logging import DEBUG, Logger, getLogger
|
33
34
|
from subprocess import PIPE
|
34
35
|
from sys import stderr, stdout
|
35
36
|
from typing import (
|
@@ -47,6 +48,7 @@ from typing import (
|
|
47
48
|
|
48
49
|
from typing_extensions import deprecated
|
49
50
|
|
51
|
+
from utilities.dataclasses import replace_non_sentinel
|
50
52
|
from utilities.datetime import (
|
51
53
|
MINUTE,
|
52
54
|
SECOND,
|
@@ -610,12 +612,9 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
610
612
|
"""An infinite loop which processes a queue."""
|
611
613
|
|
612
614
|
_await_upon_aenter: bool = field(default=False, init=False, repr=False)
|
613
|
-
_queue: EnhancedQueue[_T] = field(
|
614
|
-
|
615
|
-
|
616
|
-
def __post_init__(self) -> None:
|
617
|
-
super().__post_init__()
|
618
|
-
self._queue = EnhancedQueue()
|
615
|
+
_queue: EnhancedQueue[_T] = field(
|
616
|
+
default_factory=EnhancedQueue, init=False, repr=False
|
617
|
+
)
|
619
618
|
|
620
619
|
def __len__(self) -> int:
|
621
620
|
return self._queue.qsize()
|
@@ -623,8 +622,8 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
623
622
|
@override
|
624
623
|
async def _core(self) -> None:
|
625
624
|
"""Run the core part of the loop."""
|
626
|
-
|
627
|
-
|
625
|
+
if self.empty():
|
626
|
+
return
|
628
627
|
await self._process_queue()
|
629
628
|
|
630
629
|
@abstractmethod
|
@@ -643,6 +642,10 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
643
642
|
"""Put items into the queue at the end without blocking."""
|
644
643
|
self._queue.put_right_nowait(*items) # pragma: no cover
|
645
644
|
|
645
|
+
def qsize(self) -> int:
|
646
|
+
"""Get the number of items in the queue."""
|
647
|
+
return self._queue.qsize()
|
648
|
+
|
646
649
|
async def run_until_empty(self, *, stop: bool = False) -> None:
|
647
650
|
"""Run until the queue is empty."""
|
648
651
|
while not self.empty():
|
@@ -654,6 +657,464 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
654
657
|
##
|
655
658
|
|
656
659
|
|
660
|
+
@dataclass(kw_only=True, slots=True)
|
661
|
+
class LooperError(Exception): ...
|
662
|
+
|
663
|
+
|
664
|
+
@dataclass(kw_only=True, slots=True)
|
665
|
+
class LooperTimeoutError(LooperError):
|
666
|
+
duration: Duration | None = None
|
667
|
+
|
668
|
+
@override
|
669
|
+
def __str__(self) -> str:
|
670
|
+
return "Timeout" if self.duration is None else f"Timeout after {self.duration}"
|
671
|
+
|
672
|
+
|
673
|
+
@dataclass(kw_only=True, slots=True)
|
674
|
+
class _LooperNoTaskError(LooperError):
|
675
|
+
looper: Looper
|
676
|
+
|
677
|
+
@override
|
678
|
+
def __str__(self) -> str:
|
679
|
+
return f"{self.looper} has no running task"
|
680
|
+
|
681
|
+
|
682
|
+
@dataclass(kw_only=True, unsafe_hash=True)
|
683
|
+
class Looper(Generic[_T]):
|
684
|
+
"""A looper of a core coroutine, handling errors."""
|
685
|
+
|
686
|
+
auto_start: bool = field(default=False, repr=False)
|
687
|
+
freq: Duration = field(default=SECOND, repr=False)
|
688
|
+
backoff: Duration = field(default=10 * SECOND, repr=False)
|
689
|
+
logger: str | None = field(default=None, repr=False)
|
690
|
+
timeout: Duration | None = field(default=None, repr=False)
|
691
|
+
timeout_error: type[Exception] = field(default=LooperTimeoutError, repr=False)
|
692
|
+
# settings
|
693
|
+
_backoff: float = field(init=False, repr=False)
|
694
|
+
_debug: bool = field(default=False, repr=False)
|
695
|
+
_freq: float = field(init=False, repr=False)
|
696
|
+
# counts
|
697
|
+
_entries: int = field(default=0, init=False, repr=False)
|
698
|
+
_core_attempts: int = field(default=0, init=False, repr=False)
|
699
|
+
_core_successes: int = field(default=0, init=False, repr=False)
|
700
|
+
_core_failures: int = field(default=0, init=False, repr=False)
|
701
|
+
_initialization_attempts: int = field(default=0, init=False, repr=False)
|
702
|
+
_initialization_successes: int = field(default=0, init=False, repr=False)
|
703
|
+
_initialization_failures: int = field(default=0, init=False, repr=False)
|
704
|
+
_tear_down_attempts: int = field(default=0, init=False, repr=False)
|
705
|
+
_tear_down_successes: int = field(default=0, init=False, repr=False)
|
706
|
+
_tear_down_failures: int = field(default=0, init=False, repr=False)
|
707
|
+
_restart_attempts: int = field(default=0, init=False, repr=False)
|
708
|
+
_restart_successes: int = field(default=0, init=False, repr=False)
|
709
|
+
_restart_failures: int = field(default=0, init=False, repr=False)
|
710
|
+
_stops: int = field(default=0, init=False, repr=False)
|
711
|
+
# flags
|
712
|
+
_is_entered: Event = field(default_factory=Event, init=False, repr=False)
|
713
|
+
_is_initialized: Event = field(default_factory=Event, init=False, repr=False)
|
714
|
+
_is_initializing: Event = field(default_factory=Event, init=False, repr=False)
|
715
|
+
_is_pending_restart: Event = field(default_factory=Event, init=False, repr=False)
|
716
|
+
_is_pending_stop: Event = field(default_factory=Event, init=False, repr=False)
|
717
|
+
_is_pending_stop_when_empty: Event = field(
|
718
|
+
default_factory=Event, init=False, repr=False
|
719
|
+
)
|
720
|
+
_is_stopped: Event = field(default_factory=Event, init=False, repr=False)
|
721
|
+
_is_tearing_down: Event = field(default_factory=Event, init=False, repr=False)
|
722
|
+
# internal objects
|
723
|
+
_lock: Lock = field(default_factory=Lock, init=False, repr=False, hash=False)
|
724
|
+
_logger: Logger = field(init=False, repr=False, hash=False)
|
725
|
+
_queue: EnhancedQueue[_T] = field(
|
726
|
+
default_factory=EnhancedQueue, init=False, repr=False, hash=False
|
727
|
+
)
|
728
|
+
_stack: AsyncExitStack = field(
|
729
|
+
default_factory=AsyncExitStack, init=False, repr=False, hash=False
|
730
|
+
)
|
731
|
+
_task: Task[None] | None = field(default=None, init=False, repr=False, hash=False)
|
732
|
+
|
733
|
+
def __post_init__(self) -> None:
|
734
|
+
self._backoff = datetime_duration_to_float(self.backoff)
|
735
|
+
self._freq = datetime_duration_to_float(self.freq)
|
736
|
+
self._logger = getLogger(name=self.logger)
|
737
|
+
self._logger.setLevel(DEBUG)
|
738
|
+
|
739
|
+
async def __aenter__(self) -> Self:
|
740
|
+
"""Enter the context manager."""
|
741
|
+
match self._is_entered.is_set():
|
742
|
+
case True:
|
743
|
+
_ = self._debug and self._logger.debug("%s: already entered", self)
|
744
|
+
case False:
|
745
|
+
_ = self._debug and self._logger.debug("%s: entering context...", self)
|
746
|
+
self._is_entered.set()
|
747
|
+
async with self._lock:
|
748
|
+
self._entries += 1
|
749
|
+
self._task = create_task(self.run_looper())
|
750
|
+
for looper in self._yield_sub_loopers():
|
751
|
+
_ = self._debug and self._logger.debug(
|
752
|
+
"%s: adding sub-looper %s", self, looper
|
753
|
+
)
|
754
|
+
if not looper.auto_start:
|
755
|
+
self._logger.warning(
|
756
|
+
"%s: changing sub-looper %s to auto-start...", self, looper
|
757
|
+
)
|
758
|
+
async with self._lock:
|
759
|
+
looper.auto_start = True
|
760
|
+
_ = await self._stack.enter_async_context(looper)
|
761
|
+
if self.auto_start:
|
762
|
+
_ = self._debug and self._logger.debug("%s: auto-starting...", self)
|
763
|
+
with suppress(self.timeout_error):
|
764
|
+
await self._task
|
765
|
+
case _ as never:
|
766
|
+
assert_never(never)
|
767
|
+
return self
|
768
|
+
|
769
|
+
async def __aexit__(
|
770
|
+
self,
|
771
|
+
exc_type: type[BaseException] | None = None,
|
772
|
+
exc_value: BaseException | None = None,
|
773
|
+
traceback: TracebackType | None = None,
|
774
|
+
) -> None:
|
775
|
+
"""Exit the context manager."""
|
776
|
+
match self._is_entered.is_set():
|
777
|
+
case True:
|
778
|
+
_ = self._debug and self._logger.debug("%s: exiting context...", self)
|
779
|
+
self._is_entered.clear()
|
780
|
+
if (
|
781
|
+
(exc_type is not None)
|
782
|
+
and (exc_value is not None)
|
783
|
+
and (traceback is not None)
|
784
|
+
):
|
785
|
+
_ = self._debug and self._logger.warning(
|
786
|
+
"%s: encountered %s whilst in context",
|
787
|
+
self,
|
788
|
+
repr_error(exc_value),
|
789
|
+
)
|
790
|
+
_ = await self._stack.__aexit__(exc_type, exc_value, traceback)
|
791
|
+
await self.stop()
|
792
|
+
case False:
|
793
|
+
_ = self._debug and self._logger.debug("%s: already exited", self)
|
794
|
+
case _ as never:
|
795
|
+
assert_never(never)
|
796
|
+
|
797
|
+
def __await__(self) -> Any:
|
798
|
+
match self._task:
|
799
|
+
case None:
|
800
|
+
raise _LooperNoTaskError(looper=self)
|
801
|
+
case Task() as task:
|
802
|
+
return task.__await__()
|
803
|
+
case _ as never:
|
804
|
+
assert_never(never)
|
805
|
+
|
806
|
+
def __len__(self) -> int:
|
807
|
+
return self._queue.qsize()
|
808
|
+
|
809
|
+
async def core(self) -> None:
|
810
|
+
"""Core part of running the looper."""
|
811
|
+
|
812
|
+
def empty(self) -> bool:
|
813
|
+
"""Check if the queue is empty."""
|
814
|
+
return self._queue.empty()
|
815
|
+
|
816
|
+
def get_all_nowait(self, *, reverse: bool = False) -> Sequence[_T]:
|
817
|
+
"""Remove and return all items from the queue without blocking."""
|
818
|
+
return self._queue.get_all_nowait(reverse=reverse)
|
819
|
+
|
820
|
+
def get_left_nowait(self) -> _T:
|
821
|
+
"""Remove and return an item from the start of the queue without blocking."""
|
822
|
+
return self._queue.get_left_nowait()
|
823
|
+
|
824
|
+
def get_right_nowait(self) -> _T:
|
825
|
+
"""Remove and return an item from the end of the queue without blocking."""
|
826
|
+
return self._queue.get_right_nowait()
|
827
|
+
|
828
|
+
async def initialize(self) -> Exception | None:
|
829
|
+
"""Initialize the looper."""
|
830
|
+
match self._is_initializing.is_set():
|
831
|
+
case True:
|
832
|
+
_ = self._debug and self._logger.debug("%s: already initializing", self)
|
833
|
+
return None
|
834
|
+
case False:
|
835
|
+
_ = self._debug and self._logger.debug("%s: initializing...", self)
|
836
|
+
self._is_initializing.set()
|
837
|
+
self._is_initialized.clear()
|
838
|
+
async with self._lock:
|
839
|
+
self._initialization_attempts += 1
|
840
|
+
try:
|
841
|
+
await self._initialize_core()
|
842
|
+
except Exception as error: # noqa: BLE001
|
843
|
+
_ = self._logger.warning(
|
844
|
+
"%s: encountered %s whilst initializing",
|
845
|
+
self,
|
846
|
+
repr_error(error),
|
847
|
+
)
|
848
|
+
async with self._lock:
|
849
|
+
self._initialization_failures += 1
|
850
|
+
ret = error
|
851
|
+
else:
|
852
|
+
_ = self._debug and self._logger.debug(
|
853
|
+
"%s: finished initializing", self
|
854
|
+
)
|
855
|
+
self._is_initialized.set()
|
856
|
+
async with self._lock:
|
857
|
+
self._initialization_successes += 1
|
858
|
+
ret = None
|
859
|
+
finally:
|
860
|
+
self._is_initializing.clear()
|
861
|
+
return ret
|
862
|
+
case _ as never:
|
863
|
+
assert_never(never)
|
864
|
+
|
865
|
+
async def _initialize_core(self) -> None:
|
866
|
+
"""Core part of initializing the looper."""
|
867
|
+
|
868
|
+
def put_left_nowait(self, *items: _T) -> None:
|
869
|
+
"""Put items into the queue at the start without blocking."""
|
870
|
+
self._queue.put_left_nowait(*items)
|
871
|
+
|
872
|
+
def put_right_nowait(self, *items: _T) -> None:
|
873
|
+
"""Put items into the queue at the end without blocking."""
|
874
|
+
self._queue.put_right_nowait(*items)
|
875
|
+
|
876
|
+
def qsize(self) -> int:
|
877
|
+
"""Get the number of items in the queue."""
|
878
|
+
return self._queue.qsize()
|
879
|
+
|
880
|
+
def replace(
|
881
|
+
self,
|
882
|
+
*,
|
883
|
+
auto_start: bool | Sentinel = sentinel,
|
884
|
+
freq: Duration | Sentinel = sentinel,
|
885
|
+
backoff: Duration | Sentinel = sentinel,
|
886
|
+
logger: str | None | Sentinel = sentinel,
|
887
|
+
timeout: Duration | None | Sentinel = sentinel,
|
888
|
+
) -> Self:
|
889
|
+
"""Replace elements of the looper."""
|
890
|
+
return replace_non_sentinel(
|
891
|
+
self,
|
892
|
+
auto_start=auto_start,
|
893
|
+
freq=freq,
|
894
|
+
backoff=backoff,
|
895
|
+
logger=logger,
|
896
|
+
timeout=timeout,
|
897
|
+
)
|
898
|
+
|
899
|
+
def request_restart(self) -> None:
|
900
|
+
"""Request the looper to restart."""
|
901
|
+
match self._is_pending_restart.is_set():
|
902
|
+
case True:
|
903
|
+
_ = self._debug and self._logger.debug(
|
904
|
+
"%s: already requested restart", self
|
905
|
+
)
|
906
|
+
case False:
|
907
|
+
_ = self._debug and self._logger.debug(
|
908
|
+
"%s: requesting restart...", self
|
909
|
+
)
|
910
|
+
self._is_pending_restart.set()
|
911
|
+
case _ as never:
|
912
|
+
assert_never(never)
|
913
|
+
|
914
|
+
def request_stop(self) -> None:
|
915
|
+
"""Request the looper to stop."""
|
916
|
+
match self._is_pending_stop.is_set():
|
917
|
+
case True:
|
918
|
+
_ = self._debug and self._logger.debug(
|
919
|
+
"%s: already requested stop", self
|
920
|
+
)
|
921
|
+
case False:
|
922
|
+
_ = self._debug and self._logger.debug("%s: requesting stop...", self)
|
923
|
+
self._is_pending_stop.set()
|
924
|
+
case _ as never:
|
925
|
+
assert_never(never)
|
926
|
+
|
927
|
+
def request_stop_when_empty(self) -> None:
|
928
|
+
"""Request the looper to stop when the queue is empty."""
|
929
|
+
match self._is_pending_stop_when_empty.is_set():
|
930
|
+
case True:
|
931
|
+
_ = self._debug and self._logger.debug(
|
932
|
+
"%s: already requested stop when empty", self
|
933
|
+
)
|
934
|
+
case False:
|
935
|
+
_ = self._debug and self._logger.debug(
|
936
|
+
"%s: requesting stop when empty...", self
|
937
|
+
)
|
938
|
+
self._is_pending_stop_when_empty.set()
|
939
|
+
case _ as never:
|
940
|
+
assert_never(never)
|
941
|
+
|
942
|
+
async def restart(self) -> None:
|
943
|
+
"""Restart the looper."""
|
944
|
+
_ = self._debug and self._logger.debug("%s: restarting...", self)
|
945
|
+
self._is_pending_restart.clear()
|
946
|
+
async with self._lock:
|
947
|
+
self._restart_attempts += 1
|
948
|
+
tear_down = await self.tear_down()
|
949
|
+
initialization = await self.initialize()
|
950
|
+
match tear_down, initialization:
|
951
|
+
case None, None:
|
952
|
+
_ = self._debug and self._logger.debug("%s: finished restarting", self)
|
953
|
+
async with self._lock:
|
954
|
+
self._restart_successes += 1
|
955
|
+
case Exception(), None:
|
956
|
+
_ = self._logger.warning(
|
957
|
+
"%s: encountered %s whilst restarting, during tear down",
|
958
|
+
self,
|
959
|
+
repr_error(tear_down),
|
960
|
+
)
|
961
|
+
async with self._lock:
|
962
|
+
self._restart_failures += 1
|
963
|
+
case None, Exception():
|
964
|
+
_ = self._logger.warning(
|
965
|
+
"%s: encountered %s whilst restarting, during initialization",
|
966
|
+
self,
|
967
|
+
repr_error(initialization),
|
968
|
+
)
|
969
|
+
async with self._lock:
|
970
|
+
self._restart_failures += 1
|
971
|
+
case Exception(), Exception():
|
972
|
+
_ = self._logger.warning(
|
973
|
+
"%s: encountered %s (tear down) and then %s (initialization) whilst restarting",
|
974
|
+
self,
|
975
|
+
repr_error(tear_down),
|
976
|
+
repr_error(initialization),
|
977
|
+
)
|
978
|
+
async with self._lock:
|
979
|
+
self._restart_failures += 1
|
980
|
+
case _ as never:
|
981
|
+
assert_never(never)
|
982
|
+
|
983
|
+
async def run_looper(self) -> None:
|
984
|
+
"""Run the looper."""
|
985
|
+
async with timeout_dur(duration=self.timeout, error=self.timeout_error):
|
986
|
+
while True:
|
987
|
+
if self._is_stopped.is_set():
|
988
|
+
_ = self._debug and self._logger.debug("%s: stopped", self)
|
989
|
+
return
|
990
|
+
if (self._is_pending_stop.is_set()) or (
|
991
|
+
self._is_pending_stop_when_empty.is_set() and self.empty()
|
992
|
+
):
|
993
|
+
await self.stop()
|
994
|
+
elif self._is_pending_restart.is_set():
|
995
|
+
await self.restart()
|
996
|
+
elif not self._is_initialized.is_set():
|
997
|
+
_ = await self.initialize()
|
998
|
+
else:
|
999
|
+
_ = self._debug and self._logger.debug("%s: running core...", self)
|
1000
|
+
async with self._lock:
|
1001
|
+
self._core_attempts += 1
|
1002
|
+
try:
|
1003
|
+
await self.core()
|
1004
|
+
except Exception as error: # noqa: BLE001
|
1005
|
+
_ = self._logger.warning(
|
1006
|
+
"%s: encountered %s whilst running core...",
|
1007
|
+
self,
|
1008
|
+
repr_error(error),
|
1009
|
+
)
|
1010
|
+
async with self._lock:
|
1011
|
+
self._core_failures += 1
|
1012
|
+
self.request_restart()
|
1013
|
+
await sleep(self._backoff)
|
1014
|
+
else:
|
1015
|
+
async with self._lock:
|
1016
|
+
self._core_successes += 1
|
1017
|
+
await sleep(self._freq)
|
1018
|
+
|
1019
|
+
@property
|
1020
|
+
def stats(self) -> _LooperStats:
|
1021
|
+
"""Return the statistics."""
|
1022
|
+
return _LooperStats(
|
1023
|
+
entries=self._entries,
|
1024
|
+
core_attempts=self._core_attempts,
|
1025
|
+
core_successes=self._core_successes,
|
1026
|
+
core_failures=self._core_failures,
|
1027
|
+
initialization_attempts=self._initialization_attempts,
|
1028
|
+
initialization_successes=self._initialization_successes,
|
1029
|
+
initialization_failures=self._initialization_failures,
|
1030
|
+
tear_down_attempts=self._tear_down_attempts,
|
1031
|
+
tear_down_successes=self._tear_down_successes,
|
1032
|
+
tear_down_failures=self._tear_down_failures,
|
1033
|
+
restart_attempts=self._restart_attempts,
|
1034
|
+
restart_successes=self._restart_successes,
|
1035
|
+
restart_failures=self._restart_failures,
|
1036
|
+
stops=self._stops,
|
1037
|
+
)
|
1038
|
+
|
1039
|
+
async def stop(self) -> None:
|
1040
|
+
"""Stop the looper."""
|
1041
|
+
match self._is_stopped.is_set():
|
1042
|
+
case True:
|
1043
|
+
_ = self._debug and self._logger.debug("%s: already stopped", self)
|
1044
|
+
case False:
|
1045
|
+
_ = self._debug and self._logger.debug("%s: stopping...", self)
|
1046
|
+
self._is_pending_stop.clear()
|
1047
|
+
self._is_stopped.set()
|
1048
|
+
async with self._lock:
|
1049
|
+
self._stops += 1
|
1050
|
+
_ = self._debug and self._logger.debug("%s: stopped", self)
|
1051
|
+
case _ as never:
|
1052
|
+
assert_never(never)
|
1053
|
+
|
1054
|
+
async def tear_down(self) -> Exception | None:
|
1055
|
+
"""Tear down the looper."""
|
1056
|
+
match self._is_tearing_down.is_set():
|
1057
|
+
case True:
|
1058
|
+
_ = self._debug and self._logger.debug("%s: already tearing down", self)
|
1059
|
+
return None
|
1060
|
+
case False:
|
1061
|
+
_ = self._debug and self._logger.debug("%s: tearing down...", self)
|
1062
|
+
self._is_tearing_down.set()
|
1063
|
+
async with self._lock:
|
1064
|
+
self._tear_down_attempts += 1
|
1065
|
+
try:
|
1066
|
+
await self._tear_down_core()
|
1067
|
+
except Exception as error: # noqa: BLE001
|
1068
|
+
_ = self._logger.warning(
|
1069
|
+
"%s: encountered %s whilst tearing down",
|
1070
|
+
self,
|
1071
|
+
repr_error(error),
|
1072
|
+
)
|
1073
|
+
async with self._lock:
|
1074
|
+
self._tear_down_failures += 1
|
1075
|
+
ret = error
|
1076
|
+
else:
|
1077
|
+
_ = self._debug and self._logger.debug(
|
1078
|
+
"%s: finished tearing down", self
|
1079
|
+
)
|
1080
|
+
async with self._lock:
|
1081
|
+
self._tear_down_successes += 1
|
1082
|
+
ret = None
|
1083
|
+
finally:
|
1084
|
+
self._is_tearing_down.clear()
|
1085
|
+
return ret
|
1086
|
+
case _ as never:
|
1087
|
+
assert_never(never)
|
1088
|
+
|
1089
|
+
async def _tear_down_core(self) -> None:
|
1090
|
+
"""Core part of tearing down the looper."""
|
1091
|
+
|
1092
|
+
def _yield_sub_loopers(self) -> Iterator[Looper]:
|
1093
|
+
"""Yield all sub-loopers."""
|
1094
|
+
yield from []
|
1095
|
+
|
1096
|
+
|
1097
|
+
@dataclass(kw_only=True, slots=True)
|
1098
|
+
class _LooperStats:
|
1099
|
+
entries: int = 0
|
1100
|
+
core_attempts: int = 0
|
1101
|
+
core_successes: int = 0
|
1102
|
+
core_failures: int = 0
|
1103
|
+
initialization_attempts: int = 0
|
1104
|
+
initialization_successes: int = 0
|
1105
|
+
initialization_failures: int = 0
|
1106
|
+
tear_down_attempts: int = 0
|
1107
|
+
tear_down_successes: int = 0
|
1108
|
+
tear_down_failures: int = 0
|
1109
|
+
restart_attempts: int = 0
|
1110
|
+
restart_successes: int = 0
|
1111
|
+
restart_failures: int = 0
|
1112
|
+
stops: int = 0
|
1113
|
+
|
1114
|
+
|
1115
|
+
##
|
1116
|
+
|
1117
|
+
|
657
1118
|
class UniquePriorityQueue(PriorityQueue[tuple[TSupportsRichComparison, THashable]]):
|
658
1119
|
"""Priority queue with unique tasks."""
|
659
1120
|
|
@@ -878,6 +1339,9 @@ __all__ = [
|
|
878
1339
|
"InfiniteLooper",
|
879
1340
|
"InfiniteLooperError",
|
880
1341
|
"InfiniteQueueLooper",
|
1342
|
+
"Looper",
|
1343
|
+
"LooperError",
|
1344
|
+
"LooperTimeoutError",
|
881
1345
|
"StreamCommandOutput",
|
882
1346
|
"UniquePriorityQueue",
|
883
1347
|
"UniqueQueue",
|
File without changes
|
File without changes
|