dycw-utilities 0.126.12__py3-none-any.whl → 0.129.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.126.12.dist-info → dycw_utilities-0.129.13.dist-info}/METADATA +16 -10
- {dycw_utilities-0.126.12.dist-info → dycw_utilities-0.129.13.dist-info}/RECORD +24 -23
- utilities/__init__.py +1 -1
- utilities/aiolimiter.py +25 -0
- utilities/asyncio.py +62 -426
- utilities/datetime.py +0 -8
- utilities/fastapi.py +26 -12
- utilities/git.py +5 -58
- utilities/hypothesis.py +1 -11
- utilities/logging.py +69 -56
- utilities/pathlib.py +83 -13
- utilities/pyinstrument.py +6 -4
- utilities/pytest_regressions.py +2 -2
- utilities/python_dotenv.py +10 -6
- utilities/redis.py +5 -58
- utilities/scipy.py +1 -1
- utilities/slack_sdk.py +2 -54
- utilities/sqlalchemy.py +2 -65
- utilities/traceback.py +278 -12
- utilities/types.py +2 -2
- utilities/version.py +0 -8
- utilities/whenever.py +64 -1
- {dycw_utilities-0.126.12.dist-info → dycw_utilities-0.129.13.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.126.12.dist-info → dycw_utilities-0.129.13.dist-info}/licenses/LICENSE +0 -0
utilities/asyncio.py
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import datetime as dt
|
4
|
-
from abc import ABC, abstractmethod
|
5
3
|
from asyncio import (
|
6
|
-
CancelledError,
|
7
4
|
Event,
|
8
5
|
Lock,
|
9
6
|
PriorityQueue,
|
@@ -19,7 +16,7 @@ from asyncio import (
|
|
19
16
|
sleep,
|
20
17
|
timeout,
|
21
18
|
)
|
22
|
-
from collections.abc import Callable,
|
19
|
+
from collections.abc import Callable, Iterable, Iterator
|
23
20
|
from contextlib import (
|
24
21
|
AbstractAsyncContextManager,
|
25
22
|
AsyncExitStack,
|
@@ -37,7 +34,6 @@ from typing import (
|
|
37
34
|
TYPE_CHECKING,
|
38
35
|
Any,
|
39
36
|
Generic,
|
40
|
-
NoReturn,
|
41
37
|
Self,
|
42
38
|
TextIO,
|
43
39
|
TypeVar,
|
@@ -50,27 +46,19 @@ from typing_extensions import deprecated
|
|
50
46
|
|
51
47
|
from utilities.dataclasses import replace_non_sentinel
|
52
48
|
from utilities.datetime import (
|
53
|
-
MINUTE,
|
54
49
|
SECOND,
|
55
50
|
datetime_duration_to_float,
|
56
|
-
datetime_duration_to_timedelta,
|
57
51
|
get_now,
|
58
52
|
round_datetime,
|
59
53
|
)
|
60
|
-
from utilities.errors import
|
61
|
-
from utilities.functions import ensure_int, ensure_not_none
|
54
|
+
from utilities.errors import repr_error
|
55
|
+
from utilities.functions import ensure_int, ensure_not_none
|
62
56
|
from utilities.random import SYSTEM_RANDOM
|
63
57
|
from utilities.sentinel import Sentinel, sentinel
|
64
|
-
from utilities.types import
|
65
|
-
Coroutine1,
|
66
|
-
DurationOrEveryDuration,
|
67
|
-
MaybeCallableEvent,
|
68
|
-
MaybeType,
|
69
|
-
THashable,
|
70
|
-
TSupportsRichComparison,
|
71
|
-
)
|
58
|
+
from utilities.types import MaybeCallableEvent, THashable, TSupportsRichComparison
|
72
59
|
|
73
60
|
if TYPE_CHECKING:
|
61
|
+
import datetime as dt
|
74
62
|
from asyncio import _CoroutineLike
|
75
63
|
from asyncio.subprocess import Process
|
76
64
|
from collections import deque
|
@@ -319,359 +307,10 @@ class EnhancedTaskGroup(TaskGroup):
|
|
319
307
|
##
|
320
308
|
|
321
309
|
|
322
|
-
@dataclass(kw_only=True, unsafe_hash=True)
|
323
|
-
class InfiniteLooper(ABC, Generic[THashable]):
|
324
|
-
"""An infinite loop which can throw exceptions by setting events."""
|
325
|
-
|
326
|
-
sleep_core: DurationOrEveryDuration = field(default=SECOND, repr=False)
|
327
|
-
sleep_restart: DurationOrEveryDuration = field(default=MINUTE, repr=False)
|
328
|
-
duration: Duration | None = field(default=None, repr=False)
|
329
|
-
logger: str | None = field(default=None, repr=False)
|
330
|
-
_await_upon_aenter: bool = field(default=True, init=False, repr=False)
|
331
|
-
_depth: int = field(default=0, init=False, repr=False)
|
332
|
-
_events: Mapping[THashable | None, Event] = field(
|
333
|
-
default_factory=dict, init=False, repr=False, hash=False
|
334
|
-
)
|
335
|
-
_stack: AsyncExitStack = field(
|
336
|
-
default_factory=AsyncExitStack, init=False, repr=False
|
337
|
-
)
|
338
|
-
_task: Task[None] | None = field(default=None, init=False, repr=False)
|
339
|
-
|
340
|
-
def __post_init__(self) -> None:
|
341
|
-
self._events = {
|
342
|
-
event: Event() for event, _ in self._yield_events_and_exceptions()
|
343
|
-
}
|
344
|
-
|
345
|
-
async def __aenter__(self) -> Self:
|
346
|
-
"""Context manager entry."""
|
347
|
-
if self._depth == 0:
|
348
|
-
self._task = create_task(self._run_looper())
|
349
|
-
if self._await_upon_aenter:
|
350
|
-
with suppress(CancelledError):
|
351
|
-
await self._task
|
352
|
-
_ = await self._stack.__aenter__()
|
353
|
-
self._depth += 1
|
354
|
-
return self
|
355
|
-
|
356
|
-
async def __aexit__(
|
357
|
-
self,
|
358
|
-
exc_type: type[BaseException] | None = None,
|
359
|
-
exc_value: BaseException | None = None,
|
360
|
-
traceback: TracebackType | None = None,
|
361
|
-
) -> None:
|
362
|
-
"""Context manager exit."""
|
363
|
-
_ = (exc_type, exc_value, traceback)
|
364
|
-
self._depth = max(self._depth - 1, 0)
|
365
|
-
if (self._depth == 0) and (self._task is not None):
|
366
|
-
with suppress(CancelledError):
|
367
|
-
await self._task
|
368
|
-
self._task = None
|
369
|
-
try:
|
370
|
-
await self._teardown()
|
371
|
-
except Exception as error: # noqa: BLE001
|
372
|
-
self._error_upon_teardown(error)
|
373
|
-
_ = await self._stack.__aexit__(exc_type, exc_value, traceback)
|
374
|
-
|
375
|
-
async def stop(self) -> None:
|
376
|
-
"""Stop the service."""
|
377
|
-
if self._task is None:
|
378
|
-
raise ImpossibleCaseError(case=[f"{self._task=}"]) # pragma: no cover
|
379
|
-
with suppress(CancelledError):
|
380
|
-
_ = self._task.cancel()
|
381
|
-
|
382
|
-
async def _run_looper(self) -> None:
|
383
|
-
"""Run the looper."""
|
384
|
-
match self.duration:
|
385
|
-
case None:
|
386
|
-
await self._run_looper_without_timeout()
|
387
|
-
case int() | float() | dt.timedelta() as duration:
|
388
|
-
try:
|
389
|
-
async with timeout_dur(duration=duration):
|
390
|
-
return await self._run_looper_without_timeout()
|
391
|
-
except TimeoutError:
|
392
|
-
await self.stop()
|
393
|
-
case _ as never:
|
394
|
-
assert_never(never)
|
395
|
-
return None
|
396
|
-
|
397
|
-
async def _run_looper_without_timeout(self) -> None:
|
398
|
-
"""Run the looper without a timeout."""
|
399
|
-
coroutines = list(self._yield_coroutines())
|
400
|
-
loopers = list(self._yield_loopers())
|
401
|
-
if (len(coroutines) == 0) and (len(loopers) == 0):
|
402
|
-
return await self._run_looper_by_itself()
|
403
|
-
return await self._run_looper_with_others(coroutines, loopers)
|
404
|
-
|
405
|
-
async def _run_looper_by_itself(self) -> None:
|
406
|
-
"""Run the looper by itself."""
|
407
|
-
whitelisted = tuple(self._yield_whitelisted_errors())
|
408
|
-
blacklisted = tuple(self._yield_blacklisted_errors())
|
409
|
-
while True:
|
410
|
-
try:
|
411
|
-
self._reset_events()
|
412
|
-
try:
|
413
|
-
await self._initialize()
|
414
|
-
except Exception as error: # noqa: BLE001
|
415
|
-
self._error_upon_initialize(error)
|
416
|
-
await self._run_sleep(self.sleep_restart)
|
417
|
-
else:
|
418
|
-
while True:
|
419
|
-
try:
|
420
|
-
event = next(
|
421
|
-
key
|
422
|
-
for (key, value) in self._events.items()
|
423
|
-
if value.is_set()
|
424
|
-
)
|
425
|
-
except StopIteration:
|
426
|
-
await self._core()
|
427
|
-
await self._run_sleep(self.sleep_core)
|
428
|
-
else:
|
429
|
-
self._raise_error(event)
|
430
|
-
except InfiniteLooperError:
|
431
|
-
raise
|
432
|
-
except BaseException as error1:
|
433
|
-
match error1:
|
434
|
-
case Exception():
|
435
|
-
if isinstance(error1, blacklisted):
|
436
|
-
raise
|
437
|
-
case BaseException():
|
438
|
-
if not isinstance(error1, whitelisted):
|
439
|
-
raise
|
440
|
-
case _ as never:
|
441
|
-
assert_never(never)
|
442
|
-
self._error_upon_core(error1)
|
443
|
-
try:
|
444
|
-
await self._teardown()
|
445
|
-
except BaseException as error2: # noqa: BLE001
|
446
|
-
self._error_upon_teardown(error2)
|
447
|
-
finally:
|
448
|
-
await self._run_sleep(self.sleep_restart)
|
449
|
-
|
450
|
-
async def _run_looper_with_others(
|
451
|
-
self,
|
452
|
-
coroutines: Iterable[Callable[[], Coroutine1[None]]],
|
453
|
-
loopers: Iterable[InfiniteLooper[Any]],
|
454
|
-
/,
|
455
|
-
) -> None:
|
456
|
-
"""Run multiple loopers."""
|
457
|
-
while True:
|
458
|
-
self._reset_events()
|
459
|
-
try:
|
460
|
-
async with TaskGroup() as tg, AsyncExitStack() as stack:
|
461
|
-
_ = tg.create_task(self._run_looper_by_itself())
|
462
|
-
_ = [tg.create_task(c()) for c in coroutines]
|
463
|
-
_ = [
|
464
|
-
tg.create_task(stack.enter_async_context(lo)) for lo in loopers
|
465
|
-
]
|
466
|
-
except ExceptionGroup as error:
|
467
|
-
self._error_group_upon_others(error)
|
468
|
-
await self._run_sleep(self.sleep_restart)
|
469
|
-
|
470
|
-
async def _initialize(self) -> None:
|
471
|
-
"""Initialize the loop."""
|
472
|
-
|
473
|
-
async def _core(self) -> None:
|
474
|
-
"""Run the core part of the loop."""
|
475
|
-
|
476
|
-
async def _teardown(self) -> None:
|
477
|
-
"""Tear down the loop."""
|
478
|
-
|
479
|
-
def _error_upon_initialize(self, error: Exception, /) -> None:
|
480
|
-
"""Handle any errors upon initializing the looper."""
|
481
|
-
if self.logger is not None:
|
482
|
-
getLogger(name=self.logger).error(
|
483
|
-
"%r encountered %r whilst initializing; sleeping %s...",
|
484
|
-
get_class_name(self),
|
485
|
-
repr_error(error),
|
486
|
-
self._sleep_restart_desc,
|
487
|
-
)
|
488
|
-
|
489
|
-
def _error_upon_core(self, error: BaseException, /) -> None:
|
490
|
-
"""Handle any errors upon running the core function."""
|
491
|
-
if self.logger is not None:
|
492
|
-
getLogger(name=self.logger).error(
|
493
|
-
"%r encountered %r; sleeping %s...",
|
494
|
-
get_class_name(self),
|
495
|
-
repr_error(error),
|
496
|
-
self._sleep_restart_desc,
|
497
|
-
)
|
498
|
-
|
499
|
-
def _error_upon_teardown(self, error: BaseException, /) -> None:
|
500
|
-
"""Handle any errors upon tearing down the looper."""
|
501
|
-
if self.logger is not None:
|
502
|
-
getLogger(name=self.logger).error(
|
503
|
-
"%r encountered %r whilst tearing down; sleeping %s...",
|
504
|
-
get_class_name(self),
|
505
|
-
repr_error(error),
|
506
|
-
self._sleep_restart_desc,
|
507
|
-
)
|
508
|
-
|
509
|
-
def _error_group_upon_others(self, group: ExceptionGroup, /) -> None:
|
510
|
-
"""Handle any errors upon running the core function."""
|
511
|
-
if self.logger is not None:
|
512
|
-
errors = group.exceptions
|
513
|
-
n = len(errors)
|
514
|
-
msgs = [f"{get_class_name(self)!r} encountered {n} error(s):"]
|
515
|
-
msgs.extend(
|
516
|
-
f"- Error #{i}/{n}: {repr_error(e)}"
|
517
|
-
for i, e in enumerate(errors, start=1)
|
518
|
-
)
|
519
|
-
msgs.append(f"Sleeping {self._sleep_restart_desc}...")
|
520
|
-
getLogger(name=self.logger).error("\n".join(msgs))
|
521
|
-
|
522
|
-
def _raise_error(self, event: THashable | None, /) -> NoReturn:
|
523
|
-
"""Raise the error corresponding to given event."""
|
524
|
-
mapping = dict(self._yield_events_and_exceptions())
|
525
|
-
error = mapping.get(event, InfiniteLooperError)
|
526
|
-
raise error
|
527
|
-
|
528
|
-
def _reset_events(self) -> None:
|
529
|
-
"""Reset the events."""
|
530
|
-
self._events = {
|
531
|
-
event: Event() for event, _ in self._yield_events_and_exceptions()
|
532
|
-
}
|
533
|
-
|
534
|
-
async def _run_sleep(self, sleep: DurationOrEveryDuration, /) -> None:
|
535
|
-
"""Sleep until the next part of the loop."""
|
536
|
-
match sleep:
|
537
|
-
case int() | float() | dt.timedelta() as duration:
|
538
|
-
await sleep_dur(duration=duration)
|
539
|
-
case "every", (int() | float() | dt.timedelta()) as duration:
|
540
|
-
await sleep_until_rounded(duration)
|
541
|
-
case _ as never:
|
542
|
-
assert_never(never)
|
543
|
-
|
544
|
-
@property
|
545
|
-
def _sleep_restart_desc(self) -> str:
|
546
|
-
"""Get a description of the sleep until restart."""
|
547
|
-
match self.sleep_restart:
|
548
|
-
case int() | float() | dt.timedelta() as duration:
|
549
|
-
timedelta = datetime_duration_to_timedelta(duration)
|
550
|
-
return f"for {timedelta}"
|
551
|
-
case "every", (int() | float() | dt.timedelta()) as duration:
|
552
|
-
timedelta = datetime_duration_to_timedelta(duration)
|
553
|
-
return f"until next {timedelta}"
|
554
|
-
case _ as never:
|
555
|
-
assert_never(never)
|
556
|
-
|
557
|
-
def _set_event(self, *, event: THashable | None = None) -> None:
|
558
|
-
"""Set the given event."""
|
559
|
-
try:
|
560
|
-
event_obj = self._events[event]
|
561
|
-
except KeyError:
|
562
|
-
raise _InfiniteLooperNoSuchEventError(looper=self, event=event) from None
|
563
|
-
event_obj.set()
|
564
|
-
|
565
|
-
def _yield_events_and_exceptions(
|
566
|
-
self,
|
567
|
-
) -> Iterator[tuple[THashable | None, MaybeType[Exception]]]:
|
568
|
-
"""Yield the events & exceptions."""
|
569
|
-
yield (None, _InfiniteLooperDefaultEventError(looper=self))
|
570
|
-
|
571
|
-
def _yield_coroutines(self) -> Iterator[Callable[[], Coroutine1[None]]]:
|
572
|
-
"""Yield any other coroutines which must also be run."""
|
573
|
-
yield from []
|
574
|
-
|
575
|
-
def _yield_loopers(self) -> Iterator[InfiniteLooper[Any]]:
|
576
|
-
"""Yield any other loopers which must also be run."""
|
577
|
-
yield from []
|
578
|
-
|
579
|
-
def _yield_blacklisted_errors(self) -> Iterator[type[Exception]]:
|
580
|
-
"""Yield any exceptions which the looper ought to catch terminate upon."""
|
581
|
-
yield from []
|
582
|
-
|
583
|
-
def _yield_whitelisted_errors(self) -> Iterator[type[BaseException]]:
|
584
|
-
"""Yield any exceptions which the looper ought to catch and allow running."""
|
585
|
-
yield from []
|
586
|
-
|
587
|
-
|
588
|
-
@dataclass(kw_only=True, slots=True)
|
589
|
-
class InfiniteLooperError(Exception):
|
590
|
-
looper: InfiniteLooper[Any]
|
591
|
-
|
592
|
-
|
593
|
-
@dataclass(kw_only=True, slots=True)
|
594
|
-
class _InfiniteLooperNoSuchEventError(InfiniteLooperError):
|
595
|
-
event: Hashable
|
596
|
-
|
597
|
-
@override
|
598
|
-
def __str__(self) -> str:
|
599
|
-
return f"{get_class_name(self.looper)!r} does not have an event {self.event!r}"
|
600
|
-
|
601
|
-
|
602
|
-
@dataclass(kw_only=True, slots=True)
|
603
|
-
class _InfiniteLooperDefaultEventError(InfiniteLooperError):
|
604
|
-
@override
|
605
|
-
def __str__(self) -> str:
|
606
|
-
return f"{get_class_name(self.looper)!r} default event error"
|
607
|
-
|
608
|
-
|
609
|
-
##
|
610
|
-
|
611
|
-
|
612
|
-
@dataclass(kw_only=True)
|
613
|
-
class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
614
|
-
"""An infinite loop which processes a queue."""
|
615
|
-
|
616
|
-
_await_upon_aenter: bool = field(default=False, init=False, repr=False)
|
617
|
-
_queue: EnhancedQueue[_T] = field(
|
618
|
-
default_factory=EnhancedQueue, init=False, repr=False
|
619
|
-
)
|
620
|
-
|
621
|
-
def __len__(self) -> int:
|
622
|
-
return self._queue.qsize()
|
623
|
-
|
624
|
-
@override
|
625
|
-
async def _core(self) -> None:
|
626
|
-
"""Run the core part of the loop."""
|
627
|
-
if self.empty():
|
628
|
-
return
|
629
|
-
await self._process_queue()
|
630
|
-
|
631
|
-
@abstractmethod
|
632
|
-
async def _process_queue(self) -> None:
|
633
|
-
"""Process the queue."""
|
634
|
-
|
635
|
-
def empty(self) -> bool:
|
636
|
-
"""Check if the queue is empty."""
|
637
|
-
return self._queue.empty()
|
638
|
-
|
639
|
-
def put_left_nowait(self, *items: _T) -> None:
|
640
|
-
"""Put items into the queue at the start without blocking."""
|
641
|
-
self._queue.put_left_nowait(*items) # pragma: no cover
|
642
|
-
|
643
|
-
def put_right_nowait(self, *items: _T) -> None:
|
644
|
-
"""Put items into the queue at the end without blocking."""
|
645
|
-
self._queue.put_right_nowait(*items) # pragma: no cover
|
646
|
-
|
647
|
-
def qsize(self) -> int:
|
648
|
-
"""Get the number of items in the queue."""
|
649
|
-
return self._queue.qsize()
|
650
|
-
|
651
|
-
async def run_until_empty(self, *, stop: bool = False) -> None:
|
652
|
-
"""Run until the queue is empty."""
|
653
|
-
while not self.empty():
|
654
|
-
await self._process_queue()
|
655
|
-
if stop:
|
656
|
-
await self.stop()
|
657
|
-
|
658
|
-
|
659
|
-
##
|
660
|
-
|
661
|
-
|
662
310
|
@dataclass(kw_only=True, slots=True)
|
663
311
|
class LooperError(Exception): ...
|
664
312
|
|
665
313
|
|
666
|
-
@dataclass(kw_only=True, slots=True)
|
667
|
-
class LooperTimeoutError(LooperError):
|
668
|
-
duration: Duration | None = None
|
669
|
-
|
670
|
-
@override
|
671
|
-
def __str__(self) -> str:
|
672
|
-
return "Timeout" if self.duration is None else f"Timeout after {self.duration}"
|
673
|
-
|
674
|
-
|
675
314
|
@dataclass(kw_only=True, slots=True)
|
676
315
|
class _LooperNoTaskError(LooperError):
|
677
316
|
looper: Looper
|
@@ -691,7 +330,6 @@ class Looper(Generic[_T]):
|
|
691
330
|
empty_upon_exit: bool = field(default=False, repr=False)
|
692
331
|
logger: str | None = field(default=None, repr=False)
|
693
332
|
timeout: Duration | None = field(default=None, repr=False)
|
694
|
-
timeout_error: type[Exception] = field(default=LooperTimeoutError, repr=False)
|
695
333
|
# settings
|
696
334
|
_backoff: float = field(init=False, repr=False)
|
697
335
|
_debug: bool = field(default=False, repr=False)
|
@@ -715,6 +353,7 @@ class Looper(Generic[_T]):
|
|
715
353
|
_is_entered: Event = field(default_factory=Event, init=False, repr=False)
|
716
354
|
_is_initialized: Event = field(default_factory=Event, init=False, repr=False)
|
717
355
|
_is_initializing: Event = field(default_factory=Event, init=False, repr=False)
|
356
|
+
_is_pending_back_off: Event = field(default_factory=Event, init=False, repr=False)
|
718
357
|
_is_pending_restart: Event = field(default_factory=Event, init=False, repr=False)
|
719
358
|
_is_pending_stop: Event = field(default_factory=Event, init=False, repr=False)
|
720
359
|
_is_pending_stop_when_empty: Event = field(
|
@@ -757,7 +396,7 @@ class Looper(Generic[_T]):
|
|
757
396
|
_ = await self._stack.enter_async_context(looper)
|
758
397
|
if self.auto_start:
|
759
398
|
_ = self._debug and self._logger.debug("%s: auto-starting...", self)
|
760
|
-
with suppress(
|
399
|
+
with suppress(TimeoutError):
|
761
400
|
await self._task
|
762
401
|
case _ as never:
|
763
402
|
assert_never(never)
|
@@ -801,6 +440,11 @@ class Looper(Generic[_T]):
|
|
801
440
|
def __len__(self) -> int:
|
802
441
|
return self._queue.qsize()
|
803
442
|
|
443
|
+
async def _apply_back_off(self) -> None:
|
444
|
+
"""Apply a back off period."""
|
445
|
+
await sleep(self._backoff)
|
446
|
+
self._is_pending_back_off.clear()
|
447
|
+
|
804
448
|
async def core(self) -> None:
|
805
449
|
"""Core part of running the looper."""
|
806
450
|
|
@@ -820,7 +464,9 @@ class Looper(Generic[_T]):
|
|
820
464
|
"""Remove and return an item from the end of the queue without blocking."""
|
821
465
|
return self._queue.get_right_nowait()
|
822
466
|
|
823
|
-
async def initialize(
|
467
|
+
async def initialize(
|
468
|
+
self, *, skip_sleep_if_failure: bool = False
|
469
|
+
) -> Exception | None:
|
824
470
|
"""Initialize the looper."""
|
825
471
|
match self._is_initializing.is_set():
|
826
472
|
case True:
|
@@ -838,21 +484,21 @@ class Looper(Generic[_T]):
|
|
838
484
|
async with self._lock:
|
839
485
|
self._initialization_failures += 1
|
840
486
|
ret = error
|
841
|
-
match
|
487
|
+
match skip_sleep_if_failure:
|
842
488
|
case True:
|
843
489
|
_ = self._logger.warning(
|
844
|
-
"%s: encountered %s whilst initializing
|
490
|
+
"%s: encountered %s whilst initializing",
|
845
491
|
self,
|
846
492
|
repr_error(error),
|
847
|
-
self.backoff,
|
848
493
|
)
|
849
|
-
await sleep(self._backoff)
|
850
494
|
case False:
|
851
495
|
_ = self._logger.warning(
|
852
|
-
"%s: encountered %s whilst initializing",
|
496
|
+
"%s: encountered %s whilst initializing; sleeping for %s...",
|
853
497
|
self,
|
854
498
|
repr_error(error),
|
499
|
+
self.backoff,
|
855
500
|
)
|
501
|
+
await self._apply_back_off()
|
856
502
|
case _ as never:
|
857
503
|
assert_never(never)
|
858
504
|
else:
|
@@ -893,7 +539,6 @@ class Looper(Generic[_T]):
|
|
893
539
|
backoff: Duration | Sentinel = sentinel,
|
894
540
|
logger: str | None | Sentinel = sentinel,
|
895
541
|
timeout: Duration | None | Sentinel = sentinel,
|
896
|
-
timeout_error: type[Exception] | Sentinel = sentinel,
|
897
542
|
_debug: bool | Sentinel = sentinel,
|
898
543
|
**kwargs: Any,
|
899
544
|
) -> Self:
|
@@ -906,11 +551,25 @@ class Looper(Generic[_T]):
|
|
906
551
|
backoff=backoff,
|
907
552
|
logger=logger,
|
908
553
|
timeout=timeout,
|
909
|
-
timeout_error=timeout_error,
|
910
554
|
_debug=_debug,
|
911
555
|
**kwargs,
|
912
556
|
)
|
913
557
|
|
558
|
+
def request_back_off(self) -> None:
|
559
|
+
"""Request the looper to back off."""
|
560
|
+
match self._is_pending_back_off.is_set():
|
561
|
+
case True:
|
562
|
+
_ = self._debug and self._logger.debug(
|
563
|
+
"%s: already requested back off", self
|
564
|
+
)
|
565
|
+
case False:
|
566
|
+
_ = self._debug and self._logger.debug(
|
567
|
+
"%s: requesting back off...", self
|
568
|
+
)
|
569
|
+
self._is_pending_back_off.set()
|
570
|
+
case _ as never:
|
571
|
+
assert_never(never)
|
572
|
+
|
914
573
|
def request_restart(self) -> None:
|
915
574
|
"""Request the looper to restart."""
|
916
575
|
match self._is_pending_restart.is_set():
|
@@ -925,6 +584,7 @@ class Looper(Generic[_T]):
|
|
925
584
|
self._is_pending_restart.set()
|
926
585
|
case _ as never:
|
927
586
|
assert_never(never)
|
587
|
+
self.request_back_off()
|
928
588
|
|
929
589
|
def request_stop(self) -> None:
|
930
590
|
"""Request the looper to stop."""
|
@@ -954,20 +614,20 @@ class Looper(Generic[_T]):
|
|
954
614
|
case _ as never:
|
955
615
|
assert_never(never)
|
956
616
|
|
957
|
-
async def restart(self
|
617
|
+
async def restart(self) -> None:
|
958
618
|
"""Restart the looper."""
|
959
619
|
_ = self._debug and self._logger.debug("%s: restarting...", self)
|
960
620
|
self._is_pending_restart.clear()
|
961
621
|
async with self._lock:
|
962
622
|
self._restart_attempts += 1
|
963
|
-
tear_down = await self.tear_down(
|
964
|
-
initialization = await self.initialize(
|
965
|
-
match tear_down, initialization
|
966
|
-
case None, None
|
623
|
+
tear_down = await self.tear_down(skip_sleep_if_failure=True)
|
624
|
+
initialization = await self.initialize(skip_sleep_if_failure=True)
|
625
|
+
match tear_down, initialization:
|
626
|
+
case None, None:
|
967
627
|
_ = self._debug and self._logger.debug("%s: finished restarting", self)
|
968
628
|
async with self._lock:
|
969
629
|
self._restart_successes += 1
|
970
|
-
case Exception(), None
|
630
|
+
case Exception(), None:
|
971
631
|
async with self._lock:
|
972
632
|
self._restart_failures += 1
|
973
633
|
_ = self._logger.warning(
|
@@ -976,16 +636,8 @@ class Looper(Generic[_T]):
|
|
976
636
|
repr_error(tear_down),
|
977
637
|
self.backoff,
|
978
638
|
)
|
979
|
-
await
|
980
|
-
case Exception()
|
981
|
-
async with self._lock:
|
982
|
-
self._restart_failures += 1
|
983
|
-
_ = self._logger.warning(
|
984
|
-
"%s: encountered %s whilst restarting (tear down)",
|
985
|
-
self,
|
986
|
-
repr_error(tear_down),
|
987
|
-
)
|
988
|
-
case None, Exception(), True:
|
639
|
+
await self._apply_back_off()
|
640
|
+
case None, Exception():
|
989
641
|
async with self._lock:
|
990
642
|
self._restart_failures += 1
|
991
643
|
_ = self._logger.warning(
|
@@ -994,16 +646,8 @@ class Looper(Generic[_T]):
|
|
994
646
|
repr_error(initialization),
|
995
647
|
self.backoff,
|
996
648
|
)
|
997
|
-
await
|
998
|
-
case
|
999
|
-
async with self._lock:
|
1000
|
-
self._restart_failures += 1
|
1001
|
-
_ = self._logger.warning(
|
1002
|
-
"%s: encountered %s whilst restarting (initialize)",
|
1003
|
-
self,
|
1004
|
-
repr_error(initialization),
|
1005
|
-
)
|
1006
|
-
case Exception(), Exception(), True:
|
649
|
+
await self._apply_back_off()
|
650
|
+
case Exception(), Exception():
|
1007
651
|
async with self._lock:
|
1008
652
|
self._restart_failures += 1
|
1009
653
|
_ = self._logger.warning(
|
@@ -1013,23 +657,14 @@ class Looper(Generic[_T]):
|
|
1013
657
|
repr_error(initialization),
|
1014
658
|
self.backoff,
|
1015
659
|
)
|
1016
|
-
await
|
1017
|
-
case Exception(), Exception(), False:
|
1018
|
-
async with self._lock:
|
1019
|
-
self._restart_failures += 1
|
1020
|
-
_ = self._logger.warning(
|
1021
|
-
"%s: encountered %s (tear down) and then %s (initialization) whilst restarting",
|
1022
|
-
self,
|
1023
|
-
repr_error(tear_down),
|
1024
|
-
repr_error(initialization),
|
1025
|
-
)
|
660
|
+
await self._apply_back_off()
|
1026
661
|
case _ as never:
|
1027
662
|
assert_never(never)
|
1028
663
|
|
1029
664
|
async def run_looper(self) -> None:
|
1030
665
|
"""Run the looper."""
|
1031
666
|
try:
|
1032
|
-
async with timeout_dur(duration=self.timeout
|
667
|
+
async with timeout_dur(duration=self.timeout):
|
1033
668
|
while True:
|
1034
669
|
if self._is_stopped.is_set():
|
1035
670
|
_ = self._debug and self._logger.debug("%s: stopped", self)
|
@@ -1038,10 +673,12 @@ class Looper(Generic[_T]):
|
|
1038
673
|
self._is_pending_stop_when_empty.is_set() and self.empty()
|
1039
674
|
):
|
1040
675
|
await self.stop()
|
676
|
+
elif self._is_pending_back_off.is_set():
|
677
|
+
await self._apply_back_off()
|
1041
678
|
elif self._is_pending_restart.is_set():
|
1042
|
-
await self.restart(
|
679
|
+
await self.restart()
|
1043
680
|
elif not self._is_initialized.is_set():
|
1044
|
-
_ = await self.initialize(
|
681
|
+
_ = await self.initialize()
|
1045
682
|
else:
|
1046
683
|
_ = self._debug and self._logger.debug(
|
1047
684
|
"%s: running core...", self
|
@@ -1059,7 +696,6 @@ class Looper(Generic[_T]):
|
|
1059
696
|
async with self._lock:
|
1060
697
|
self._core_failures += 1
|
1061
698
|
self.request_restart()
|
1062
|
-
await sleep(self._backoff)
|
1063
699
|
else:
|
1064
700
|
async with self._lock:
|
1065
701
|
self._core_successes += 1
|
@@ -1068,6 +704,8 @@ class Looper(Generic[_T]):
|
|
1068
704
|
if error.args[0] == "generator didn't stop after athrow()":
|
1069
705
|
return
|
1070
706
|
raise
|
707
|
+
except TimeoutError:
|
708
|
+
pass
|
1071
709
|
|
1072
710
|
async def run_until_empty(self) -> None:
|
1073
711
|
"""Run until the queue is empty."""
|
@@ -1111,7 +749,9 @@ class Looper(Generic[_T]):
|
|
1111
749
|
case _ as never:
|
1112
750
|
assert_never(never)
|
1113
751
|
|
1114
|
-
async def tear_down(
|
752
|
+
async def tear_down(
|
753
|
+
self, *, skip_sleep_if_failure: bool = False
|
754
|
+
) -> Exception | None:
|
1115
755
|
"""Tear down the looper."""
|
1116
756
|
match self._is_tearing_down.is_set():
|
1117
757
|
case True:
|
@@ -1128,21 +768,21 @@ class Looper(Generic[_T]):
|
|
1128
768
|
async with self._lock:
|
1129
769
|
self._tear_down_failures += 1
|
1130
770
|
ret = error
|
1131
|
-
match
|
771
|
+
match skip_sleep_if_failure:
|
1132
772
|
case True:
|
1133
773
|
_ = self._logger.warning(
|
1134
|
-
"%s: encountered %s whilst tearing down
|
774
|
+
"%s: encountered %s whilst tearing down",
|
1135
775
|
self,
|
1136
776
|
repr_error(error),
|
1137
|
-
self.backoff,
|
1138
777
|
)
|
1139
|
-
await sleep(self._backoff)
|
1140
778
|
case False:
|
1141
779
|
_ = self._logger.warning(
|
1142
|
-
"%s: encountered %s whilst tearing down",
|
780
|
+
"%s: encountered %s whilst tearing down; sleeping for %s...",
|
1143
781
|
self,
|
1144
782
|
repr_error(error),
|
783
|
+
self.backoff,
|
1145
784
|
)
|
785
|
+
await self._apply_back_off()
|
1146
786
|
case _ as never:
|
1147
787
|
assert_never(never)
|
1148
788
|
else:
|
@@ -1425,12 +1065,8 @@ async def timeout_dur(
|
|
1425
1065
|
__all__ = [
|
1426
1066
|
"EnhancedQueue",
|
1427
1067
|
"EnhancedTaskGroup",
|
1428
|
-
"InfiniteLooper",
|
1429
|
-
"InfiniteLooperError",
|
1430
|
-
"InfiniteQueueLooper",
|
1431
1068
|
"Looper",
|
1432
1069
|
"LooperError",
|
1433
|
-
"LooperTimeoutError",
|
1434
1070
|
"StreamCommandOutput",
|
1435
1071
|
"UniquePriorityQueue",
|
1436
1072
|
"UniqueQueue",
|