dycw-utilities 0.126.9__py3-none-any.whl → 0.126.11__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,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.126.9
3
+ Version: 0.126.11
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
7
7
  Requires-Dist: typing-extensions<4.14,>=4.13.1
8
8
  Provides-Extra: test
9
9
  Requires-Dist: dycw-pytest-only<2.2,>=2.1.1; extra == 'test'
10
- Requires-Dist: hypothesis<6.133,>=6.132.0; extra == 'test'
10
+ Requires-Dist: hypothesis<6.134,>=6.133.0; extra == 'test'
11
11
  Requires-Dist: pytest-asyncio<1.1,>=1.0.0; extra == 'test'
12
12
  Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
13
13
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
@@ -77,7 +77,7 @@ Provides-Extra: zzz-test-hypothesis
77
77
  Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-hypothesis'
78
78
  Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
79
79
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
80
- Requires-Dist: hypothesis<6.133,>=6.132.0; extra == 'zzz-test-hypothesis'
80
+ Requires-Dist: hypothesis<6.134,>=6.133.0; extra == 'zzz-test-hypothesis'
81
81
  Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
82
82
  Requires-Dist: numpy<2.3,>=2.2.6; extra == 'zzz-test-hypothesis'
83
83
  Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=aVKs1kn02xCyHOT2ZG3T7fvbEh9r8Eu-f2_9scWzbPI,60
1
+ utilities/__init__.py,sha256=zZdDjXYw0iXgNb3iikpaOv2GNjXKtYX9dLFyhvGm1Bs,61
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=phbGti22VSe9cu-SwM1vP8kyUg8AUDHvvciMvE6JnCg,51842
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -23,7 +23,7 @@ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
23
23
  utilities/git.py,sha256=wpt5dZ5Oi5931pN24_VLZYaQOvmR0OcQuVtgHzFUN1k,2359
24
24
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
25
25
  utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
26
- utilities/hypothesis.py,sha256=tgUQ3egBETjo_A8xzEmfuYMGYf8o30PtZD8FqBfwsPM,45267
26
+ utilities/hypothesis.py,sha256=snJ35u9-dXKn3Unac4IPW2V4JtRUg5B7SsDBrQHIx9g,44834
27
27
  utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
28
28
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
29
29
  utilities/iterables.py,sha256=mDqw2_0MUVp-P8FklgcaVTi2TXduH0MxbhTDzzhSBho,44915
@@ -61,7 +61,7 @@ utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq
61
61
  utilities/python_dotenv.py,sha256=iWcnpXbH7S6RoXHiLlGgyuH6udCupAcPd_gQ0eAenQ0,3190
62
62
  utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
63
63
  utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
64
- utilities/redis.py,sha256=XywncvQak9AYgdeN2M3vDJM9MEJsIFSfOiWVCKZVGjY,32697
64
+ utilities/redis.py,sha256=pMKJjNI5e0lG-FZh2_idMBBmfgNr53KIGjBquZQOLZc,37556
65
65
  utilities/reprlib.py,sha256=Re9bk3n-kC__9DxQmRlevqFA86pE6TtVfWjUgpbVOv0,1849
66
66
  utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
67
67
  utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
@@ -69,7 +69,7 @@ utilities/sentinel.py,sha256=3jIwgpMekWgDAxPDA_hXMP2St43cPhciKN3LWiZ7kv0,1248
69
69
  utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
70
70
  utilities/slack_sdk.py,sha256=h2DiVkcFyYcT5zzZOAo6CSith5BBlHUbXeOJSL1neK8,5948
71
71
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
72
- utilities/sqlalchemy.py,sha256=KraI3PGrIs8ZpTQi0rZzBMjlcPbgWNipXlLySeu4aiY,36765
72
+ utilities/sqlalchemy.py,sha256=rHixzaYl_QHTR1dAhLj15ntiE5kwZWY4MKIr97crDts,39602
73
73
  utilities/sqlalchemy_polars.py,sha256=s7hQNep2O5DTgIRXyN_JRQma7a4DAtNd25tshaZW8iw,15490
74
74
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
75
75
  utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
@@ -77,7 +77,7 @@ utilities/string.py,sha256=XmU-s04qIV_tODnKl2pQiwmHaxzgOqRKU-RyzdrfvSE,620
77
77
  utilities/sys.py,sha256=h0Xr7Vj86wNalvwJVP1wj5Y0kD_VWm1vzuXZ_jw94mE,2743
