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.
- stranded/__init__.py +51 -0
- stranded/abc/__init__.py +21 -0
- stranded/abc/composer_.py +191 -0
- stranded/argparse/__init__.py +24 -0
- stranded/argparse/abc/__init__.py +21 -0
- stranded/argparse/abc/argument_parser_.py +539 -0
- stranded/argparse/argument_parser_.py +13 -0
- stranded/argparse/asyncio/__init__.py +24 -0
- stranded/argparse/asyncio/argument_parser_.py +50 -0
- stranded/argparse/threading/__init__.py +24 -0
- stranded/argparse/threading/argument_parser_.py +52 -0
- stranded/asyncio/__init__.py +21 -0
- stranded/asyncio/composer_.py +76 -0
- stranded/builtins/__init__.py +21 -0
- stranded/builtins/exception_.py +6 -0
- stranded/composer_.py +27 -0
- stranded/execution/__init__.py +30 -0
- stranded/execution/abc/__init__.py +21 -0
- stranded/execution/abc/scheduler_.py +59 -0
- stranded/execution/asyncio/__init__.py +21 -0
- stranded/execution/asyncio/scheduler_.py +86 -0
- stranded/execution/scheduler_.py +59 -0
- stranded/execution/threading/__init__.py +21 -0
- stranded/execution/threading/scheduler_.py +116 -0
- stranded/functools/__init__.py +51 -0
- stranded/functools/abc/__init__.py +39 -0
- stranded/functools/abc/herd_.py +96 -0
- stranded/functools/abc/lru_cache_.py +192 -0
- stranded/functools/abc/retry_.py +60 -0
- stranded/functools/abc/throttle_.py +97 -0
- stranded/functools/asyncio/__init__.py +51 -0
- stranded/functools/asyncio/herd_.py +89 -0
- stranded/functools/asyncio/lru_cache_.py +92 -0
- stranded/functools/asyncio/retry_.py +43 -0
- stranded/functools/asyncio/throttle_.py +74 -0
- stranded/functools/herd_.py +18 -0
- stranded/functools/lru_cache_.py +18 -0
- stranded/functools/retry_.py +13 -0
- stranded/functools/threading/__init__.py +51 -0
- stranded/functools/threading/herd_.py +88 -0
- stranded/functools/threading/lru_cache_.py +90 -0
- stranded/functools/threading/retry_.py +42 -0
- stranded/functools/threading/throttle_.py +75 -0
- stranded/functools/throttle_.py +13 -0
- stranded/logging/__init__.py +24 -0
- stranded/logging/abc/__init__.py +21 -0
- stranded/logging/abc/logger_.py +96 -0
- stranded/logging/asyncio/__init__.py +21 -0
- stranded/logging/asyncio/logger_.py +41 -0
- stranded/logging/logger_.py +13 -0
- stranded/logging/threading/__init__.py +21 -0
- stranded/logging/threading/logger_.py +41 -0
- stranded/py.typed +0 -0
- stranded/sqlite3/__init__.py +24 -0
- stranded/sqlite3/abc/__init__.py +21 -0
- stranded/sqlite3/abc/db_.py +149 -0
- stranded/sqlite3/asyncio/__init__.py +24 -0
- stranded/sqlite3/asyncio/db_.py +43 -0
- stranded/sqlite3/db_.py +13 -0
- stranded/sqlite3/threading/__init__.py +24 -0
- stranded/sqlite3/threading/db_.py +43 -0
- stranded/threading/__init__.py +21 -0
- stranded/threading/composer_.py +74 -0
- stranded/types/__init__.py +21 -0
- stranded/types/constraint.py +7 -0
- stranded/types/convert.py +158 -0
- stranded-0.0.1.dist-info/METADATA +173 -0
- stranded-0.0.1.dist-info/RECORD +71 -0
- stranded-0.0.1.dist-info/WHEEL +5 -0
- stranded-0.0.1.dist-info/licenses/LICENSE +21 -0
- 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()
|