dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (84) hide show
  1. dycw_utilities-0.175.31.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.31.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +113 -64
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +381 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +1 -1
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +12 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +74 -85
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/parse.py +2 -2
  39. utilities/pathlib.py +66 -34
  40. utilities/permissions.py +298 -0
  41. utilities/platform.py +4 -4
  42. utilities/polars.py +934 -420
  43. utilities/polars_ols.py +1 -1
  44. utilities/postgres.py +296 -174
  45. utilities/pottery.py +8 -73
  46. utilities/pqdm.py +3 -3
  47. utilities/pwd.py +28 -0
  48. utilities/pydantic.py +11 -0
  49. utilities/pydantic_settings.py +240 -0
  50. utilities/pydantic_settings_sops.py +76 -0
  51. utilities/pyinstrument.py +5 -5
  52. utilities/pytest.py +155 -46
  53. utilities/pytest_plugins/pytest_randomly.py +1 -1
  54. utilities/pytest_plugins/pytest_regressions.py +7 -3
  55. utilities/pytest_regressions.py +27 -8
  56. utilities/random.py +11 -6
  57. utilities/re.py +1 -1
  58. utilities/redis.py +101 -64
  59. utilities/sentinel.py +10 -0
  60. utilities/shelve.py +4 -1
  61. utilities/shutil.py +25 -0
  62. utilities/slack_sdk.py +8 -3
  63. utilities/sqlalchemy.py +422 -352
  64. utilities/sqlalchemy_polars.py +28 -52
  65. utilities/string.py +1 -1
  66. utilities/subprocess.py +1947 -0
  67. utilities/tempfile.py +95 -4
  68. utilities/testbook.py +50 -0
  69. utilities/text.py +165 -42
  70. utilities/timer.py +2 -2
  71. utilities/traceback.py +46 -36
  72. utilities/types.py +62 -23
  73. utilities/typing.py +479 -19
  74. utilities/uuid.py +42 -5
  75. utilities/version.py +27 -26
  76. utilities/whenever.py +661 -151
  77. utilities/zoneinfo.py +80 -22
  78. dycw_utilities-0.148.5.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.5.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
  82. utilities/eventkit.py +0 -388
  83. utilities/period.py +0 -237
  84. utilities/typed_settings.py +0 -144
utilities/redis.py CHANGED
@@ -25,7 +25,9 @@ from utilities.contextlib import enhanced_async_context_manager
25
25
  from utilities.errors import ImpossibleCaseError
26
26
  from utilities.functions import ensure_int, identity
27
27
  from utilities.iterables import always_iterable, one
28
- from utilities.whenever import MILLISECOND, SECOND, to_milliseconds, to_seconds
28
+ from utilities.os import is_pytest
29
+ from utilities.typing import is_instance_gen
30
+ from utilities.whenever import MILLISECOND, SECOND, to_milliseconds, to_nanoseconds
29
31
 
30
32
  if TYPE_CHECKING:
31
33
  from collections.abc import AsyncIterator, Awaitable, Collection, Iterable
@@ -35,7 +37,7 @@ if TYPE_CHECKING:
35
37
  from redis.typing import EncodableT
36
38
 
37
39
  from utilities.iterables import MaybeIterable
38
- from utilities.types import Delta, MaybeListStr, MaybeSequence, MaybeType, TypeLike
40
+ from utilities.types import Delta, MaybeSequence, MaybeType, TypeLike
39
41
 
40
42
 
41
43
  _PUBLISH_TIMEOUT: Delta = SECOND
@@ -187,7 +189,7 @@ def redis_hash_map_key[K, V](
187
189
  value_serializer: Callable[[V], bytes] | None = None,
188
190
  value_deserializer: Callable[[bytes], V] | None = None,
189
191
  timeout: Delta | None = None,
190
- error: type[Exception] = TimeoutError,
192
+ error: MaybeType[BaseException] = TimeoutError,
191
193
  ttl: Delta | None = None,
192
194
  ) -> RedisHashMapKey[K, V]: ...
193
195
  @overload
@@ -202,7 +204,7 @@ def redis_hash_map_key[K, V1, V2](
202
204
  value_serializer: Callable[[V1 | V2], bytes] | None = None,
203
205
  value_deserializer: Callable[[bytes], V1 | V2] | None = None,
204
206
  timeout: Delta | None = None,
205
- error: type[Exception] = TimeoutError,
207
+ error: MaybeType[BaseException] = TimeoutError,
206
208
  ttl: Delta | None = None,
207
209
  ) -> RedisHashMapKey[K, V1 | V2]: ...
