stranded 0.0.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.
Files changed (71) hide show
  1. stranded/__init__.py +51 -0
  2. stranded/abc/__init__.py +21 -0
  3. stranded/abc/composer_.py +191 -0
  4. stranded/argparse/__init__.py +24 -0
  5. stranded/argparse/abc/__init__.py +21 -0
  6. stranded/argparse/abc/argument_parser_.py +539 -0
  7. stranded/argparse/argument_parser_.py +13 -0
  8. stranded/argparse/asyncio/__init__.py +24 -0
  9. stranded/argparse/asyncio/argument_parser_.py +50 -0
  10. stranded/argparse/threading/__init__.py +24 -0
  11. stranded/argparse/threading/argument_parser_.py +52 -0
  12. stranded/asyncio/__init__.py +21 -0
  13. stranded/asyncio/composer_.py +76 -0
  14. stranded/builtins/__init__.py +21 -0
  15. stranded/builtins/exception_.py +6 -0
  16. stranded/composer_.py +27 -0
  17. stranded/execution/__init__.py +30 -0
  18. stranded/execution/abc/__init__.py +21 -0
  19. stranded/execution/abc/scheduler_.py +59 -0
  20. stranded/execution/asyncio/__init__.py +21 -0
  21. stranded/execution/asyncio/scheduler_.py +86 -0
  22. stranded/execution/scheduler_.py +59 -0
  23. stranded/execution/threading/__init__.py +21 -0
  24. stranded/execution/threading/scheduler_.py +116 -0
  25. stranded/functools/__init__.py +51 -0
  26. stranded/functools/abc/__init__.py +39 -0
  27. stranded/functools/abc/herd_.py +96 -0
  28. stranded/functools/abc/lru_cache_.py +192 -0
  29. stranded/functools/abc/retry_.py +60 -0
  30. stranded/functools/abc/throttle_.py +97 -0
  31. stranded/functools/asyncio/__init__.py +51 -0
  32. stranded/functools/asyncio/herd_.py +89 -0
  33. stranded/functools/asyncio/lru_cache_.py +92 -0
  34. stranded/functools/asyncio/retry_.py +43 -0
  35. stranded/functools/asyncio/throttle_.py +74 -0
  36. stranded/functools/herd_.py +18 -0
  37. stranded/functools/lru_cache_.py +18 -0
  38. stranded/functools/retry_.py +13 -0
  39. stranded/functools/threading/__init__.py +51 -0
  40. stranded/functools/threading/herd_.py +88 -0
  41. stranded/functools/threading/lru_cache_.py +90 -0
  42. stranded/functools/threading/retry_.py +42 -0
  43. stranded/functools/threading/throttle_.py +75 -0
  44. stranded/functools/throttle_.py +13 -0
  45. stranded/logging/__init__.py +24 -0
  46. stranded/logging/abc/__init__.py +21 -0
  47. stranded/logging/abc/logger_.py +96 -0
  48. stranded/logging/asyncio/__init__.py +21 -0
  49. stranded/logging/asyncio/logger_.py +41 -0
  50. stranded/logging/logger_.py +13 -0
  51. stranded/logging/threading/__init__.py +21 -0
  52. stranded/logging/threading/logger_.py +41 -0
  53. stranded/py.typed +0 -0
  54. stranded/sqlite3/__init__.py +24 -0
  55. stranded/sqlite3/abc/__init__.py +21 -0
  56. stranded/sqlite3/abc/db_.py +149 -0
  57. stranded/sqlite3/asyncio/__init__.py +24 -0
  58. stranded/sqlite3/asyncio/db_.py +43 -0
  59. stranded/sqlite3/db_.py +13 -0
  60. stranded/sqlite3/threading/__init__.py +24 -0
  61. stranded/sqlite3/threading/db_.py +43 -0
  62. stranded/threading/__init__.py +21 -0
  63. stranded/threading/composer_.py +74 -0
  64. stranded/types/__init__.py +21 -0
  65. stranded/types/constraint.py +7 -0
  66. stranded/types/convert.py +158 -0
  67. stranded-0.0.1.dist-info/METADATA +173 -0
  68. stranded-0.0.1.dist-info/RECORD +71 -0
  69. stranded-0.0.1.dist-info/WHEEL +5 -0
  70. stranded-0.0.1.dist-info/licenses/LICENSE +21 -0
  71. stranded-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib as _importlib
