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.
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, Hashable, Iterable, Iterator, Mapping
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 ImpossibleCaseError, repr_error
61
- from utilities.functions import ensure_int, ensure_not_none, get_class_name
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(self.timeout_error):
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(self, *, sleep_if_failure: bool) -> Exception | None:
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 sleep_if_failure:
487
+ match skip_sleep_if_failure:
842
488
  case True:
843
489
  _ = self._logger.warning(
844
- "%s: encountered %s whilst initializing; sleeping for %s...",
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, *, sleep_if_failure: bool) -> None:
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(sleep_if_failure=False)
964
- initialization = await self.initialize(sleep_if_failure=False)
965
- match tear_down, initialization, sleep_if_failure:
966
- case None, None, bool():
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, True:
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 sleep(self._backoff)
980
- case Exception(), None, False:
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 sleep(self._backoff)
998
- case None, Exception(), False:
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 sleep(self._backoff)
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, error=self.timeout_error):
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(sleep_if_failure=True)
679
+ await self.restart()
1043
680
  elif not self._is_initialized.is_set():
1044
- _ = await self.initialize(sleep_if_failure=True)
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(self, *, sleep_if_failure: bool) -> Exception | None:
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 sleep_if_failure:
771
+ match skip_sleep_if_failure:
1132
772
  case True:
1133
773
  _ = self._logger.warning(
1134
- "%s: encountered %s whilst tearing down; sleeping for %s...",
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",