208
210
  @overload
@@ -217,7 +219,7 @@ def redis_hash_map_key[K, V1, V2, V3](
217
219
  value_serializer: Callable[[V1 | V2 | V3], bytes] | None = None,
218
220
  value_deserializer: Callable[[bytes], V1 | V2 | V3] | None = None,
219
221
  timeout: Delta | None = None,
220
- error: type[Exception] = TimeoutError,
222
+ error: MaybeType[BaseException] = TimeoutError,
221
223
  ttl: Delta | None = None,
222
224
  ) -> RedisHashMapKey[K, V1 | V2 | V3]: ...
223
225
  @overload
@@ -232,7 +234,7 @@ def redis_hash_map_key[K1, K2, V](
232
234
  value_serializer: Callable[[V], bytes] | None = None,
233
235
  value_deserializer: Callable[[bytes], V] | None = None,
234
236
  timeout: Delta | None = None,
235
- error: type[Exception] = TimeoutError,
237
+ error: MaybeType[BaseException] = TimeoutError,
236
238
  ttl: Delta | None = None,
237
239
  ) -> RedisHashMapKey[K1 | K2, V]: ...
238
240
  @overload
@@ -247,7 +249,7 @@ def redis_hash_map_key[K1, K2, V1, V2](
247
249
  value_serializer: Callable[[V1 | V2], bytes] | None = None,
248
250
  value_deserializer: Callable[[bytes], V1 | V2] | None = None,
249
251
  timeout: Delta | None = None,
250
- error: type[Exception] = TimeoutError,
252
+ error: MaybeType[BaseException] = TimeoutError,
251
253
  ttl: Delta | None = None,
252
254
  ) -> RedisHashMapKey[K1 | K2, V1 | V2]: ...
253
255
  @overload
@@ -262,7 +264,7 @@ def redis_hash_map_key[K1, K2, V1, V2, V3](
262
264
  value_serializer: Callable[[V1 | V2 | V3], bytes] | None = None,
263
265
  value_deserializer: Callable[[bytes], V1 | V2 | V3] | None = None,
264
266
  timeout: Delta | None = None,
265
- error: type[Exception] = TimeoutError,
267
+ error: MaybeType[BaseException] = TimeoutError,
266
268
  ttl: Delta | None = None,
267
269
  ) -> RedisHashMapKey[K1 | K2, V1 | V2 | V3]: ...
268
270
  @overload
@@ -277,7 +279,7 @@ def redis_hash_map_key[K1, K2, K3, V](
277
279
  value_serializer: Callable[[V], bytes] | None = None,
278
280
  value_deserializer: Callable[[bytes], V] | None = None,
279
281
  timeout: Delta | None = None,
280
- error: type[Exception] = TimeoutError,
282
+ error: MaybeType[BaseException] = TimeoutError,
281
283
  ttl: Delta | None = None,
282
284
  ) -> RedisHashMapKey[K1 | K2 | K3, V]: ...
283
285
  @overload
@@ -292,7 +294,7 @@ def redis_hash_map_key[K1, K2, K3, V1, V2](
292
294
  value_serializer: Callable[[V1 | V2], bytes] | None = None,
293
295
  value_deserializer: Callable[[bytes], V1 | V2] | None = None,
294
296
  timeout: Delta | None = None,
295
- error: type[Exception] = TimeoutError,
297
+ error: MaybeType[BaseException] = TimeoutError,
296
298
  ttl: Delta | None = None,
297
299
  ) -> RedisHashMapKey[K1 | K2 | K3, V1 | V2]: ...
298
300
  @overload
@@ -307,7 +309,7 @@ def redis_hash_map_key[K1, K2, K3, V1, V2, V3](
307
309
  value_serializer: Callable[[V1 | V2 | V3], bytes] | None = None,
308
310
  value_deserializer: Callable[[bytes], V1 | V2 | V3] | None = None,
309
311
  timeout: Delta | None = None,
310
- error: type[Exception] = TimeoutError,
312
+ error: MaybeType[BaseException] = TimeoutError,
311
313
  ttl: Delta | None = None,
312
314
  ) -> RedisHashMapKey[K1 | K2 | K3, V1 | V2 | V3]: ...