78
78
  utilities/tempfile.py,sha256=VqmZJAhTJ1OaVywFzk5eqROV8iJbW9XQ_QYAV0bpdRo,1384
79
79
  utilities/tenacity.py,sha256=1PUvODiBVgeqIh7G5TRt5WWMSqjLYkEqP53itT97WQc,4914
80
- utilities/text.py,sha256=Fo12N4aA7k2rnb4W4vH9iiDh88Q5_nvRssTkfNsvVM8,10965
80
+ utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
81
81
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
82
82
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
83
83
  utilities/traceback.py,sha256=p9WATV-e4_5AW6SvyRBiU-MY8XnEFcKgrFNUoFzalXI,27521
@@ -91,7 +91,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
91
91
  utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
92
92
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
93
93
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
94
- dycw_utilities-0.126.9.dist-info/METADATA,sha256=kgxJx1OLQSkOqyenGUbtVzwcPfk8bt0raQsEypd5FsE,12803
95
- dycw_utilities-0.126.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
- dycw_utilities-0.126.9.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
- dycw_utilities-0.126.9.dist-info/RECORD,,
94
+ dycw_utilities-0.126.11.dist-info/METADATA,sha256=BpSldlx63eNjZ98CKmHpEi6Ee2Zm5HqWnYZrKbBOICI,12804
95
+ dycw_utilities-0.126.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
+ dycw_utilities-0.126.11.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
+ dycw_utilities-0.126.11.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.126.9"
3
+ __version__ = "0.126.11"
utilities/hypothesis.py CHANGED
@@ -8,12 +8,11 @@ from datetime import timezone
8
8
  from enum import Enum, auto
9
9
  from functools import partial
10
10
  from math import ceil, floor, inf, isclose, isfinite, nan
11
- from os import environ, getpid
11
+ from os import environ
12
12
  from pathlib import Path
13
13
  from re import search
14
14
  from string import ascii_letters, ascii_lowercase, ascii_uppercase, digits, printable
15
15
  from subprocess import check_call
