dycw-utilities 0.113.5__py3-none-any.whl → 0.114.1__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.113.5
3
+ Version: 0.114.1
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=ZrPzCX_NTAeiRK-IwTswtKYPrKcHyvHM-9skmFcsa3Q,60
1
+ utilities/__init__.py,sha256=cCgankjsOm9nkKXgXTLbYLmBYYnt73fexYfZXeoBRUc,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=q6hRkJ8GLJcmbah9ifdkw9LUcXAZlLh0g10aZ8cjZ54,17883
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
@@ -64,9 +64,9 @@ utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
64
64
  utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
65
65
  utilities/sentinel.py,sha256=3jIwgpMekWgDAxPDA_hXMP2St43cPhciKN3LWiZ7kv0,1248
66
66
  utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
67
- utilities/slack_sdk.py,sha256=SeDNMh24IPiEBWoGMdgvrflUaFa9TGlTS03H9-NKaQw,4132
67
+ utilities/slack_sdk.py,sha256=Gbla983KulSSXnNyzaXgYQLKoq84KvLH8SdhxU-jQ0Q,4126
68
68
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
69
- utilities/sqlalchemy.py,sha256=GWzp54TP3F2mGhxPTn0c56KxxDeN9VKLMagcRSELhf4,35453
69
+ utilities/sqlalchemy.py,sha256=bs7rD1f8yB0uaFMYgmjo8wEoGow0x6aiELSYTPY_Img,35447
70
70
  utilities/sqlalchemy_polars.py,sha256=wjJpoUo-yO9E2ujpG_06vV5r2OdvBiQ4yvV6wKCa2Tk,15605
71
71
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
72
72
  utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