313
315
  @overload
@@ -322,7 +324,7 @@ def redis_hash_map_key[K, K1, K2, K3, V, V1, V2, V3](
322
324
  value_serializer: Callable[[V1 | V2 | V3], bytes] | None = None,
323
325
  value_deserializer: Callable[[bytes], V1 | V2 | V3] | None = None,
324
326
  timeout: Delta | None = None,
325
- error: type[Exception] = TimeoutError,
327
+ error: MaybeType[BaseException] = TimeoutError,
326
328
  ttl: Delta | None = None,
327
329
  ) -> RedisHashMapKey[K, V]: ...
328
330
  def redis_hash_map_key[K, V](
@@ -337,7 +339,7 @@ def redis_hash_map_key[K, V](
337
339
  value_deserializer: Callable[[bytes], Any] | None = None,
338
340
  timeout: Delta | None = None,
339
341
  ttl: Delta | None = None,
340
- error: type[Exception] = TimeoutError,
342
+ error: MaybeType[BaseException] = TimeoutError,
341
343
  ) -> RedisHashMapKey[K, V]:
342
344
  """Create a redis key."""
343
345
  return RedisHashMapKey( # skipif-ci-and-not-linux
@@ -386,7 +388,7 @@ class RedisKey[T]:
386
388
  match result: # skipif-ci-and-not-linux
387
389
  case 0 | 1 as value:
388
390
  return bool(value)
389
- case _ as never:
391
+ case never:
390
392
  assert_never(never)
391
393
 
392
394
  async def get(self, redis: Redis, /) -> T:
@@ -425,7 +427,7 @@ def redis_key[T](
425
427
  serializer: Callable[[T], bytes] | None = None,
426
428
  deserializer: Callable[[bytes], T] | None = None,
427
429
  timeout: Delta | None = None,
428
- error: type[Exception] = TimeoutError,
430
+ error: MaybeType[BaseException] = TimeoutError,
429
431
  ttl: Delta | None = None,
430
432
  ) -> RedisKey[T]: ...
431
433
  @overload
@@ -437,7 +439,7 @@ def redis_key[T1, T2](
437
439
  serializer: Callable[[T1 | T2], bytes] | None = None,
438
440
  deserializer: Callable[[bytes], T1 | T2] | None = None,
439
441
  timeout: Delta | None = None,
440
- error: type[Exception] = TimeoutError,
442
+ error: MaybeType[BaseException] = TimeoutError,
441
443
  ttl: Delta | None = None,
442
444
  ) -> RedisKey[T1 | T2]: ...
443
445
  @overload
@@ -449,7 +451,7 @@ def redis_key[T1, T2, T3](
449
451
  serializer: Callable[[T1 | T2 | T3], bytes] | None = None,
450
452
  deserializer: Callable[[bytes], T1 | T2 | T3] | None = None,
451
453
  timeout: Delta | None = None,
452
- error: type[Exception] = TimeoutError,
454
+ error: MaybeType[BaseException] = TimeoutError,
453
455
  ttl: Delta | None = None,
454
456
  ) -> RedisKey[T1 | T2 | T3]: ...
455
457
  @overload
@@ -461,7 +463,7 @@ def redis_key[T1, T2, T3, T4](
461
463
  serializer: Callable[[T1 | T2 | T3 | T4], bytes] | None = None,
462
464
  deserializer: Callable[[bytes], T1 | T2 | T3 | T4] | None = None,
463
465
  timeout: Delta | None = None,
464
- error: type[Exception] = TimeoutError,
466
+ error: MaybeType[BaseException] = TimeoutError,
465
467
  ttl: Delta | None = None,
466
468
  ) -> RedisKey[T1 | T2 | T3 | T4]: ...
467
469
  @overload
@@ -473,7 +475,7 @@ def redis_key[T1, T2, T3, T4, T5](
473
475
  serializer: Callable[[T1 | T2 | T3 | T4 | T5], bytes] | None = None,
474
476
  deserializer: Callable[[bytes], T1 | T2 | T3 | T4 | T5] | None = None,
475
477
  timeout: Delta | None = None,
476
- error: type[Exception] = TimeoutError,
478
+ error: MaybeType[BaseException] = TimeoutError,
477
479
  ttl: Delta | None = None,
478
480
  ) -> RedisKey[T1 | T2 | T3 | T4 | T5]: ...
479
481
  @overload
@@ -485,7 +487,7 @@ def redis_key[T, T1, T2, T3, T4, T5](
485
487
  serializer: Callable[[T1 | T2 | T3 | T4 | T5], bytes] | None = None,
486
488
  deserializer: Callable[[bytes], T1 | T2 | T3 | T4 | T5] | None = None,
487
489
  timeout: Delta | None = None,
488
- error: type[Exception] = TimeoutError,
490
+ error: MaybeType[BaseException] = TimeoutError,
489
491
  ttl: Delta | None = None,
490
492
  ) -> RedisKey[T]: ...
491
493
  def redis_key[T](
@@ -496,7 +498,7 @@ def redis_key[T](
496
498
  serializer: Callable[[Any], bytes] | None = None,
497
499
  deserializer: Callable[[bytes], Any] | None = None,
498
500
  timeout: Delta | None = None,
499
- error: type[Exception] = TimeoutError,
501
+ error: MaybeType[BaseException] = TimeoutError,
500
502
  ttl: Delta | None = None,
501
503
  ) -> RedisKey[T]:
502
504
  """Create a redis key."""
@@ -522,7 +524,7 @@ async def publish[T](
522
524
  /,
523
525
  *,
524
526
  serializer: Callable[[T], EncodableT],
525
- timeout: Delta = _PUBLISH_TIMEOUT,
527
+ timeout: Delta | None = _PUBLISH_TIMEOUT,
526
528
  ) -> int: ...
527
529
  @overload
528
530
  async def publish(
@@ -532,7 +534,7 @@ async def publish(
532
534
  /,
533
535
  *,
534
536
  serializer: None = None,
535
- timeout: Delta = _PUBLISH_TIMEOUT,
537
+ timeout: Delta | None = _PUBLISH_TIMEOUT,
536
538
  ) -> int: ...
537
539
  @overload
538
540
  async def publish[T](
@@ -542,7 +544,7 @@ async def publish[T](
542
544
  /,
543
545
  *,
544
546
  serializer: Callable[[T], EncodableT] | None = None,
545
- timeout: Delta = _PUBLISH_TIMEOUT,
547
+ timeout: Delta | None = _PUBLISH_TIMEOUT,
546
548
  ) -> int: ...
547
549
  async def publish[T](
548
550
  redis: Redis,
@@ -551,7 +553,7 @@ async def publish[T](
551
553
  /,
552
554
  *,
553
555
  serializer: Callable[[T], EncodableT] | None = None,
554
- timeout: Delta = _PUBLISH_TIMEOUT,
556
+ timeout: Delta | None = _PUBLISH_TIMEOUT,
555
557
  ) -> int:
556
558
  """Publish an object to a channel."""
557
559
  match data, serializer: # skipif-ci-and-not-linux
@@ -561,7 +563,7 @@ async def publish[T](
561
563
  raise PublishError(data=data)
562
564
  case _, Callable():
563
565
  data_use = serializer(data)
564
- case _ as never:
566
+ case never:
565
567
  assert_never(never)
566
568
  async with timeout_td(timeout): # skipif-ci-and-not-linux
567
569
  response = await redis.publish(channel, data_use) # skipif-ci-and-not-linux
@@ -583,11 +585,11 @@ class PublishError(Exception):
583
585
  async def publish_many[T](
584
586
  redis: Redis,
585
587
  channel: str,
586
- data: MaybeSequence[bytes | T] | MaybeListStr,
588
+ data: MaybeSequence[bytes | str | T],
587
589
  /,
588
590
  *,
589
591
  serializer: Callable[[T], EncodableT] | None = None,
590
- timeout: Delta = _PUBLISH_TIMEOUT,
592
+ timeout: Delta | None = _PUBLISH_TIMEOUT,
591
593
  ) -> Sequence[bool]:
592
594
  """Publish an object/multiple objects to a channel."""
593
595
  async with TaskGroup() as tg:
@@ -613,7 +615,7 @@ async def _try_publish[T](
613
615
  /,
614
616
  *,
615
617
  serializer: Callable[[T], EncodableT] | None = None,
616
- timeout: Delta = _PUBLISH_TIMEOUT,
618
+ timeout: Delta | None = _PUBLISH_TIMEOUT,
617
619
  ) -> bool:
618
620
  try:
619
621
  _ = await publish(redis, channel, data, serializer=serializer, timeout=timeout)
@@ -638,9 +640,11 @@ def subscribe(
638
640
  /,
639
641
  *,
640
642
  timeout: Delta | None = _SUBSCRIBE_TIMEOUT,
641
- sleep: Delta = _SUBSCRIBE_SLEEP,
642
643
  output: Literal["raw"],
643
- filter_: Callable[[_RedisMessage], bool] | None = None,
644
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
645
+ filter_: Callable[[bytes], bool] | None = None,
646
+ error_filter: Callable[[bytes, Exception], None] | None = None,
647
+ sleep: Delta = _SUBSCRIBE_SLEEP,
644
648
  ) -> AsyncIterator[Task[None]]: ...
645
649
  @overload
646
650
  @enhanced_async_context_manager
@@ -651,9 +655,11 @@ def subscribe(
651
655
  /,
652
656
  *,
653
657
  timeout: Delta | None = _SUBSCRIBE_TIMEOUT,
654
- sleep: Delta = _SUBSCRIBE_SLEEP,
655
658
  output: Literal["bytes"],
659
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
656
660
  filter_: Callable[[bytes], bool] | None = None,
661
+ error_filter: Callable[[bytes, Exception], None] | None = None,
662
+ sleep: Delta = _SUBSCRIBE_SLEEP,
657
663
  ) -> AsyncIterator[Task[None]]: ...
658
664
  @overload
659
665
  @enhanced_async_context_manager
@@ -664,9 +670,11 @@ def subscribe(
664
670
  /,
665
671
  *,
666
672
  timeout: Delta | None = _SUBSCRIBE_TIMEOUT,
667
- sleep: Delta = _SUBSCRIBE_SLEEP,
668
673
  output: Literal["text"] = "text",
674
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
669
675
  filter_: Callable[[str], bool] | None = None,
676
+ error_filter: Callable[[str, Exception], None] | None = None,
677
+ sleep: Delta = _SUBSCRIBE_SLEEP,
670
678
  ) -> AsyncIterator[Task[None]]: ...
671
679
  @overload
672
680
  @enhanced_async_context_manager
@@ -677,9 +685,11 @@ def subscribe[T](
677
685
  /,
678
686
  *,
679
687
  timeout: Delta | None = _SUBSCRIBE_TIMEOUT,
680
- sleep: Delta = _SUBSCRIBE_SLEEP,
681
688
  output: Callable[[bytes], T],
689
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
682
690
  filter_: Callable[[T], bool] | None = None,
691
+ error_filter: Callable[[T, Exception], None] | None = None,
692
+ sleep: Delta = _SUBSCRIBE_SLEEP,
683
693
  ) -> AsyncIterator[Task[None]]: ...
684
694
  @enhanced_async_context_manager
685
695
  async def subscribe[T](
@@ -689,28 +699,27 @@ async def subscribe[T](
689
699
  /,
690
700
  *,
691
701
  timeout: Delta | None = _SUBSCRIBE_TIMEOUT,
692
- sleep: Delta = _SUBSCRIBE_SLEEP,
693
702
  output: Literal["raw", "bytes", "text"] | Callable[[bytes], T] = "text",
694
- filter_: Callable[[Any], bool] | None = None,
703
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
704
+ filter_: Callable[[T], bool] | None = None,
705
+ error_filter: Callable[[T, Exception], None] | None = None,
706
+ sleep: Delta = _SUBSCRIBE_SLEEP,
695
707
  ) -> AsyncIterator[Task[None]]:
696
708
  """Subscribe to the data of a given channel(s)."""
697
709
  channels = list(always_iterable(channels)) # skipif-ci-and-not-linux
698
710
  match output: # skipif-ci-and-not-linux
699
711
  case "raw":
700
- transform = cast("Any", identity)
712
+ transform = cast("Callable[[_RedisMessage], T]", identity)
701
713
  case "bytes":
702
- transform = cast("Any", itemgetter("data"))
714
+ transform = cast("Callable[[_RedisMessage], T]", itemgetter("data"))
703
715
  case "text":
704
-
705
- def transform(message: _RedisMessage, /) -> str: # pyright: ignore[reportRedeclaration]
706
- return message["data"].decode()
707
-
716
+ transform = cast("Callable[[_RedisMessage], T]", _decoded_data)
708
717
  case Callable() as deserialize:
709
718
 
710
719
  def transform(message: _RedisMessage, /) -> T:
711
720
  return deserialize(message["data"])
712
721
 
713
- case _ as never:
722
+ case never:
714
723
  assert_never(never)
715
724
 
716
725
  task = create_task( # skipif-ci-and-not-linux
@@ -720,8 +729,10 @@ async def subscribe[T](
720
729
  transform,
721
730
  queue,
722
731
  timeout=timeout,
723
- sleep=sleep,
732
+ error_transform=error_transform,
724
733
  filter_=filter_,
734
+ error_filter=error_filter,
735
+ sleep=sleep,
725
736
  )
726
737
  )
727
738
  try: # skipif-ci-and-not-linux
@@ -730,27 +741,31 @@ async def subscribe[T](
730
741
  try:
731
742
  _ = task.cancel()
732
743
  except RuntimeError as error: # pragma: no cover
733
- from utilities.pytest import is_pytest
734
-
735
744
  if (not is_pytest()) or (error.args[0] != "Event loop is closed"):
736
745
  raise
737
746
  with suppress(CancelledError):
738
747
  await task
739
748
 
740
749
 
741
- async def _subscribe_core(
750
+ def _decoded_data(message: _RedisMessage, /) -> str:
751
+ return message["data"].decode()
752
+
753
+
754
+ async def _subscribe_core[T](
742
755
  redis: Redis,
743
756
  channels: MaybeIterable[str],
744
- transform: Callable[[_RedisMessage], Any],
757
+ transform: Callable[[_RedisMessage], T],
745
758
  queue: Queue[Any],
746
759
  /,
747
760
  *,
748
761
  timeout: Delta | None = _SUBSCRIBE_TIMEOUT,
762
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
763
+ filter_: Callable[[T], bool] | None = None,
764
+ error_filter: Callable[[T, Exception], None] | None = None,
749
765
  sleep: Delta = _SUBSCRIBE_SLEEP,
750
- filter_: Callable[[Any], bool] | None = None,
751
766
  ) -> None:
752
767
  timeout_use = ( # skipif-ci-and-not-linux
753
- None if timeout is None else to_seconds(timeout)
768
+ None if timeout is None else (to_nanoseconds(timeout) / 1e9)
754
769
  )
755
770
  is_subscribe_message = partial( # skipif-ci-and-not-linux
756
771
  _is_message, channels={c.encode() for c in channels}
@@ -759,9 +774,14 @@ async def _subscribe_core(
759
774
  while True:
760
775
  message = await pubsub.get_message(timeout=timeout_use)
761
776
  if is_subscribe_message(message):
762
- transformed = transform(message)
763
- if (filter_ is None) or filter_(transformed):
764
- queue.put_nowait(transformed)
777
+ _handle_message(
778
+ message,
779
+ transform,
780
+ queue,
781
+ error_transform=error_transform,
782
+ filter_=filter_,
783
+ error_filter=error_filter,
784
+ )
765
785
  else:
766
786
  await sleep_td(sleep)
767
787
 
@@ -769,17 +789,34 @@ async def _subscribe_core(
769
789
  def _is_message(
770
790
  message: Any, /, *, channels: Collection[bytes]
771
791
  ) -> TypeGuard[_RedisMessage]:
772
- return (
773
- isinstance(message, Mapping)
774
- and ("type" in message)
775
- and (message["type"] in {"subscribe", "psubscribe", "message", "pmessage"})
776
- and ("pattern" in message)
777
- and ((message["pattern"] is None) or isinstance(message["pattern"], str))
778
- and ("channel" in message)
779
- and (message["channel"] in channels)
780
- and ("data" in message)
781
- and isinstance(message["data"], bytes)
782
- )
792
+ return is_instance_gen(message, _RedisMessage) and (message["channel"] in channels)
793
+
794
+
795
+ def _handle_message[T](
796
+ message: _RedisMessage,
797
+ transform: Callable[[_RedisMessage], T],
798
+ queue: Queue[Any],
799
+ /,
800
+ *,
801
+ error_transform: Callable[[_RedisMessage, Exception], None] | None = None,
802
+ filter_: Callable[[T], bool] | None = None,
803
+ error_filter: Callable[[T, Exception], None] | None = None,
804
+ ) -> None:
805
+ try:
806
+ transformed = transform(message)
807
+ except Exception as error: # noqa: BLE001
808
+ if error_transform is not None:
809
+ error_transform(message, error)
810
+ return
811
+ if filter_ is None:
812
+ queue.put_nowait(transformed)
813
+ return
814
+ try:
815
+ if filter_(transformed):
816
+ queue.put_nowait(transformed)
817
+ except Exception as error: # noqa: BLE001
818
+ if error_filter is not None:
819
+ error_filter(transformed, error)
783
820
 
784
821
 
785
822
  class _RedisMessage(TypedDict):
utilities/sentinel.py CHANGED
@@ -4,6 +4,8 @@ from dataclasses import dataclass
4
4
  from re import IGNORECASE, search
5
5
  from typing import Any, override
6
6
 
7
+ from typing_extensions import TypeIs
8
+
7
9
 
8
10
  class _Meta(type):
9
11
  """Metaclass for the sentinel."""
@@ -34,6 +36,13 @@ class Sentinel(metaclass=_Meta):
34
36
 
35
37
  sentinel = Sentinel()
36
38
 
39
+ ##
40
+
41
+
42
+ def is_sentinel(obj: Any, /) -> TypeIs[Sentinel]:
43
+ """Check if an object is the sentinel."""
44
+ return obj is sentinel
45
+
37
46
 
38
47
  ##
39
48
 
@@ -58,6 +67,7 @@ __all__ = [
58
67
  "SENTINEL_REPR",
59
68
  "ParseSentinelError",
60
69
  "Sentinel",
70
+ "is_sentinel",
61
71
  "parse_sentinel",
62
72
  "sentinel",
63
73
  ]
utilities/shelve.py CHANGED
@@ -12,12 +12,15 @@ if TYPE_CHECKING:
12
12
  from utilities.types import PathLike
13
13
 
14
14
 
15
+ type _Flag = Literal["r", "w", "c", "n"]
16
+
17
+
15
18
  @contextmanager
16
19
  def yield_shelf(
17
20
  path: PathLike,
18
21
  /,
19
22
  *,
20
- flag: Literal["r", "w", "c", "n"] = "c",
23
+ flag: _Flag = "c",
21
24
  protocol: int | None = None,
22
25
  writeback: bool = False,
23
26
  ) -> Iterator[Shelf[Any]]:
utilities/shutil.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import override
7
+
8
+
9
+ def which(cmd: str, /) -> Path:
10
+ path = shutil.which(cmd)
11
+ if path is None:
12
+ raise WhichError(cmd=cmd)
13
+ return Path(path)
14
+
15
+
16
+ @dataclass(kw_only=True, slots=True)
17
+ class WhichError(Exception):
18
+ cmd: str
19
+
20
+ @override
21
+ def __str__(self) -> str:
22
+ return f"{self.cmd!r} not found"
23
+
24
+
25
+ __all__ = ["WhichError", "which"]
utilities/slack_sdk.py CHANGED
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from slack_sdk.webhook import WebhookResponse
16
16
  from whenever import TimeDelta
17
17
 
18
- from utilities.types import Delta
18
+ from utilities.types import Delta, MaybeType
19
19
 
20
20
 
21
21
  _TIMEOUT: Delta = MINUTE
@@ -39,11 +39,16 @@ def _get_client(url: str, /, *, timeout: Delta = _TIMEOUT) -> WebhookClient:
39
39
 
40
40
 
41
41
  async def send_to_slack_async(
42
- url: str, text: str, /, *, timeout: TimeDelta = _TIMEOUT
42
+ url: str,
43
+ text: str,
44
+ /,
45
+ *,
46
+ timeout: TimeDelta = _TIMEOUT,
47
+ error: MaybeType[BaseException] = TimeoutError,
43
48
  ) -> None:
44
49
  """Send a message via Slack."""
45
50
  client = _get_async_client(url, timeout=timeout)
46
- async with timeout_td(timeout):
51
+ async with timeout_td(timeout, error=error):
47
52
  response = await client.send(text=text)
48
53
  if response.status_code != HTTPStatus.OK: # pragma: no cover
49
54
  raise SendToSlackError(text=text, response=response)