python-wrap 0.2.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.
- python_wrap/__init__.py +272 -0
- python_wrap/_core.py +212 -0
- python_wrap/aio.py +620 -0
- python_wrap/batching.py +556 -0
- python_wrap/bulkhead.py +431 -0
- python_wrap/caching.py +1613 -0
- python_wrap/circuit.py +553 -0
- python_wrap/concurrency.py +1886 -0
- python_wrap/config.py +217 -0
- python_wrap/deadline.py +168 -0
- python_wrap/decorators.py +7 -0
- python_wrap/devtools.py +385 -0
- python_wrap/errors.py +86 -0
- python_wrap/events.py +161 -0
- python_wrap/hedging.py +192 -0
- python_wrap/logs.py +767 -0
- python_wrap/metrics.py +242 -0
- python_wrap/periodic.py +491 -0
- python_wrap/policy.py +263 -0
- python_wrap/py.typed +0 -0
- python_wrap/retry.py +1324 -0
- python_wrap/state.py +500 -0
- python_wrap/testing.py +662 -0
- python_wrap/timing.py +535 -0
- python_wrap/validation.py +662 -0
- python_wrap-0.2.1.dist-info/METADATA +485 -0
- python_wrap-0.2.1.dist-info/RECORD +29 -0
- python_wrap-0.2.1.dist-info/WHEEL +4 -0
- python_wrap-0.2.1.dist-info/licenses/LICENSE +21 -0
python_wrap/policy.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Reusable, named bundles of decorators that always stack in the right order."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
+
from typing import Any, NoReturn, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
9
|
+
|
|
10
|
+
__all__ = ["ROLES", "Policy", "policy"]
|
|
11
|
+
|
|
12
|
+
_Decorator = Callable[[Any], Any]
|
|
13
|
+
|
|
14
|
+
#: Role names, outermost first. The canonical order, with the reason for each position:
|
|
15
|
+
#:
|
|
16
|
+
#: - ``observe``: timing, logging, audit see the outcome the caller experiences.
|
|
17
|
+
#: - ``validate``: reject bad input before anything is retried, cached or counted.
|
|
18
|
+
#: - ``fallback``: catches whatever the layers below could not fix.
|
|
19
|
+
#: - ``cache``: a hit skips everything below; failures are never cached.
|
|
20
|
+
#: - ``limiter``: one rate-limit token per logical call, not per attempt.
|
|
21
|
+
#: - ``bulkhead``: caps concurrent logical calls into the dependency.
|
|
22
|
+
#: - ``breaker``: counts calls whose retries were exhausted.
|
|
23
|
+
#: - ``retry``: repeats everything below it.
|
|
24
|
+
#: - ``timeout``: a deadline per attempt.
|
|
25
|
+
#: - ``guard``: per-attempt state protection (revert_on_failure, transactional).
|
|
26
|
+
ROLES: tuple[str, ...] = (
|
|
27
|
+
"observe",
|
|
28
|
+
"validate",
|
|
29
|
+
"fallback",
|
|
30
|
+
"cache",
|
|
31
|
+
"limiter",
|
|
32
|
+
"bulkhead",
|
|
33
|
+
"breaker",
|
|
34
|
+
"retry",
|
|
35
|
+
"timeout",
|
|
36
|
+
"guard",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _as_tuple(role: str, value: Any) -> tuple[_Decorator, ...]:
|
|
41
|
+
if value is None:
|
|
42
|
+
return ()
|
|
43
|
+
items = tuple(value) if isinstance(value, (list, tuple)) else (value,)
|
|
44
|
+
for item in items:
|
|
45
|
+
if not callable(item):
|
|
46
|
+
raise TypeError(f"policy {role}= must be a decorator or a list of them; got {item!r}")
|
|
47
|
+
return items
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _apply_one(decorator: _Decorator, target: Any) -> Any:
|
|
51
|
+
"""Apply a decorator, looking through staticmethod/classmethod descriptors."""
|
|
52
|
+
if isinstance(target, (staticmethod, classmethod)):
|
|
53
|
+
return type(target)(decorator(target.__func__))
|
|
54
|
+
return decorator(target)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Policy:
|
|
58
|
+
"""An immutable, named bundle of decorators applied in a fixed, correct order.
|
|
59
|
+
|
|
60
|
+
Create one with `policy`. A `Policy` is itself a decorator, so it works on
|
|
61
|
+
plain functions, ``async def`` functions and methods, and keeps the
|
|
62
|
+
decorated function's type. Roles run outermost first in the order of `ROLES`,
|
|
63
|
+
wherever you pass them; `outer` decorators wrap all roles and `inner`
|
|
64
|
+
decorators sit inside all roles, each group in the order given.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
```python
|
|
68
|
+
resilient = policy(retry=retry(max_attempts=3), timeout=timeout(2), name="io")
|
|
69
|
+
|
|
70
|
+
@resilient
|
|
71
|
+
def fetch() -> str: ...
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
__slots__ = ("_inner", "_name", "_outer", "_roles")
|
|
76
|
+
|
|
77
|
+
_name: str | None
|
|
78
|
+
_outer: tuple[_Decorator, ...]
|
|
79
|
+
_inner: tuple[_Decorator, ...]
|
|
80
|
+
_roles: dict[str, tuple[_Decorator, ...]]
|
|
81
|
+
|
|
82
|
+
def __reduce__(self) -> NoReturn:
|
|
83
|
+
raise TypeError(
|
|
84
|
+
"cannot pickle a Policy: it holds decorator closures. Define the policy at "
|
|
85
|
+
"module level so each process builds it on import; functions decorated "
|
|
86
|
+
"with it pickle by reference."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
name: str | None,
|
|
93
|
+
outer: tuple[_Decorator, ...],
|
|
94
|
+
inner: tuple[_Decorator, ...],
|
|
95
|
+
roles: Mapping[str, tuple[_Decorator, ...]],
|
|
96
|
+
) -> None:
|
|
97
|
+
object.__setattr__(self, "_name", name)
|
|
98
|
+
object.__setattr__(self, "_outer", outer)
|
|
99
|
+
object.__setattr__(self, "_inner", inner)
|
|
100
|
+
object.__setattr__(self, "_roles", dict(roles))
|
|
101
|
+
|
|
102
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
103
|
+
raise AttributeError("Policy is immutable; use .with_(...) to derive a variant")
|
|
104
|
+
|
|
105
|
+
def __delattr__(self, key: str) -> None:
|
|
106
|
+
raise AttributeError("Policy is immutable; use .with_(...) to derive a variant")
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def name(self) -> str | None:
|
|
110
|
+
"""The policy name, if any."""
|
|
111
|
+
return self._name
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def order(self) -> tuple[str, ...]:
|
|
115
|
+
"""The final layer names, outermost first (``outer``, roles, ``inner``)."""
|
|
116
|
+
names: list[str] = ["outer"] * len(self._outer)
|
|
117
|
+
for role in ROLES:
|
|
118
|
+
names += [role] * len(self._roles.get(role, ()))
|
|
119
|
+
names += ["inner"] * len(self._inner)
|
|
120
|
+
return tuple(names)
|
|
121
|
+
|
|
122
|
+
def _layers(self) -> list[_Decorator]:
|
|
123
|
+
layers: list[_Decorator] = list(self._outer)
|
|
124
|
+
for role in ROLES:
|
|
125
|
+
layers.extend(self._roles.get(role, ()))
|
|
126
|
+
layers.extend(self._inner)
|
|
127
|
+
return layers
|
|
128
|
+
|
|
129
|
+
def apply(self, func: F) -> F:
|
|
130
|
+
"""Decorate `func` with every layer in the policy's order.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
func: A function, ``async def`` function, or static/class method.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The decorated function (same type as `func`).
|
|
137
|
+
"""
|
|
138
|
+
result: Any = func
|
|
139
|
+
for decorator in reversed(self._layers()):
|
|
140
|
+
result = _apply_one(decorator, result)
|
|
141
|
+
return cast(F, result)
|
|
142
|
+
|
|
143
|
+
def __call__(self, func: F) -> F:
|
|
144
|
+
return self.apply(func)
|
|
145
|
+
|
|
146
|
+
def with_(self, **changes: Any) -> Policy:
|
|
147
|
+
"""Return a new policy with some roles replaced.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
**changes: Any role name, `outer`, `inner` or `name`. A value of
|
|
151
|
+
``None`` removes that role; otherwise it replaces it.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A new `Policy`; this one is unchanged.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
TypeError: For an unknown keyword or a non-callable decorator.
|
|
158
|
+
"""
|
|
159
|
+
known = {*ROLES, "outer", "inner", "name"}
|
|
160
|
+
unknown = set(changes) - known
|
|
161
|
+
if unknown:
|
|
162
|
+
raise TypeError(f"unknown policy option(s): {', '.join(sorted(unknown))}")
|
|
163
|
+
roles = dict(self._roles)
|
|
164
|
+
for role in ROLES:
|
|
165
|
+
if role in changes:
|
|
166
|
+
roles[role] = _as_tuple(role, changes[role])
|
|
167
|
+
return Policy(
|
|
168
|
+
name=changes.get("name", self._name),
|
|
169
|
+
outer=_as_tuple("outer", changes["outer"]) if "outer" in changes else self._outer,
|
|
170
|
+
inner=_as_tuple("inner", changes["inner"]) if "inner" in changes else self._inner,
|
|
171
|
+
roles=roles,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def __repr__(self) -> str:
|
|
175
|
+
order = " > ".join(self.order) or "(empty)"
|
|
176
|
+
label = f"{self._name!r}, " if self._name else ""
|
|
177
|
+
return f"Policy({label}{order})"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def policy(
|
|
181
|
+
*,
|
|
182
|
+
name: str | None = None,
|
|
183
|
+
observe: _Decorator | Sequence[_Decorator] | None = None,
|
|
184
|
+
validate: _Decorator | Sequence[_Decorator] | None = None,
|
|
185
|
+
fallback: _Decorator | Sequence[_Decorator] | None = None,
|
|
186
|
+
cache: _Decorator | Sequence[_Decorator] | None = None,
|
|
187
|
+
limiter: _Decorator | Sequence[_Decorator] | None = None,
|
|
188
|
+
bulkhead: _Decorator | Sequence[_Decorator] | None = None,
|
|
189
|
+
breaker: _Decorator | Sequence[_Decorator] | None = None,
|
|
190
|
+
retry: _Decorator | Sequence[_Decorator] | None = None,
|
|
191
|
+
timeout: _Decorator | Sequence[_Decorator] | None = None,
|
|
192
|
+
guard: _Decorator | Sequence[_Decorator] | None = None,
|
|
193
|
+
outer: _Decorator | Sequence[_Decorator] | None = None,
|
|
194
|
+
inner: _Decorator | Sequence[_Decorator] | None = None,
|
|
195
|
+
) -> Policy:
|
|
196
|
+
"""Build a reusable `Policy` from decorators placed in named roles.
|
|
197
|
+
|
|
198
|
+
Each keyword is a role, and the role (not the keyword order) decides where the
|
|
199
|
+
decorator sits. The canonical order, outermost first, is `ROLES`: ``observe``
|
|
200
|
+
(timing, logging), ``validate``, ``fallback``, ``cache``, ``limiter``
|
|
201
|
+
(rate limits), ``bulkhead``, ``breaker`` (a `CircuitBreaker` or
|
|
202
|
+
`circuit_breaker(...)`), ``retry``, ``timeout``, ``guard``
|
|
203
|
+
(revert_on_failure and similar). The reasons are in the stacking guide.
|
|
204
|
+
|
|
205
|
+
A role takes one decorator or a list of them (outermost first). Use `outer`
|
|
206
|
+
and `inner` for any decorator that has no role: `outer` ones wrap every role,
|
|
207
|
+
`inner` ones sit just above the function.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
name: Label shown in ``repr``.
|
|
211
|
+
observe: Observers such as `timing` or `audit`.
|
|
212
|
+
validate: Input checks such as `validate_args`.
|
|
213
|
+
fallback: `fallback` or `suppress`.
|
|
214
|
+
cache: `memoize`, `once`, `singleflight`.
|
|
215
|
+
limiter: `rate_limit`, `throttle`.
|
|
216
|
+
bulkhead: a `Bulkhead` or `limit_concurrency(n)`.
|
|
217
|
+
breaker: A `CircuitBreaker` or `circuit_breaker(...)`.
|
|
218
|
+
retry: `retry(...)`.
|
|
219
|
+
timeout: `timeout(...)`.
|
|
220
|
+
guard: Per-attempt state protection.
|
|
221
|
+
outer: Unclassified decorators placed outside every role.
|
|
222
|
+
inner: Unclassified decorators placed inside every role.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
An immutable `Policy`, usable as a decorator.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
TypeError: If an argument is not callable (or a list of callables).
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
```python
|
|
232
|
+
from python_wrap import CircuitBreaker, retry, timeout, policy
|
|
233
|
+
|
|
234
|
+
resilient = policy(
|
|
235
|
+
timeout=timeout(2),
|
|
236
|
+
retry=retry(max_attempts=3, on=ConnectionError),
|
|
237
|
+
breaker=CircuitBreaker(failure_threshold=5, on=ConnectionError),
|
|
238
|
+
name="payments",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@resilient
|
|
242
|
+
def charge(amount: int) -> int:
|
|
243
|
+
return amount
|
|
244
|
+
```
|
|
245
|
+
"""
|
|
246
|
+
given = {
|
|
247
|
+
"observe": observe,
|
|
248
|
+
"validate": validate,
|
|
249
|
+
"fallback": fallback,
|
|
250
|
+
"cache": cache,
|
|
251
|
+
"limiter": limiter,
|
|
252
|
+
"bulkhead": bulkhead,
|
|
253
|
+
"breaker": breaker,
|
|
254
|
+
"retry": retry,
|
|
255
|
+
"timeout": timeout,
|
|
256
|
+
"guard": guard,
|
|
257
|
+
}
|
|
258
|
+
return Policy(
|
|
259
|
+
name=name,
|
|
260
|
+
outer=_as_tuple("outer", outer),
|
|
261
|
+
inner=_as_tuple("inner", inner),
|
|
262
|
+
roles={role: _as_tuple(role, value) for role, value in given.items()},
|
|
263
|
+
)
|
python_wrap/py.typed
ADDED
|
File without changes
|