16
- from threading import get_ident
17
16
  from typing import (
18
17
  TYPE_CHECKING,
19
18
  Any,
@@ -24,7 +23,6 @@ from typing import (
24
23
  overload,
25
24
  override,
26
25
  )
27
- from uuid import uuid4
28
26
 
29
27
  from hypothesis import HealthCheck, Phase, Verbosity, assume, settings
30
28
  from hypothesis.errors import InvalidArgument
@@ -89,7 +87,6 @@ from utilities.pathlib import temp_cwd
89
87
  from utilities.platform import IS_WINDOWS
90
88
  from utilities.sentinel import Sentinel, sentinel
91
89
  from utilities.tempfile import TEMP_DIR, TemporaryDirectory
92
- from utilities.tzlocal import get_now_local
93
90
  from utilities.version import Version
94
91
  from utilities.zoneinfo import UTC
95
92
 
@@ -1411,20 +1408,6 @@ def uint64s(
1411
1408
  ##
1412
1409
 
1413
1410
 
1414
- @composite
1415
- def unique_strs(draw: DrawFn, /) -> str:
1416
- """Strategy for generating unique strings."""
1417
- now = get_now_local()
1418
- pid = getpid()
1419
- ident = just(get_ident())
1420
- key = str(uuid4()).replace("-", "")
1421
- full = f"{now:%Y%m%d%H%M%S%f}_{pid}_{ident}_{key}"
1422
- return draw(just(full))
1423
-
1424
-
1425
- ##
1426
-
1427
-
1428
1411
  @composite
1429
1412
  def versions(draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> Version:
1430
1413
  """Strategy for generating versions."""
@@ -1553,7 +1536,6 @@ __all__ = [
1553
1536
  "triples",
1554
1537
  "uint32s",
1555
1538
  "uint64s",
1556
- "unique_strs",
1557
1539
  "versions",
1558
1540
  "zoned_datetimes",
1559
1541
  ]
utilities/redis.py CHANGED
@@ -25,7 +25,14 @@ from typing import (
25
25
  from redis.asyncio import Redis
26
26
  from redis.typing import EncodableT
27
27
 
28
- from utilities.asyncio import EnhancedQueue, InfiniteQueueLooper, Looper, timeout_dur
28
+ from utilities.asyncio import (
29
+ EnhancedQueue,
30
+ InfiniteQueueLooper,
31
+ Looper,
32
+ LooperTimeoutError,
33
+ timeout_dur,
34
+ )
35
+ from utilities.contextlib import suppress_super_object_attribute_error
29
36
  from utilities.datetime import (
30
37
  MILLISECOND,
31
38
  SECOND,
@@ -72,6 +79,9 @@ _V2 = TypeVar("_V2")
72
79
  _V3 = TypeVar("_V3")
73
80
 
74
81
 
82
+ _PUBLISH_TIMEOUT: Duration = SECOND
83
+
84
+
75
85
  ##
76
86
 
77
87
 
@@ -548,9 +558,6 @@ def redis_key(
548
558
  ##
549
559
 
550
560
 
551
- _PUBLISH_TIMEOUT: Duration = SECOND
552
-
553
-
554
561
  @overload
555
562
  async def publish(
556
563
  redis: Redis,
@@ -666,7 +673,7 @@ class PublishService(Looper[tuple[str, _T]]):
666
673
  # self
667
674
  redis: Redis
668
675
  serializer: Callable[[_T], EncodableT] = serialize
669
- publish_timeout: Duration = SECOND
676
+ publish_timeout: Duration = _PUBLISH_TIMEOUT
670
677
 
671
678
  @override
672
679
  async def core(self) -> None:
@@ -685,6 +692,55 @@ class PublishService(Looper[tuple[str, _T]]):
685
692
  ##
686
693
 
687
694
 
695
+ @dataclass(kw_only=True)
696
+ class PublishServiceMixin(Generic[_T]):
697
+ """Mix-in for the publish service."""
698
+
699
+ # base - looper
700
+ publish_service_freq: Duration = field(default=MILLISECOND, repr=False)
701
+ publish_service_backoff: Duration = field(default=SECOND, repr=False)
702
+ publish_service_empty_upon_exit: bool = field(default=False, repr=False)
703
+ publish_service_logger: str | None = field(default=None, repr=False)
704
+ publish_service_timeout: Duration | None = field(default=None, repr=False)
705
+ publish_service_timeout_error: type[Exception] = field(
706
+ default=LooperTimeoutError, repr=False
707
+ )
708
+ publish_service_debug: bool = field(default=False, repr=False)
709
+ _is_pending_restart: Event = field(default_factory=Event, init=False, repr=False)
710
+ # base - publish service
711
+ publish_service_redis: Redis
712
+ publish_service_serializer: Callable[[_T], EncodableT] = serialize
713
+ publish_service_publish_timeout: Duration = _PUBLISH_TIMEOUT
714
+ # self
715
+ _publish_service: PublishService[_T] = field(init=False, repr=False)
716
+
717
+ def __post_init__(self) -> None:
718
+ with suppress_super_object_attribute_error(): # skipif-ci-and-not-linux
719
+ super().__post_init__() # pyright: ignore[reportAttributeAccessIssue]
720
+ self._publish_service = PublishService( # skipif-ci-and-not-linux
721
+ # looper
722
+ freq=self.publish_service_freq,
723
+ backoff=self.publish_service_backoff,
724
+ empty_upon_exit=self.publish_service_empty_upon_exit,
725
+ logger=self.publish_service_logger,
726
+ timeout=self.publish_service_timeout,
727
+ timeout_error=self.publish_service_timeout_error,
728
+ _debug=self.publish_service_debug,
729
+ # publish service
730
+ redis=self.publish_service_redis,
731
+ serializer=self.publish_service_serializer,
732
+ publish_timeout=self.publish_service_publish_timeout,
733
+ )
734
+
735
+ def _yield_sub_loopers(self) -> Iterator[Looper[Any]]:
736
+ with suppress_super_object_attribute_error(): # skipif-ci-and-not-linux
737
+ yield from super()._yield_sub_loopers() # pyright: ignore[reportAttributeAccessIssue]
738
+ yield self._publish_service # skipif-ci-and-not-linux
739
+
740
+
741
+ ##
742
+
743
+
688
744
  _SUBSCRIBE_TIMEOUT: Duration = SECOND
689
745
  _SUBSCRIBE_SLEEP: Duration = MILLISECOND
690
746
 
@@ -920,6 +976,58 @@ class SubscribeService(Looper[_T]):
920
976
  ##
921
977
 
922
978
 
979
+ @dataclass(kw_only=True)
980
+ class SubscribeServiceMixin(Generic[_T]):
981
+ """Mix-in for the subscribe service."""
982
+
983
+ # base - looper
984
+ subscribe_service_freq: Duration = field(default=MILLISECOND, repr=False)
985
+ subscribe_service_backoff: Duration = field(default=SECOND, repr=False)
986
+ subscribe_service_empty_upon_exit: bool = field(default=False, repr=False)
987
+ subscribe_service_logger: str | None = field(default=None, repr=False)
988
+ subscribe_service_timeout: Duration | None = field(default=None, repr=False)
989
+ subscribe_service_timeout_error: type[Exception] = field(
990
+ default=LooperTimeoutError, repr=False
991
+ )
992
+ subscribe_service_debug: bool = field(default=False, repr=False)
993
+ # base - looper
994
+ subscribe_service_redis: Redis
995
+ subscribe_service_channel: str
996
+ subscribe_service_deserializer: Callable[[bytes], _T] = deserialize
997
+ subscribe_service_subscribe_sleep: Duration = _SUBSCRIBE_SLEEP
998
+ subscribe_service_subscribe_timeout: Duration | None = _SUBSCRIBE_TIMEOUT
999
+ # self
1000
+ _subscribe_service: SubscribeService[_T] = field(init=False, repr=False)
1001
+
1002
+ def __post_init__(self) -> None:
1003
+ with suppress_super_object_attribute_error(): # skipif-ci-and-not-linux
1004
+ super().__post_init__() # pyright: ignore[reportAttributeAccessIssue]
1005
+ self._subscribe_service = SubscribeService( # skipif-ci-and-not-linux
1006
+ # looper
1007
+ freq=self.subscribe_service_freq,
1008
+ backoff=self.subscribe_service_backoff,
1009
+ empty_upon_exit=self.subscribe_service_empty_upon_exit,
1010
+ logger=self.subscribe_service_logger,
1011
+ timeout=self.subscribe_service_timeout,
1012
+ timeout_error=self.subscribe_service_timeout_error,
1013
+ _debug=self.subscribe_service_debug,
1014
+ # subscribe service
1015
+ redis=self.subscribe_service_redis,
1016
+ channel=self.subscribe_service_channel,
1017
+ deserializer=self.subscribe_service_deserializer,
1018
+ subscribe_sleep=self.subscribe_service_subscribe_sleep,
1019
+ subscribe_timeout=self.subscribe_service_subscribe_timeout,
1020
+ )
1021
+
1022
+ def _yield_sub_loopers(self) -> Iterator[Looper[Any]]:
1023
+ with suppress_super_object_attribute_error(): # skipif-ci-and-not-linux
1024
+ yield from super()._yield_sub_loopers() # pyright: ignore[reportAttributeAccessIssue]
1025
+ yield self._subscribe_service # skipif-ci-and-not-linux
1026
+
1027
+
1028
+ ##
1029
+
1030
+
923
1031
  @asynccontextmanager
924
1032
  async def yield_pubsub(
925
1033
  redis: Redis, channels: MaybeIterable[str], /
@@ -1000,11 +1108,13 @@ def _deserialize(
1000
1108
 
1001
1109
  __all__ = [
1002
1110
  "PublishService",
1111
+ "PublishServiceMixin",
1003
1112
  "Publisher",
1004
1113
  "PublisherError",
1005
1114
  "RedisHashMapKey",
1006
1115
  "RedisKey",
1007
1116
  "SubscribeService",
1117
+ "SubscribeServiceMixin",
1008
1118
  "publish",
1009
1119
  "redis_hash_map_key",
1010
1120
  "redis_key",
utilities/sqlalchemy.py CHANGED
@@ -57,7 +57,13 @@ from sqlalchemy.orm import (
57
57
  from sqlalchemy.orm.exc import UnmappedClassError
58
58
  from sqlalchemy.pool import NullPool, Pool
59
59
 
60
- from utilities.asyncio import InfiniteQueueLooper, Looper, timeout_dur
60
+ from utilities.asyncio import (
61
+ InfiniteQueueLooper,
62
+ Looper,
63
+ LooperTimeoutError,
64
+ timeout_dur,
65
+ )
66
+ from utilities.contextlib import suppress_super_object_attribute_error
61
67
  from utilities.datetime import SECOND
62
68
  from utilities.functions import (
63
69
  ensure_str,
@@ -691,6 +697,63 @@ class UpsertService(Looper[_InsertItem]):
691
697
  )
692
698
 
693
699
 
700
+ @dataclass(kw_only=True)
701
+ class UpsertServiceMixin:
702
+ """Mix-in for the upsert service."""
703
+
704
+ # base - looper
705
+ upsert_service_freq: Duration = field(default=SECOND, repr=False)
706
+ upsert_service_backoff: Duration = field(default=SECOND, repr=False)
707
+ upsert_service_empty_upon_exit: bool = field(default=False, repr=False)
708
+ upsert_service_logger: str | None = field(default=None, repr=False)
709
+ upsert_service_timeout: Duration | None = field(default=None, repr=False)
710
+ upsert_service_timeout_error: type[Exception] = field(
711
+ default=LooperTimeoutError, repr=False
712
+ )
713
+ upsert_service_debug: bool = field(default=False, repr=False)
714
+ # base - upsert service
715
+ upsert_service_database: AsyncEngine
716
+ upsert_service_snake: bool = False
717
+ upsert_service_selected_or_all: _SelectedOrAll = "selected"
718
+ upsert_service_chunk_size_frac: float = CHUNK_SIZE_FRAC
719
+ upsert_service_assume_tables_exist: bool = False
720
+ upsert_service_timeout_create: Duration | None = None
721
+ upsert_service_error_create: type[Exception] = TimeoutError
722
+ upsert_service_timeout_insert: Duration | None = None
723
+ upsert_service_error_insert: type[Exception] = TimeoutError
724
+ # self
725
+ _upsert_service: UpsertService = field(init=False, repr=False)
726
+
727
+ def __post_init__(self) -> None:
728
+ with suppress_super_object_attribute_error():
729
+ super().__post_init__() # pyright: ignore[reportAttributeAccessIssue]
730
+ self._upsert_service = UpsertService(
731
+ # looper
732
+ freq=self.upsert_service_freq,
733
+ backoff=self.upsert_service_backoff,
734
+ empty_upon_exit=self.upsert_service_empty_upon_exit,
735
+ logger=self.upsert_service_logger,
736
+ timeout=self.upsert_service_timeout,
737
+ timeout_error=self.upsert_service_timeout_error,
738
+ _debug=self.upsert_service_debug,
739
+ # upsert service
740
+ engine=self.upsert_service_database,
741
+ snake=self.upsert_service_snake,
742
+ selected_or_all=self.upsert_service_selected_or_all,
743
+ chunk_size_frac=self.upsert_service_chunk_size_frac,
744
+ assume_tables_exist=self.upsert_service_assume_tables_exist,
745
+ timeout_create=self.upsert_service_timeout_create,
746
+ error_create=self.upsert_service_error_create,
747
+ timeout_insert=self.upsert_service_timeout_insert,
748
+ error_insert=self.upsert_service_error_insert,
749
+ )
750
+
751
+ def _yield_sub_loopers(self) -> Iterator[Looper[Any]]:
752
+ with suppress_super_object_attribute_error():
753
+ yield from super()._yield_sub_loopers() # pyright: ignore[reportAttributeAccessIssue]
754
+ yield self._upsert_service
755
+
756
+
694
757
  ##
695
758
 
696
759
 
@@ -1147,6 +1210,7 @@ __all__ = [
1147
1210
  "TablenameMixin",
1148
1211
  "UpsertItemsError",
1149
1212
  "UpsertService",
1213
+ "UpsertServiceMixin",
1150
1214
  "Upserter",
1151
1215
  "UpserterError",
1152
1216
  "check_engine",
utilities/text.py CHANGED
@@ -4,9 +4,13 @@ import re
4
4
  from collections import deque
5
5
  from dataclasses import dataclass
6
6
  from itertools import chain
7
+ from os import getpid
7
8
  from re import IGNORECASE, Match, escape, search
8
9
  from textwrap import dedent
10
+ from threading import get_ident
11
+ from time import time_ns
9
12
  from typing import TYPE_CHECKING, Any, Literal, overload, override
13
+ from uuid import uuid4
10
14
 
11
15
  from utilities.iterables import CheckDuplicatesError, check_duplicates, transpose
12
16
  from utilities.reprlib import get_repr
@@ -395,6 +399,18 @@ def strip_and_dedent(text: str, /, *, trailing: bool = False) -> str:
395
399
  return f"{result}\n" if trailing else result
396
400
 
397
401
 
402
+ ##
403
+
404
+
405
+ def unique_str() -> str:
406
+ """Generate at unique string."""
407
+ now = time_ns()
408
+ pid = getpid()
409
+ ident = get_ident()
410
+ key = str(uuid4()).replace("-", "")
411
+ return f"{now}_{pid}_{ident}_{key}"
412
+
413
+
398
414
  __all__ = [
399
415
  "BRACKETS",
400
416
  "DEFAULT_SEPARATOR",
@@ -413,4 +429,5 @@ __all__ = [
413
429
  "split_str",
414
430
  "str_encode",
415
431
  "strip_and_dedent",
432
+ "unique_str",
416
433
  ]