4
+ import typing as _typing
5
+
6
+ if _typing.TYPE_CHECKING:
7
+ from . import herd_
8
+ from .herd_ import Herd
9
+ from . import lru_cache_
10
+ from .lru_cache_ import LruCache
11
+ from . import retry_
12
+ from .retry_ import Retry
13
+ from . import throttle_
14
+ from .throttle_ import Throttle
15
+
16
+
17
+ def __getattr__(name: str) -> _typing.Any:
18
+ match name:
19
+ case 'herd_': return _importlib.import_module('.herd_', __name__)
20
+ case 'Herd': return _importlib.import_module('.herd_', __name__).Herd
21
+ case 'lru_cache_': return _importlib.import_module('.lru_cache_', __name__)
22
+ case 'LruCache': return _importlib.import_module('.lru_cache_', __name__).LruCache
23
+ case 'retry_': return _importlib.import_module('.retry_', __name__)
24
+ case 'Retry': return _importlib.import_module('.retry_', __name__).Retry
25
+ case 'throttle_': return _importlib.import_module('.throttle_', __name__)
26
+ case 'Throttle': return _importlib.import_module('.throttle_', __name__).Throttle
27
+ case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
28
+
29
+
30
+ __all__ = (
31
+ 'herd_',
32
+ 'Herd',
33
+ 'lru_cache_',
34
+ 'LruCache',
35
+ 'retry_',
36
+ 'Retry',
37
+ 'throttle_',
38
+ 'Throttle',
39
+ )
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import dataclasses
5
+ import typing
6
+ import weakref
7
+
8
+ from ...abc import composer_
9
+
10
+
11
+ type GenerateKey = typing.Callable[..., Key]
12
+ type Key = typing.Hashable
13
+
14
+
15
+ Raise = composer_.Raise
16
+ Stop = composer_.Stop
17
+ Param = composer_.Param
18
+ Return = composer_.Return
19
+
20
+
21
+ @dataclasses.dataclass(frozen=True, kw_only=True)
22
+ class Future[RetT](abc.ABC):
23
+
24
+ @abc.abstractmethod
25
+ def set_value(self, value: Return[RetT] | Raise | Stop) -> None: ...
26
+
27
+ @abc.abstractmethod
28
+ def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: ...
29
+
30
+ def __get__(self, instance: composer_.Instance, owner: type[object] | None) -> typing.Self:
31
+ return self
32
+
33
+
34
+ @typing.runtime_checkable
35
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol): ...
36
+
37
+
38
+ @dataclasses.dataclass(frozen=True, kw_only=True)
39
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC): ...
40
+
41
+
42
+ @dataclasses.dataclass(frozen=True, kw_only=True)
43
+ class Exit[**ParamT, RetT, FutureT](composer_.Exit[ParamT, RetT], abc.ABC):
44
+ future: FutureT
45
+ key: Key
46
+
47
+
48
+ @dataclasses.dataclass(frozen=True, kw_only=True)
49
+ class Enter[**ParamT, RetT, FutureT](composer_.Enter[ParamT, RetT], abc.ABC):
50
+ # The in-flight-call cache lives on the Enter now that Enter/Exit no longer reach Composed.
51
+ future_by_key: dict[Key, FutureT] = dataclasses.field(default_factory=dict)
52
+
53
+ @staticmethod
54
+ def create_key(*args: ParamT.args, **kwargs: ParamT.kwargs) -> Key:
55
+ return tuple(args), tuple(sorted([*kwargs.items()]))
56
+
57
+ def _dispatch(
58
+ self, *args: ParamT.args, **kwargs: ParamT.kwargs,
59
+ ) -> tuple[Exit[ParamT, RetT, Future[RetT]], Composee[ParamT, RetT]] | tuple[Future[RetT]]:
60
+ key = self.create_key(*args, **kwargs)
61
+ future = self.future_by_key.get(key)
62
+ match future is None:
63
+ case True:
64
+ future = self.future_by_key[key] = self.composer.future_t() # type: ignore[attr-defined]
65
+ return self.composer.Exit(enter=self, future=future, key=key), self.composee # type: ignore[call-arg, return-value]
66
+ case False:
67
+ return future,
68
+ assert False, "Unreachable"
69
+
70
+
71
+ @dataclasses.dataclass(frozen=True, kw_only=True)
72
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC):
73
+ # Enters rather than whole bound compositions: a bound composition holds the bound composee,
74
+ # which refers back to the instance that keys it here, and so would outlive it forever.
75
+ enter_by_instance: weakref.WeakKeyDictionary[
76
+ composer_.Instance, composer_.EnterT[ParamT, RetT],
77
+ ] = dataclasses.field(default_factory=weakref.WeakKeyDictionary)
78
+
79
+ @typing.override
80
+ def create_enter(self, instance: composer_.Instance) -> composer_.EnterT[ParamT, RetT]:
81
+ if (enter := self.enter_by_instance.get(instance)) is not None:
82
+ return enter
83
+ match self.enter:
84
+ case Enter() as enter_:
85
+ return self.enter_by_instance.setdefault(instance, dataclasses.replace(enter_, future_by_key={}))
86
+ assert False, "unreachable"
87
+
88
+
89
+ @dataclasses.dataclass(frozen=True, kw_only=True)
90
+ class Herd[**ParamT, RetT](composer_.Composer[ParamT, RetT], abc.ABC):
91
+ @property
92
+ @abc.abstractmethod
93
+ def future_t(self) -> type: ...
94
+
95
+
96
+ Composer = Herd
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import collections
5
+ import contextlib
6
+ import dataclasses
7
+ import itertools
8
+ import typing
9
+ import weakref
10
+ import sys
11
+
12
+ from ...abc import composer_
13
+ from ...builtins import exception_
14
+
15
+
16
+ type GenerateKey = typing.Callable[..., Key]
17
+ type FutureByKey = collections.OrderedDict[Key, typing.Any]
18
+
19
+
20
+ Raise = composer_.Raise
21
+ Stop = composer_.Stop
22
+ Param = composer_.Param
23
+ Return = composer_.Return
24
+
25
+
26
+ class Exception(exception_.Exception): ... # noqa
27
+
28
+
29
+ @typing.final
30
+ class Ref[T](weakref.ref[T]):
31
+ """Weak reference to a memoized argument that evicts its cache entry when the referent dies.
32
+
33
+ Hashes and compares like the referent while the referent is alive, so that `Key`s built from
34
+ equal arguments are equal. The referent's hash is cached at construction, which keeps the key
35
+ hashable - and therefore removable from the cache - after the referent is gone.
36
+ """
37
+ __slots__ = ('future_by_key', 'key')
38
+
39
+ future_by_key: weakref.ref[FutureByKey] | None
40
+ key: Key | None
41
+
42
+ def __new__(cls, referent: T) -> typing.Self:
43
+ return super().__new__(cls, referent, cls.__discard)
44
+
45
+ def __init__(self, referent: T) -> None:
46
+ super().__init__(referent, type(self).__discard) # type: ignore[call-arg]
47
+ self.future_by_key = None
48
+ self.key = None
49
+ hash(self) # Cache the referent's hash while the referent is still alive.
50
+
51
+ def __discard(self) -> None:
52
+ # Called with this (now dead) reference as `self` once the referent is garbage collected.
53
+ if (future_by_key_ref := self.future_by_key) is None or (key := self.key) is None:
54
+ return
55
+ if (future_by_key := future_by_key_ref()) is not None:
56
+ future_by_key.pop(key, None)
57
+
58
+ def bind(self, future_by_key: weakref.ref[FutureByKey], key: Key) -> None:
59
+ """Discard `key` from `future_by_key` once the referent dies."""
60
+ self.future_by_key = future_by_key
61
+ self.key = key
62
+
63
+
64
+ def create_ref(value: typing.Any) -> typing.Hashable:
65
+ """Refer to `value` weakly if it supports weak references, and strongly otherwise."""
66
+ try:
67
+ return Ref(value)
68
+ except TypeError:
69
+ # `value` either does not support weak references (e.g. `int`, `str`, and `tuple`) or is
70
+ # unhashable, in which case caching it fails the same way an unhashable key always has.
71
+ return typing.cast(typing.Hashable, value)
72
+
73
+
74
+ @typing.final
75
+ @dataclasses.dataclass(frozen=True, kw_only=True)
76
+ class Key:
77
+ """Cache key that refers to the memoized arguments weakly wherever it can.
78
+
79
+ Keeping arguments alive for as long as they are memoized would leak every argument that a cache
80
+ ever saw, so arguments that support weak references are stored as `Ref`s. Note that an argument
81
+ that supports weak references only indirectly - a `tuple` of objects, say - is still stored
82
+ strongly, since there is nothing to hang a reference on.
83
+ """
84
+ args: tuple[typing.Hashable, ...]
85
+ kwargs: tuple[tuple[str, typing.Hashable], ...]
86
+
87
+ @classmethod
88
+ def create(cls, *args: typing.Any, **kwargs: typing.Any) -> typing.Self:
89
+ return cls(
90
+ args=tuple(create_ref(arg) for arg in args),
91
+ kwargs=tuple((name, create_ref(kwargs[name])) for name in sorted(kwargs)),
92
+ )
93
+
94
+ @property
95
+ def refs(self) -> typing.Iterator[Ref[typing.Any]]:
96
+ """The weak references that this key is made of."""
97
+ return (
98
+ value
99
+ for value in itertools.chain(self.args, (value for _, value in self.kwargs))
100
+ if isinstance(value, Ref)
101
+ )
102
+
103
+ def bind(self, future_by_key: FutureByKey) -> None:
104
+ """Discard this key from `future_by_key` as soon as any of its weak arguments dies."""
105
+ future_by_key_ref = weakref.ref(future_by_key)
106
+ for ref in self.refs:
107
+ ref.bind(future_by_key_ref, self)
108
+
109
+
110
+ @dataclasses.dataclass(frozen=True, kw_only=True)
111
+ class Future[RetT](abc.ABC):
112
+
113
+ @abc.abstractmethod
114
+ def set_value(self, value: Return[RetT] | Raise | Stop) -> None: ...
115
+
116
+ @abc.abstractmethod
117
+ def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: ...
118
+
119
+ def __get__(self, instance: composer_.Instance, owner: type[object] | None) -> typing.Self:
120
+ return self
121
+
122
+
123
+ @typing.runtime_checkable
124
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol): ...
125
+
126
+
127
+ @dataclasses.dataclass(frozen=True, kw_only=True)
128
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC): ...
129
+
130
+
131
+ @dataclasses.dataclass(frozen=True, kw_only=True)
132
+ class Exit[**ParamT, RetT, FutureT](composer_.Exit[ParamT, RetT], abc.ABC):
133
+ future: FutureT
134
+ key: Key
135
+
136
+
137
+ @dataclasses.dataclass(frozen=True, kw_only=True)
138
+ class Enter[**ParamT, RetT, FutureT](composer_.Enter[ParamT, RetT], abc.ABC):
139
+ future_by_key: collections.OrderedDict[Key, FutureT] = dataclasses.field(default_factory=collections.OrderedDict)
140
+
141
+ @staticmethod
142
+ def create_key(*args: ParamT.args, **kwargs: ParamT.kwargs) -> Key:
143
+ return Key.create(*args, **kwargs)
144
+
145
+ def get_future(self, key: Key) -> FutureT | None:
146
+ """Return the future memoized for `key`, if any, marking it as the most recently used."""
147
+ if (future := self.future_by_key.get(key)) is not None:
148
+ # A weak argument may have died - taking its entry with it - since the get.
149
+ with contextlib.suppress(KeyError):
150
+ self.future_by_key.move_to_end(key)
151
+ return future
152
+
153
+ def set_future(self, key: Key) -> FutureT:
154
+ """Memoize and return a new future for `key`, evicting the least recently used entries."""
155
+ while self.composer.size <= len(self.future_by_key): # type: ignore[attr-defined]
156
+ self.future_by_key.popitem(last=False)
157
+ future: FutureT = self.composer.future_t() # type: ignore[attr-defined]
158
+ self.future_by_key[key] = future
159
+ key.bind(self.future_by_key)
160
+ return future
161
+
162
+
163
+ @dataclasses.dataclass(frozen=True, kw_only=True)
164
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC):
165
+ # Enters rather than whole bound compositions: a bound composition holds the bound composee,
166
+ # which refers back to the instance that keys it here, and so would outlive it forever.
167
+ enter_by_instance: weakref.WeakKeyDictionary[
168
+ composer_.Instance, composer_.EnterT[ParamT, RetT],
169
+ ] = dataclasses.field(default_factory=weakref.WeakKeyDictionary)
170
+
171
+ @typing.override
172
+ def create_enter(self, instance: composer_.Instance) -> composer_.EnterT[ParamT, RetT]:
173
+ if (enter := self.enter_by_instance.get(instance)) is not None:
174
+ return enter
175
+ match self.enter:
176
+ case Enter() as enter_:
177
+ return self.enter_by_instance.setdefault(
178
+ instance, dataclasses.replace(enter_, future_by_key=collections.OrderedDict()),
179
+ )
180
+ assert False, "unreachable"
181
+
182
+
183
+ @dataclasses.dataclass(frozen=True, kw_only=True)
184
+ class LruCache[**ParamT, RetT](composer_.Composer[ParamT, RetT], abc.ABC):
185
+ size: int = sys.maxsize
186
+
187
+ @property
188
+ @abc.abstractmethod
189
+ def future_t(self) -> type: ...
190
+
191
+
192
+ Composer = LruCache
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import dataclasses
5
+ import sys
6
+ import typing
7
+
8
+ from ...abc import composer_
9
+ from ...builtins import exception_
10
+
11
+
12
+ class Exception(exception_.Exception): ... # noqa
13
+
14
+
15
+ @typing.runtime_checkable
16
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol): ...
17
+
18
+
19
+ Param = composer_.Param
20
+ Raise = composer_.Raise
21
+ Return = composer_.Return
22
+ Stop = composer_.Stop
23
+
24
+
25
+ @dataclasses.dataclass(frozen=True, kw_only=True)
26
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC): ...
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True, kw_only=True)
30
+ class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], abc.ABC):
31
+ def __call__(self, value: composer_.ValueT[ParamT, RetT], /) -> composer_.StackT:
32
+ if self.enter.n_retried < self.enter.composer.n and isinstance(value, Raise): # type: ignore[attr-defined]
33
+ return dataclasses.replace(self.enter, n_retried=self.enter.n_retried + 1), self.enter.param # type: ignore[attr-defined, call-arg]
34
+
35
+ return ()
36
+
37
+
38
+ @dataclasses.dataclass(frozen=True, kw_only=True)
39
+ class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], abc.ABC):
40
+ n_retried: int = 0
41
+ param: Param[ParamT] | None = None
42
+
43
+ def __call__(self, value: composer_.ValueT[ParamT, RetT], /) -> composer_.StackT:
44
+ match value:
45
+ case Param() as param_:
46
+ new_self = dataclasses.replace(self, param=param_)
47
+ return new_self.composer.Exit(enter=new_self), self.composee
48
+ case _: return ()
49
+
50
+
51
+ @dataclasses.dataclass(frozen=True, kw_only=True)
52
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC): ...
53
+
54
+
55
+ @dataclasses.dataclass(frozen=True, kw_only=True)
56
+ class Retry[**ParamT, RetT](composer_.Composer[ParamT, RetT], abc.ABC):
57
+ n: int = sys.maxsize
58
+
59
+
60
+ Composer = Retry
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import dataclasses
5
+ import threading
6
+ import sys
7
+ import typing
8
+ import weakref
9
+
10
+ from ...abc import composer_
11
+ from ...builtins import exception_
12
+
13
+
14
+ class Exception(exception_.Exception): ... # noqa
15
+
16
+
17
+ @dataclasses.dataclass(kw_only=True)
18
+ class State:
19
+ cap_running: int = 1
20
+ num_running: int = 0
21
+ num_waiting: int = 0
22
+
23
+
24
+ @typing.runtime_checkable
25
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol): ...
26
+
27
+
28
+ Param = composer_.Param
29
+ Raise = composer_.Raise
30
+ Return = composer_.Return
31
+ Stop = composer_.Stop
32
+
33
+
34
+ @dataclasses.dataclass(frozen=True, kw_only=True)
35
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC): ...
36
+
37
+
38
+ @dataclasses.dataclass(frozen=True, kw_only=True)
39
+ class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], abc.ABC):
40
+ def __call__(self, value: composer_.ValueT[ParamT, RetT], /) -> composer_.StackT:
41
+ state = self.enter.state # type: ignore[attr-defined]
42
+ if isinstance(value, Raise) and state.num_running <= state.cap_running:
43
+ state.cap_running //= 2
44
+ elif (
45
+ not isinstance(value, Raise)
46
+ and state.num_running == state.cap_running < self.enter.composer.max_running # type: ignore[attr-defined]
47
+ ):
48
+ state.cap_running += 1
49
+ state.num_running -= 1
50
+
51
+ if 0 < (n := state.cap_running - state.num_running):
52
+ self.enter.condition.notify(n=n) # type: ignore[attr-defined]
53
+
54
+ return ()
55
+
56
+
57
+ @dataclasses.dataclass(frozen=True, kw_only=True)
58
+ class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], abc.ABC):
59
+ # Per-composition state lives on the Enter now that Enter/Exit no longer reach Composed.
60
+ # Exit reads it back through self.enter; __get__ reinstalls a fresh-state Enter per instance.
61
+ state: State = dataclasses.field(default_factory=State)
62
+
63
+
64
+ @dataclasses.dataclass(frozen=True, kw_only=True)
65
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC):
66
+ # Enters rather than whole bound compositions: a bound composition holds the bound composee,
67
+ # which refers back to the instance that keys it here, and so would outlive it forever.
68
+ enter_by_instance: weakref.WeakKeyDictionary[
69
+ composer_.Instance, composer_.EnterT[ParamT, RetT],
70
+ ] = dataclasses.field(default_factory=weakref.WeakKeyDictionary)
71
+ lock: threading.Lock = dataclasses.field(default_factory=threading.Lock)
72
+
73
+ @typing.override
74
+ def create_enter(self, instance: composer_.Instance) -> composer_.EnterT[ParamT, RetT]:
75
+ with self.lock:
76
+ if (enter := self.enter_by_instance.get(instance)) is not None:
77
+ return enter
78
+ match self.enter:
79
+ case Enter() as enter_:
80
+ return self.enter_by_instance.setdefault(instance, dataclasses.replace(
81
+ enter_,
82
+ condition=type(enter_.condition)(), # type: ignore[attr-defined]
83
+ state=State(),
84
+ ))
85
+ assert False, "unreachable"
86
+
87
+
88
+ @dataclasses.dataclass(frozen=True, kw_only=True)
89
+ class Throttle[**ParamT, RetT](composer_.Composer[ParamT, RetT], abc.ABC):
90
+ # How many callees are allowed through concurrently before additional callees become waiters.
91
+ max_running: int = sys.maxsize
92
+
93
+ # How many callees are allowed through or to wait concurrently before additional callees are rejected.
94
+ max_waiting: int = sys.maxsize
95
+
96
+
97
+ Composer = Throttle
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib as _importlib
4
+ import typing as _typing
5
+
6
+ if _typing.TYPE_CHECKING:
7
+ from . import herd_
8
+ from .herd_ import Herd
9
+ from .herd_ import herd
10
+ from . import lru_cache_
11
+ from .lru_cache_ import LruCache
12
+ from .lru_cache_ import lru_cache
13
+ from . import retry_
14
+ from .retry_ import Retry
15
+ from .retry_ import retry
16
+ from . import throttle_
17
+ from .throttle_ import Throttle
18
+ from .throttle_ import throttle
19
+
20
+
21
+ def __getattr__(name: str) -> _typing.Any:
22
+ match name:
23
+ case 'herd_': return _importlib.import_module('.herd_', __name__)
24
+ case 'Herd': return _importlib.import_module('.herd_', __name__).Herd
25
+ case 'herd': return _importlib.import_module('.herd_', __name__).herd
26
+ case 'lru_cache_': return _importlib.import_module('.lru_cache_', __name__)
27
+ case 'LruCache': return _importlib.import_module('.lru_cache_', __name__).LruCache
28
+ case 'lru_cache': return _importlib.import_module('.lru_cache_', __name__).lru_cache
29
+ case 'retry_': return _importlib.import_module('.retry_', __name__)
30
+ case 'Retry': return _importlib.import_module('.retry_', __name__).Retry
31
+ case 'retry': return _importlib.import_module('.retry_', __name__).retry
32
+ case 'throttle_': return _importlib.import_module('.throttle_', __name__)
33
+ case 'Throttle': return _importlib.import_module('.throttle_', __name__).Throttle
34
+ case 'throttle': return _importlib.import_module('.throttle_', __name__).throttle
35
+ case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
36
+
37
+
38
+ __all__ = (
39
+ 'herd_',
40
+ 'Herd',
41
+ 'herd',
42
+ 'lru_cache_',
43
+ 'LruCache',
44
+ 'lru_cache',
45
+ 'retry_',
46
+ 'Retry',
47
+ 'retry',
48
+ 'throttle_',
49
+ 'Throttle',
50
+ 'throttle',
51
+ )
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import typing
4
+
5
+ from ..abc import herd_
6
+ from ...asyncio import composer_
7
+
8
+
9
+ Raise = composer_.Raise
10
+ Stop = composer_.Stop
11
+ Param = composer_.Param
12
+ Return = composer_.Return
13
+
14
+
15
+ @typing.runtime_checkable
16
+ class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], herd_.Composee[ParamT, RetT], typing.Protocol): ...
17
+
18
+
19
+ @typing.final
20
+ @dataclasses.dataclass(frozen=True, kw_only=True)
21
+ class Future[RetT](herd_.Future[RetT]):
22
+ future: asyncio.Future[RetT] = dataclasses.field(default_factory=asyncio.Future)
23
+
24
+ @typing.override
25
+ def set_value(self, value: Return[RetT] | Raise | Stop) -> None:
26
+ match value:
27
+ case Return(ret=ret): self.future.set_result(ret)
28
+ case Raise() as raise_: self.future.set_exception(raise_.exc_val)
29
+ case Stop() as stop_: self.future.set_exception(stop_)
30
+
31
+ @typing.override
32
+ async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> RetT:
33
+ return await self.future
34
+
35
+
36
+ @typing.final
37
+ @dataclasses.dataclass(frozen=True, kw_only=True)
38
+ class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], herd_.Connect[ParamT, RetT]): ...
39
+
40
+
41
+ @typing.final
42
+ @dataclasses.dataclass(frozen=True, kw_only=True)
43
+ class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], herd_.Exit[ParamT, RetT, Future[RetT]]):
44
+ future: Future[RetT] = dataclasses.field(default_factory=Future)
45
+
46
+ @typing.override
47
+ async def __call__(self, value: composer_.ValueT[ParamT, RetT], /) -> composer_.StackT: # type: ignore[override]
48
+ self.enter.future_by_key.pop(self.key, None) # type: ignore[attr-defined]
49
+ match value:
50
+ case Param(): pass
51
+ case Return() | Raise() | Stop(): self.future.set_value(value)
52
+
53
+ return ()
54
+
55
+
56
+ @typing.final
57
+ @dataclasses.dataclass(frozen=True, kw_only=True)
58
+ class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], herd_.Enter[ParamT, RetT, Future[RetT]]):
59
+ @typing.override
60
+ async def __call__( # type: ignore[override]
61
+ self, value: composer_.ValueT[ParamT, RetT], /,
62
+ ) -> composer_.StackT:
63
+ match value:
64
+ case Param(): return self._dispatch(*value.args, **value.kwargs) # type: ignore[return-value]
65
+ case _: return ()
66
+
67
+
68
+ @typing.final
69
+ @dataclasses.dataclass(frozen=True, kw_only=True)
70
+ class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], herd_.Composed[ParamT, RetT]): ...
71
+
72
+
73
+ @typing.final
74
+ @dataclasses.dataclass(frozen=True, kw_only=True)
75
+ class Herd[**ParamT = ..., RetT = typing.Any](composer_.Composer[ParamT, RetT], herd_.Herd[ParamT, RetT]):
76
+ Composee: typing.ClassVar = Composee
77
+ Connect: typing.ClassVar = Connect
78
+ Exit: typing.ClassVar = Exit # type: ignore[assignment]
79
+ Enter: typing.ClassVar = Enter
80
+ Composed: typing.ClassVar = Composed
81
+
82
+ @property
83
+ @typing.override
84
+ def future_t(self) -> type[Future[RetT]]:
85
+ return Future
86
+
87
+
88
+ Composer = Herd
89
+ herd = Herd()