@@ -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.113.5.dist-info/METADATA,sha256=A14piT2adjIjfWnw8bf7FeFVUDRZfuzcsKkbFdZLrzc,12943
91
- dycw_utilities-0.113.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.113.5.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.113.5.dist-info/RECORD,,
90
+ dycw_utilities-0.114.1.dist-info/METADATA,sha256=Cw2lgBuLscK2IEbNCzzQltYWeAGsvTcdpsxT5CyWNJw,12943
91
+ dycw_utilities-0.114.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.114.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.114.1.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.113.5"
3
+ __version__ = "0.114.1"
utilities/asyncio.py CHANGED
@@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
4
4
  from asyncio import (
5
5
  CancelledError,
6
6
  Event,
7
- Lock,
8
7
  PriorityQueue,
9
8
  Queue,
10
9
  QueueEmpty,
@@ -17,7 +16,7 @@ from asyncio import (
17
16
  sleep,
18
17
  timeout,
19
18
  )
20
- from collections.abc import Callable, Mapping
19
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping
21
20
  from contextlib import (
22
21
  AsyncExitStack,
23
22
  _AsyncGeneratorContextManager,
@@ -31,6 +30,7 @@ from sys import stderr, stdout
31
30
  from typing import (
32
31
  TYPE_CHECKING,
33
32
  Generic,
33
+ NoReturn,
34
34
  Self,
35
35
  TextIO,
36
36
  TypeVar,
@@ -44,7 +44,6 @@ from utilities.errors import ImpossibleCaseError
44
44
  from utilities.functions import ensure_int, ensure_not_none
45
45
  from utilities.sentinel import Sentinel, sentinel
46
46
  from utilities.types import (
47
- Coroutine1,
48
47
  MaybeCallableEvent,
49
48
  MaybeType,
50
49
  THashable,
@@ -240,7 +239,6 @@ class QueueProcessor(AsyncService, Generic[_T]):
240
239
  sleep: Duration = MILLISECOND
241
240
  _await_upon_aenter: bool = field(default=False, init=False, repr=False)
242
241
  _queue: Queue[_T] = field(init=False, repr=False)
243
- _lock: Lock = field(default_factory=Lock, init=False, repr=False)
244
242
 
245
243
  def __post_init__(self) -> None:
246
244
  self._queue = self.queue_type(
@@ -265,9 +263,9 @@ class QueueProcessor(AsyncService, Generic[_T]):
265
263
  await self._run()
266
264
  await sleep_dur(duration=self.sleep)
267
265
 
268
- async def _get_items_nowait(self, *, max_size: int | None = None) -> Sequence[_T]:
266
+ def _get_items_nowait(self, *, max_size: int | None = None) -> Sequence[_T]:
269
267
  """Get items from the queue; no waiting."""
270
- return await get_items_nowait(self._queue, max_size=max_size, lock=self._lock)
268
+ return get_items_nowait(self._queue, max_size=max_size)
271
269
 
272
270
  @abstractmethod
273
271
  async def _process_item(self, item: _T, /) -> None:
@@ -282,7 +280,7 @@ class QueueProcessor(AsyncService, Generic[_T]):
282
280
  async def _run(self) -> None:
283
281
  """Run the processer."""
284
282
  try:
285
- (item,) = await self._get_items_nowait(max_size=1)
283
+ (item,) = self._get_items_nowait(max_size=1)
286
284
  except ValueError:
287
285
  raise QueueEmpty from None
288
286
  try:
@@ -330,61 +328,155 @@ class ExceptionProcessor(QueueProcessor[Exception | type[Exception]]):
330
328
  class InfiniteLooper(ABC, Generic[THashable]):
331
329
  """An infinite loop which can throw exceptions by setting events."""
332
330
 
333
- events: Mapping[THashable, Event] = field(
331
+ _events: Mapping[THashable, Event] = field(
334
332
  default_factory=dict, init=False, repr=False
335
333
  )
336
334
  sleep_core: Duration = SECOND
337
335
  sleep_restart: Duration = MINUTE
338
336
 
339
337
  def __post_init__(self) -> None:
340
- self._reset_events()
341
-
342
- 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."""
343
351
  while True:
344
352
  try:
345
353
  self._reset_events()
346
354
  try:
347
- await self.initialize()
355
+ await self._initialize()
348
356
  except Exception as error: # noqa: BLE001
349
- self.error_upon_initialize(error)
357
+ self._error_upon_initialize(error)
350
358
  await sleep_dur(duration=self.sleep_restart)
351
359
  else:
352
360
  while True:
353
361
  try:
354
362
  event = next(
355
363
  key
356
- for (key, value) in self.events.items()
364
+ for (key, value) in self._events.items()
357
365
  if value.is_set()
358
366
  )
359
367
  except StopIteration:
360
- await self.core()
368
+ await self._core()
361
369
  await sleep_dur(duration=self.sleep_core)
362
370
  else:
363
- raise self.events_and_exceptions[event]
371
+ self._raise_error(event)
372
+ except InfiniteLooperError:
373
+ raise
364
374
  except Exception as error: # noqa: BLE001
365
- self.error_upon_core(error)
375
+ self._error_upon_core(error)
366
376
  await sleep_dur(duration=self.sleep_restart)
367
377
 
368
- @property
369
- @abstractmethod
370
- def events_and_exceptions(self) -> Mapping[THashable, MaybeType[BaseException]]:
371
- """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
372
389
 
373
- async def initialize(self) -> None:
390
+ async def _initialize(self) -> None:
374
391
  """Initialize the loop."""
375
392
 
376
- async def core(self) -> None:
377
- """Run the core."""
393
+ async def _core(self) -> None:
394
+ """Run the core part of the loop."""
378
395
 
379
- 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."""
380
398
  _ = error
381
399
 
382
- 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."""
383
402
  _ = error
384
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
+
385
410
  def _reset_events(self) -> None:
386
411
  """Reset the events."""
387
- 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)
388
480
 
389
481
 
390
482
  ##
@@ -465,9 +557,7 @@ def get_event(
465
557
  ##
466
558
 
467
559
 
468
- async def get_items(
469
- queue: Queue[_T], /, *, max_size: int | None = None, lock: Lock | None = None
470
- ) -> list[_T]:
560
+ async def get_items(queue: Queue[_T], /, *, max_size: int | None = None) -> list[_T]:
471
561
  """Get items from a queue; if empty then wait."""
472
562
  try:
473
563
  items = [await queue.get()]
@@ -476,28 +566,12 @@ async def get_items(
476
566
  return []
477
567
  raise
478
568
  max_size_use = None if max_size is None else (max_size - 1)
479
- if lock is None:
480
- items.extend(await get_items_nowait(queue, max_size=max_size_use))
481
- else:
482
- async with lock:
483
- items.extend(await get_items_nowait(queue, max_size=max_size_use))
569
+ items.extend(get_items_nowait(queue, max_size=max_size_use))
484
570
  return items
485
571
 
486
572
 
487
- async def get_items_nowait(
488
- queue: Queue[_T], /, *, max_size: int | None = None, lock: Lock | None = None
489
- ) -> list[_T]:
573
+ def get_items_nowait(queue: Queue[_T], /, *, max_size: int | None = None) -> list[_T]:
490
574
  """Get items from a queue; no waiting."""
491
- if lock is None:
492
- return _get_items_nowait_core(queue, max_size=max_size)
493
- async with lock:
494
- return _get_items_nowait_core(queue, max_size=max_size)
495
-
496
-
497
- def _get_items_nowait_core(
498
- queue: Queue[_T], /, *, max_size: int | None = None
499
- ) -> list[_T]:
500
- """Get all the items from a queue; no waiting."""
501
575
  items: list[_T] = []
502
576
  if max_size is None:
503
577
  while True:
@@ -517,6 +591,21 @@ def _get_items_nowait_core(
517
591
  ##
518
592
 
519
593
 
594
+ async def put_items(items: Iterable[_T], queue: Queue[_T], /) -> None:
595
+ """Put items into a queue; if full then wait."""
596
+ for item in items:
597
+ await queue.put(item)
598
+
599
+
600
+ def put_items_nowait(items: Iterable[_T], queue: Queue[_T], /) -> None:
601
+ """Put items into a queue; no waiting."""
602
+ for item in items:
603
+ queue.put_nowait(item)
604
+
605
+
606
+ ##
607
+
608
+
520
609
  async def sleep_dur(*, duration: Duration | None = None) -> None:
521
610
  """Sleep which accepts durations."""
522
611
  if duration is None:
@@ -595,6 +684,7 @@ __all__ = [
595
684
  "AsyncService",
596
685
  "EnhancedTaskGroup",
597
686
  "ExceptionProcessor",
687
+ "InfiniteLooperError",
598
688
  "QueueProcessor",
599
689
  "StreamCommandOutput",
600
690
  "UniquePriorityQueue",
@@ -602,6 +692,8 @@ __all__ = [
602
692
  "get_event",
603
693
  "get_items",
604
694
  "get_items_nowait",
695
+ "put_items",
696
+ "put_items_nowait",
605
697
  "sleep_dur",
606
698
  "stream_command",
607
699
  "timeout_dur",
utilities/slack_sdk.py CHANGED
@@ -78,7 +78,7 @@ class SlackHandler(Handler, QueueProcessor[str]):
78
78
  @override
79
79
  async def _process_item(self, item: str, /) -> None:
80
80
  """Process the first item."""
81
- items = list(chain([item], await self._get_items_nowait()))
81
+ items = list(chain([item], self._get_items_nowait()))
82
82
  text = "\n".join(items)
83
83
  try:
84
84
  async with timeout_dur(duration=self.timeout):
utilities/sqlalchemy.py CHANGED
@@ -627,7 +627,7 @@ class Upserter(QueueProcessor[_InsertItem]):
627
627
  @override
628
628
  async def _process_item(self, item: _InsertItem, /) -> None:
629
629
  """Process the first item."""
630
- items = list(chain([item], await self._get_items_nowait()))
630
+ items = list(chain([item], self._get_items_nowait()))
631
631
  await self._pre_upsert(items)
632
632
  await upsert_items(
633
633
  self.engine,