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.
@@ -0,0 +1,485 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-wrap
3
+ Version: 0.2.1
4
+ Summary: Everyday Python decorators that just work: retry, memoize, timeout, rate limiting, circuit breakers and more. Typed, async-aware, zero dependencies.
5
+ Project-URL: Homepage, https://github.com/LitePy/PythonWrap
6
+ Project-URL: Documentation, https://pythonwrap.readthedocs.io
7
+ Project-URL: Source, https://github.com/LitePy/PythonWrap
8
+ Project-URL: Issues, https://github.com/LitePy/PythonWrap/issues
9
+ Project-URL: Changelog, https://github.com/LitePy/PythonWrap/blob/main/CHANGELOG.md
10
+ Author-email: Mahmoud Ahmed <pythondeveloper6@gmail.com>
11
+ Maintainer-email: Mahmoud Ahmed <pythondeveloper6@gmail.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: asyncio,cache,circuit-breaker,debounce,decorators,memoize,rate-limit,retry,throttle,timeout,utilities
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Framework :: AsyncIO
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Topic :: Utilities
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown
31
+
32
+ <div align="center">
33
+
34
+ <a href="https://pythonwrap.readthedocs.io">
35
+ <picture>
36
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/LitePy/PythonWrap/main/docs/assets/logo-dark.svg">
37
+ <img alt="python-wrap" src="https://raw.githubusercontent.com/LitePy/PythonWrap/main/docs/assets/logo-light.svg" height="64">
38
+ </picture>
39
+ </a>
40
+
41
+ **The resilience and concurrency toolkit you add to every Python project.**<br>
42
+ Retry (with `Retry-After`), circuit breaker, timeout, cache, rate limit, bulkhead, batching, async helpers, metrics and test helpers. One import, sync and async, zero dependencies.
43
+
44
+ [![CI](https://github.com/LitePy/PythonWrap/actions/workflows/ci.yml/badge.svg)](https://github.com/LitePy/PythonWrap/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/python-wrap?logo=pypi&logoColor=white)](https://pypi.org/project/python-wrap/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/python-wrap?logo=python&logoColor=white)](https://pypi.org/project/python-wrap/)
47
+ [![License: MIT](https://img.shields.io/pypi/l/python-wrap)](https://github.com/LitePy/PythonWrap/blob/main/LICENSE)
48
+ [![Docs](https://readthedocs.org/projects/pythonwrap/badge/?version=latest)](https://pythonwrap.readthedocs.io)
49
+ [![Typed: mypy strict](https://img.shields.io/badge/typed-mypy%20strict-2a6db2)](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict)
50
+
51
+ [**Documentation**](https://pythonwrap.readthedocs.io) ·
52
+ [Cookbook](https://pythonwrap.readthedocs.io/en/latest/cookbook/) ·
53
+ [Performance](https://pythonwrap.readthedocs.io/en/latest/project/performance/) ·
54
+ [Migration guide](https://pythonwrap.readthedocs.io/en/latest/project/migration/) ·
55
+ [Changelog](https://github.com/LitePy/PythonWrap/blob/main/CHANGELOG.md)
56
+
57
+ </div>
58
+
59
+
60
+ ## Why python-wrap
61
+
62
+ - **One import for the things every project hand-writes.** Retry, circuit breaker, timeout, cache, rate limit, bulkhead, batching, bounded `gather`, metrics and test helpers, designed to stack and to be tested together.
63
+ - **Resilience that behaves.** `retry` honours `Retry-After`, a time budget and cancellation; a shared `deadline()` caps a whole request; `policy(...)` stacks the protections in the right order for you.
64
+ - **Zero dependencies.** Pure Python, nothing to audit or pin.
65
+ - **Typed.** Decorators preserve the wrapped signature (`ParamSpec`), and stacking keeps the exact type (`@retry` over `@memoize` still has `.cache_info()`). Ships `py.typed`, checked with `mypy --strict`.
66
+ - **Sync and async, one API.** Works on `def`, `async def` and methods. Async wrappers stay coroutine functions and never block the event loop.
67
+ - **Fast.** Cache hits and validation skip signature binding on the hot path ([numbers](https://pythonwrap.readthedocs.io/en/latest/project/performance/)).
68
+ - **Observable and testable.** One event stream for retries, cache, circuit, limiters and timeouts; `python_wrap.testing` makes tests instant and deterministic.
69
+ - **Never configures your logging**, is thread-safe, and is tested on Python 3.10 to 3.14 on Linux, macOS and Windows.
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install python-wrap
75
+ # or
76
+ uv add python-wrap
77
+ ```
78
+
79
+ Python 3.10 or newer.
80
+
81
+ ## 30-second tour
82
+
83
+ ```python
84
+ import asyncio
85
+ import time
86
+
87
+ from python_wrap import (
88
+ CircuitBreaker, gather_limit, memoize, policy, rate_limit, retry, retry_after_header, timeout,
89
+ )
90
+ from python_wrap.testing import fast_mode
91
+
92
+ # One reusable stack for a flaky HTTP dependency. Roles decide the order, not you.
93
+ http = policy(
94
+ cache=memoize(ttl=60, maxsize=256), # hits skip everything below
95
+ breaker=CircuitBreaker(failure_threshold=5, recovery_timeout=30, name="api"),
96
+ retry=retry(max_attempts=4, delay=0.5, backoff=2, jitter=0.1,
97
+ on=ConnectionError, delay_from=retry_after_header, max_time=20),
98
+ timeout=timeout(5), # per attempt
99
+ name="http",
100
+ )
101
+
102
+
103
+ @http
104
+ def fetch_user(user_id: int) -> dict[str, int]:
105
+ return {"id": user_id} # your real HTTP call goes here
106
+
107
+
108
+ print(fetch_user(1), fetch_user(1)) # the second one is a cache hit
109
+ print(fetch_user.cache_info()) # the cache API survives the stack
110
+
111
+
112
+ @rate_limit(100, 1.0, block=True) # at most 100 calls per second, waits when full
113
+ async def fetch_async(user_id: int) -> dict[str, int]:
114
+ await asyncio.sleep(0.01)
115
+ return {"id": user_id}
116
+
117
+
118
+ async def main() -> None:
119
+ # 50 requests, never more than 20 in flight; results come back in order.
120
+ users = await gather_limit(*(fetch_async(i) for i in range(50)), limit=20)
121
+ print(len(users), "users")
122
+
123
+
124
+ asyncio.run(main())
125
+
126
+
127
+ # In tests, every wait is instant but retries still run the configured number of times.
128
+ calls = []
129
+
130
+
131
+ @retry(max_attempts=4, delay=30, backoff=2, on=ConnectionError)
132
+ def flaky() -> str:
133
+ calls.append(1)
134
+ if len(calls) < 4:
135
+ raise ConnectionError
136
+ return "ok"
137
+
138
+
139
+ start = time.monotonic()
140
+ with fast_mode():
141
+ assert flaky() == "ok" # would take 30 + 60 + 120 seconds
142
+ print(f"{time.monotonic() - start:.2f}s")
143
+ ```
144
+
145
+ ## What is in the box
146
+
147
+ Everything below is importable from `python_wrap` unless a module is shown. [Choose a decorator](https://pythonwrap.readthedocs.io/en/latest/get-started/choosing/) is the quick way in.
148
+
149
+ | Area | Name | What it does |
150
+ |---|---|---|
151
+ | **Resilience** | `retry` | Backoff, jitter, `max_delay`, `max_time`, `retry_if`, `retry_if_result`, `on_retry` (async hooks too); `delay_from=retry_after_header` honours `Retry-After`; never retries cancellation |
152
+ | | `RetryBudget` | One retry allowance shared across functions, so a failing dependency does not trigger a retry storm |
153
+ | | `retrying`, `Retrying` | Retry an inline block (`for attempt in retrying(...)`, sync and `async for`) |
154
+ | | `retry_stats`, `RetryStats` | Attempts, successes and failures per retried function |
155
+ | | `fallback`, `suppress` | Return a default (or handler result), or swallow chosen exceptions |
156
+ | | `circuit_breaker`, `CircuitBreaker` | Fail fast while a dependency is down; `success_threshold`, `exclude`, `on_state_change`, `.stats`; one breaker can guard many functions |
157
+ | | `timeout`, `Timeout` | Decorator or `with timeout(5):` / `async with`; raises `TimeoutExpired`; `cleanup_grace=` bounds async cleanup |
158
+ | | `deadline`, `Deadline` | A shared time budget; `retry` and `timeout` clamp to it |
159
+ | | `hedge` | Fire a backup call when the first is slow and take the first success (idempotent calls only) |
160
+ | **Async toolkit** | `gather_limit`, `map_async`, `amap_iter` | Bounded concurrency, lazy input, ordered results; everything is cancelled and awaited on error |
161
+ | | `race`, `RaceFailed` | First success wins, the rest are cancelled |
162
+ | | `run_sync` | Call async code from sync code (refuses inside a running loop) |
163
+ | **Caching** | `memoize` | LRU + TTL, `typed`, `key=`, per-item `ttl=`, `cache_exceptions=`, `stale_ttl=` (serve stale while refreshing), `per_instance=True`, `cache_info/clear/delete/contains/get`; async in-flight dedupe |
164
+ | | `CacheStore` | Protocol to keep `memoize` entries in your own store (dict, Redis, ...) |
165
+ | | `cached_property`, `invalidate` | `functools.cached_property` plus `ttl`, locking and `async def` getters |
166
+ | | `once`, `cache` | Run once and reuse (thread-safe, `.reset()`); `cache` is the v0.1 name for `memoize(ttl=...)` |
167
+ | **Concurrency & limits** | `rate_limit`, `RateLimiter` | Sliding window, `block=True`, `key=`, several limits, one budget shared across functions |
168
+ | | `Bulkhead`, `limit_concurrency` | Cap concurrent calls, bound the queue, raise `BulkheadFull` |
169
+ | | `throttle`, `debounce` | At most once per interval; run after calls stop (`.cancel()`, `.flush()`) |
170
+ | | `singleflight`, `run_in_thread` | Share one execution between identical concurrent calls; run in a thread, get a `Future` |
171
+ | | `periodic` | Run a function every N seconds in the background, without drift, with clean `start()`/`stop()` |
172
+ | **Batching** | `batch_calls` | DataLoader pattern: many single-item calls become one bulk call; sync and async, deduped |
173
+ | **Observability & metrics** | `count_calls` | `.stats`: calls, errors, in-flight, mean, p50, p95, p99 |
174
+ | | `subscribe`, `listening` | One event hook for retry, cache, circuit, limiter, bulkhead and timeout events (`python_wrap.events` also has `filter_events`, `EVENT_NAMES`) |
175
+ | | `log_args`, `log_return`, `trace`, `audit` | Structured, redacted logging; `AuditRecord` to a sink; `trace(otel=True)` opens an OpenTelemetry span |
176
+ | | `log_context`, `current_log_context` | Contextvar fields in every log line and audit record |
177
+ | | `timing`, `profile`, `measure_memory`, `benchmark` | Duration with `threshold=`/`callback=`, `cProfile`, `tracemalloc`, `benchmark_async` |
178
+ | **Validation** | `enforce_types`, `validate_args`, `check_type` | Check from annotations, with predicates, or with explicit types |
179
+ | **State** | `transactional`, `Transaction`, `current_transaction` | Commit and rollback callbacks, usable as a (async) context manager |
180
+ | | `revert_on_failure` | Restore an object's attributes if the call raises |
181
+ | **Configuration** | `configure`, `configured`, `get_config`, `reset_config` | Project-wide defaults for logger, retry options and redaction |
182
+ | | `policy`, `Policy` | Named, immutable decorator bundles applied in the recommended order |
183
+ | **Testing** (`python_wrap.testing`) | `fast_mode`, `disable_retries`, `disable_rate_limits`, `disable_caching`, `overrides` | Skip waits, cap retries, switch limiters and caches off |
184
+ | | `FakeClock`, `fake_clock`, `spy`, `spy_on`, `eventually` | Control time, record calls, wait for background work; pytest fixtures `pw_fake_clock`, `pw_fast_mode` |
185
+ | **Dev tools** | `deprecate`, `mock_data`, `compose`, `wrap_methods` | Deprecation warnings at the caller, fixed test data, decorator bundles, decorate a whole class |
186
+
187
+ Exceptions live in `python_wrap.errors` and at the top level: `PythonWrapError`, `TimeoutExpired` (a `TimeoutError`), `RateLimitExceeded`, `CircuitOpenError`, `BulkheadFull`, `BatchError`, `ValidationError` (a `ValueError`) and `ArgumentTypeError` (a `TypeError`).
188
+
189
+ ## Highlights
190
+
191
+ ### Retry, `Retry-After` and fail-soft
192
+
193
+ ```python
194
+ from python_wrap import RetryState, retry
195
+
196
+
197
+ def note(state: RetryState) -> None:
198
+ print(f"attempt {state.attempt} failed: {state.exception!r}")
199
+
200
+
201
+ calls = 0
202
+
203
+
204
+ @retry(
205
+ max_attempts=5,
206
+ delay=0.01, # wait before the 2nd attempt
207
+ backoff=2.0, # then 2x longer each time
208
+ max_delay=0.1, # but never more than this
209
+ jitter=0.01, # plus up to this many random seconds
210
+ max_time=5, # and never spend longer than this in total
211
+ on=(ConnectionError, TimeoutError),
212
+ retry_if=lambda exc: "temporary" in str(exc),
213
+ on_retry=note,
214
+ )
215
+ def flaky() -> str:
216
+ global calls
217
+ calls += 1
218
+ if calls < 3:
219
+ raise ConnectionError("temporary glitch")
220
+ return "ok"
221
+
222
+
223
+ print(flaky()) # ok, after two retries
224
+ ```
225
+
226
+ Fail soft with `fallback(value)` or `suppress(OSError)`. The last exception is re-raised unchanged once attempts run out. `max_attempts=None` retries forever. Cancellation (`asyncio.CancelledError`, `KeyboardInterrupt`) is never retried. Add `delay_from=retry_after_header` to wait exactly as long as a 429/503 `Retry-After` header says, and retry a block of code with `for attempt in retrying(...)`.
227
+
228
+ ### Circuit breaker, timeout and deadline
229
+
230
+ ```python
231
+ import time
232
+
233
+ from python_wrap import CircuitBreaker, CircuitOpenError, CircuitState, TimeoutExpired, deadline, timeout
234
+
235
+ payments = CircuitBreaker(failure_threshold=2, recovery_timeout=30, name="payments")
236
+
237
+
238
+ @payments
239
+ def charge(amount: int) -> str:
240
+ raise ConnectionError("gateway down")
241
+
242
+
243
+ for _ in range(2):
244
+ try:
245
+ charge(10)
246
+ except ConnectionError:
247
+ pass
248
+
249
+ try:
250
+ charge(10)
251
+ except CircuitOpenError:
252
+ print("failing fast, circuit is", payments.state.value) # no call was made
253
+ assert payments.state is CircuitState.OPEN
254
+
255
+
256
+ @timeout(1)
257
+ def step(seconds: float) -> None:
258
+ time.sleep(seconds)
259
+
260
+
261
+ try:
262
+ with deadline(0.05): # one budget for everything inside; timeouts clamp to it
263
+ step(0.01)
264
+ step(1)
265
+ except TimeoutExpired:
266
+ print("out of time")
267
+ ```
268
+
269
+ After `recovery_timeout` the breaker goes half-open and lets a trial call through. Share one `CircuitBreaker` across functions for one dependency, or use `@circuit_breaker(...)` for a private one (`func.breaker`). `timeout` interrupts sync code with `SIGALRM` on the Unix main thread, cancels `async def` with `asyncio.wait_for`, and elsewhere falls back to a thread that cannot stop the work (see [Limitations](https://pythonwrap.readthedocs.io/en/latest/project/limitations/)).
270
+
271
+ ### Caching
272
+
273
+ ```python
274
+ from python_wrap import memoize
275
+
276
+
277
+ @memoize(maxsize=128, ttl=30)
278
+ def square(n: int, *, scale: int = 1) -> int:
279
+ return n * n * scale
280
+
281
+
282
+ square(3)
283
+ square(n=3) # same entry: arguments are bound to the signature
284
+ square(3, scale=1) # still the same entry
285
+ print(square.cache_info()) # 2 hits, 1 miss
286
+ square.cache_delete(3) # drop one entry
287
+ square.cache_clear() # drop everything
288
+
289
+ ```
290
+
291
+ `cached_property(ttl=)` does the same for attributes. Keys are built from the bound signature, lists, dicts and sets are converted automatically, and exceptions are not cached unless you ask (`cache_exceptions=True, exception_ttl=`). Concurrent awaits of the same missing key in an `async def` share one in-flight task. On methods the cache keeps instances alive, so give it `maxsize` or `ttl`.
292
+
293
+ ### Async toolkit, bulkheads and batching
294
+
295
+ ```python
296
+ import asyncio
297
+
298
+ from python_wrap import Bulkhead, BulkheadFull, batch_calls, map_async, race
299
+
300
+ queries: list[list[int]] = []
301
+
302
+
303
+ @batch_calls(max_size=100, max_wait=0.005) # the DataLoader pattern
304
+ async def get_user(ids: list[int]) -> dict[int, str]:
305
+ queries.append(ids) # one round trip for the whole batch
306
+ return {i: f"user{i}" for i in ids}
307
+
308
+
309
+ db = Bulkhead(2, max_waiting=1, name="db") # 2 running, 1 queued, the rest rejected
310
+
311
+
312
+ @db
313
+ async def query(n: int) -> int:
314
+ await asyncio.sleep(0.01)
315
+ return n
316
+
317
+
318
+ async def main() -> None:
319
+ users = await asyncio.gather(get_user(1), get_user(2), get_user(1))
320
+ assert users == ["user1", "user2", "user1"] and queries == [[1, 2]]
321
+
322
+ doubled = await map_async(lambda n: query(n * 2), range(3), limit=2)
323
+ assert doubled == [0, 2, 4]
324
+
325
+ results = await asyncio.gather(*(query(n) for n in range(4)), return_exceptions=True)
326
+ assert isinstance(results[3], BulkheadFull)
327
+
328
+ async def slow() -> str:
329
+ await asyncio.sleep(1)
330
+ return "slow"
331
+
332
+ async def fast() -> str:
333
+ return "fast"
334
+
335
+ assert await race(slow(), fast()) == "fast" # the loser is cancelled
336
+
337
+
338
+ asyncio.run(main())
339
+ ```
340
+
341
+ Every helper cancels and awaits the tasks it started, so nothing leaks on error or cancellation. Also see `hedge` for backup requests on slow reads and `run_sync` for calling async code from scripts.
342
+
343
+ ### Rate limits, throttle and debounce
344
+
345
+ ```python
346
+ from python_wrap import RateLimitExceeded, RateLimiter, rate_limit
347
+
348
+
349
+ @rate_limit(2, 0.2, key=lambda user, *_: user) # 2 calls / 0.2 s, per user
350
+ def send(user: str, text: str) -> str:
351
+ return f"{user}: {text}"
352
+
353
+
354
+ send("ann", "a")
355
+ send("ann", "b")
356
+ try:
357
+ send("ann", "c")
358
+ except RateLimitExceeded as exc:
359
+ print(f"slow down, retry in {exc.retry_after:.2f}s")
360
+ send("bob", "hello") # different key, different budget
361
+
362
+ github = RateLimiter(5, 1.0, name="github") # one budget shared by several functions
363
+
364
+
365
+ @github
366
+ def get_repo(name: str) -> str:
367
+ return name
368
+
369
+ ```
370
+
371
+ `rate_limit(block=True)` waits instead of raising (with `await` for `async def`), and `limits=[(5, 1), (600, 900)]` enforces several windows at once. `throttle` and `debounce` cover "at most once per interval" and "after the burst settles". The sliding window never lets a `2 * calls` burst through at a window edge.
372
+
373
+ ### Metrics and events
374
+
375
+ ```python
376
+ from python_wrap import count_calls, listening, retry
377
+
378
+
379
+ @count_calls
380
+ @retry(max_attempts=3, delay=0)
381
+ def work(attempts: list[int]) -> str:
382
+ attempts.append(1)
383
+ if len(attempts) < 3:
384
+ raise ConnectionError
385
+ return "ok"
386
+
387
+
388
+ names: list[str] = []
389
+ with listening(lambda event: names.append(event.name)):
390
+ work([])
391
+
392
+ print(names) # retry.attempt_failed, ..., retry.succeeded
393
+ print(work.stats.calls, work.stats.p95)
394
+ ```
395
+
396
+ Subscribe once and forward events to Prometheus, OpenTelemetry or statsd (recipes in the [guide](https://pythonwrap.readthedocs.io/en/latest/features/observability/metrics/)). Listeners cannot break the decorated function.
397
+
398
+ ### Validation and logging with redaction
399
+
400
+ ```python
401
+ import logging
402
+
403
+ from python_wrap import ArgumentTypeError, enforce_types, log_args
404
+
405
+ logging.basicConfig(level=logging.DEBUG, format="%(message)s") # your app configures logging
406
+
407
+
408
+ @enforce_types
409
+ def total(prices: list[float], currency: str = "USD") -> str:
410
+ return f"{sum(prices):.2f} {currency}"
411
+
412
+
413
+ print(total([1.5, 2.5]))
414
+ try:
415
+ total([1.5, "2.5"]) # type: ignore[list-item]
416
+ except ArgumentTypeError as exc:
417
+ print(exc) # parameter 'prices' expected list[float], got list containing str at index 1
418
+
419
+
420
+ @log_args(logger="myapp.auth")
421
+ def login(username: str, password: str, api_key: str = "") -> bool:
422
+ return True
423
+
424
+
425
+ login("ann", "hunter2", api_key="sk-123")
426
+ # Calling login(username='ann', password='***', api_key='***')
427
+ ```
428
+
429
+ Parameters whose names contain `password`, `token`, `secret`, `api_key` and similar (`DEFAULT_REDACT`) are masked in `log_args` and `audit`; extend with `redact_extra=` or project-wide through `configure(redact_extra=...)`. For custom rules use `validate_args(x=lambda x: x > 0)`.
430
+
431
+ ### Transactions and defaults
432
+
433
+ `@transactional` gives a function a `tx: Transaction` (declare it last or keyword-only) to register `on_commit` and `on_rollback` callbacks; `current_transaction()` reaches it from anywhere. `configure(logger=..., redact_extra=...)` sets project-wide defaults read at decoration time, and explicit arguments always win.
434
+
435
+ ## Stacking decorators and policies
436
+
437
+ Decorators are applied bottom-up and run top-down, so order matters:
438
+
439
+ | Put this... | ...outside of this | Because |
440
+ |---|---|---|
441
+ | `memoize` | `retry`, `rate_limit`, `timeout` | Cache hits skip everything; only misses pay |
442
+ | `retry` | `timeout`, `circuit_breaker`, `rate_limit` | Each attempt gets its own deadline |
443
+ | `circuit_breaker` | `retry` or `timeout` | The breaker counts calls whose retries were used up |
444
+ | `fallback` | `retry`, `circuit_breaker` | Fall back only after retries are exhausted or the circuit is open |
445
+ | `log_args`, `timing`, `audit` | everything | They observe what callers actually experience |
446
+ | `enforce_types`, `validate_args` | the rest | Reject bad input before doing any work |
447
+
448
+ You do not have to memorise this. `policy(observe=, validate=, fallback=, cache=, limiter=, bulkhead=, breaker=, retry=, timeout=, guard=)` takes each decorator by role and applies them in the recommended order, whatever order you wrote them in. Policies are immutable (`.with_(timeout=timeout(30))`), work on methods and `async def`, and `wrap_methods(policy)` protects a whole class. `compose(a, b, c)(f)` is `a(b(c(f)))` when you just want a named stack. Details: [Stacking](https://pythonwrap.readthedocs.io/en/latest/get-started/stacking/) and [Policies](https://pythonwrap.readthedocs.io/en/latest/features/resilience/policies/).
449
+
450
+ ## How it compares
451
+
452
+ python-wrap is a broad toolkit; the others are deeper in one area. Use them when that one area is all you need and you want the specialist.
453
+
454
+ | Library | Use it instead when... | python-wrap's angle |
455
+ |---|---|---|
456
+ | [tenacity](https://github.com/jd/tenacity) | You need very rich retry policies: composable stop/wait strategies, per-call overrides | `retry` covers the common cases (backoff, jitter, `Retry-After`, time budget, hooks, async) with a flat signature, and shares a deadline, breaker and events with the rest |
457
+ | [stamina](https://github.com/hynek/stamina) | You want opinionated, production-ready retries with Prometheus and structlog integration built in | Similar defaults-first retry, plus the breaker, timeout, caches and limits it composes with; metrics via a generic event hook |
458
+ | [backoff](https://github.com/litl/backoff) | You already use it and are happy. The repository was archived in 2025, so it no longer receives fixes | A maintained, typed retry with the same core ideas |
459
+ | [cachetools](https://github.com/tkem/cachetools) | You need other eviction policies (LFU, FIFO, RR), explicit cache objects or custom locks | `memoize` gives LRU + TTL, `cache_info()`, async in-flight dedupe, signature-bound keys and `cached_property(ttl=)` |
460
+ | [async-lru](https://github.com/aio-libs/async-lru) | You only cache coroutines and want a tiny dependency | Same async dedupe, also for sync code, with TTL, per-item TTL and exception caching |
461
+ | [aiolimiter](https://github.com/mjpieters/aiolimiter) | You only need an asyncio leaky-bucket limiter | Sliding window for sync and async, per-key buckets, shared `RateLimiter`, `block=True`, plus `Bulkhead` for concurrency |
462
+ | [pybreaker](https://github.com/danielfm/pybreaker) | You need listeners or circuit state shared across processes (Redis storage) | A typed `CircuitBreaker` with async, `exclude=`, `success_threshold` and `.stats`, no dependency |
463
+ | [functools](https://docs.python.org/3/library/functools.html) | A plain `lru_cache` or `cache` on a sync function with hashable arguments is enough. Prefer it for that | Adds TTL, async, unhashable arguments and `cache_delete` |
464
+
465
+ None of them cover bounded `gather`, bulkheads, batching, hedging, policies or test helpers; python-wrap trades per-area depth for one consistent, typed, dependency-free API.
466
+
467
+ ## Learn more
468
+
469
+ - [Cookbook](https://pythonwrap.readthedocs.io/en/latest/cookbook/): resilient HTTP client, async crawler, FastAPI, Django, CLI tools, DataLoader batching, testing, observability.
470
+ - [Performance](https://pythonwrap.readthedocs.io/en/latest/project/performance/): per-call overhead and how to benchmark it yourself.
471
+ - [Testing](https://pythonwrap.readthedocs.io/en/latest/testing/): `fast_mode`, `FakeClock`, `spy` and pytest fixtures.
472
+ - [Migration guide](https://pythonwrap.readthedocs.io/en/latest/project/migration/) and [CHANGELOG](https://github.com/LitePy/PythonWrap/blob/main/CHANGELOG.md).
473
+ - Runnable [examples](https://github.com/LitePy/PythonWrap/tree/main/examples).
474
+
475
+ ## Upgrading from 0.1
476
+
477
+ 0.2.0 fixes decorators that did not do what their names promised (`transactional`, `revert_on_failure`, async `timing`) and makes a few breaking changes: `run_in_thread` returns a `Future`, `benchmark` is a function, `retry_on_exception` is deprecated and bounded, `rate_limit` raises `RateLimitExceeded`, and the logging decorators default to `DEBUG`. Python 3.8 and 3.9 are no longer supported. `from python_wrap.decorators import ...` still works. See the [migration guide](https://pythonwrap.readthedocs.io/en/latest/project/migration/).
478
+
479
+ ## Contributing
480
+
481
+ Bug reports, ideas and pull requests are welcome. Read [CONTRIBUTING.md](https://github.com/LitePy/PythonWrap/blob/main/CONTRIBUTING.md) for the development setup and checks, and [SECURITY.md](https://github.com/LitePy/PythonWrap/blob/main/SECURITY.md) for reporting vulnerabilities.
482
+
483
+ ## License
484
+
485
+ [MIT](https://github.com/LitePy/PythonWrap/blob/main/LICENSE)
@@ -0,0 +1,29 @@
1
+ python_wrap/__init__.py,sha256=mOEWHKnniaYpqW2GioCvCeXFdrnAz_6dGxTJXBvHnOw,4807
2
+ python_wrap/_core.py,sha256=wpAq9lhYN1dWeSfoFqsPznxRAkF3_ui1qERmmMyRiW0,8695
3
+ python_wrap/aio.py,sha256=zFutfNc-mDAFb7ImV8oONB1mYLIWIrmmJQn-tuJmdFI,22147
4
+ python_wrap/batching.py,sha256=w9CGn60vrMjA2uu9qnMHZBRF9uAjxO8z9TzqC7FDlVo,21370
5
+ python_wrap/bulkhead.py,sha256=6p2won4-n59NbAUhThLlayPiXQQADrbGVqNiOn6D93g,15309
6
+ python_wrap/caching.py,sha256=IJZm312bUtnMRTkQRKYnuOBXMcTCu-lniuLh2h9x_Ro,62883
7
+ python_wrap/circuit.py,sha256=aw06aM9B0uNhcYZuwyVtkMwKOYwEbb2MavEkpi2Cgbc,20654
8
+ python_wrap/concurrency.py,sha256=jb_-L0BeFU17Gy78WlSqlbDcQlEXgtIHmpcDdxF6PKU,73855
9
+ python_wrap/config.py,sha256=Llp-7jlRMTEKltwL_qBdOQsaSKW7bFfsrm592cGZ4EM,7542
10
+ python_wrap/deadline.py,sha256=v2KTpf0kxdqXcGQm8YDoQ3vx-epSTSEpTt84jop4EIc,5859
11
+ python_wrap/decorators.py,sha256=x03toQUV0ZpIWimgBWXj0tihB9dYp9fdd9U0bkl2W6Y,206
12
+ python_wrap/devtools.py,sha256=4oTk5ubN8a0Knq3FzyFbtV8L8EeNQXpfl9VohAhdSLE,13512
13
+ python_wrap/errors.py,sha256=j7lGwoqS7Eq1T_I0IV-CwIrbcpxwACz2wGVsOnQdAD8,2685
14
+ python_wrap/events.py,sha256=n88H-1yyd4UHDSCFD6UofcV7pgiLK66QK-KgEy3xFW4,6083
15
+ python_wrap/hedging.py,sha256=shj4sebgYGXlWgQZfgH9Fo7Q33LEuCJcBtBWCz0wPOM,8162
16
+ python_wrap/logs.py,sha256=bfvsNUQyfS-i7SOPXO2pTvxDGBu8_BBAzwi6iKSOS3A,26802
17
+ python_wrap/metrics.py,sha256=-unhVArc_mABM35gyYyhHl7pmHD5HbM14t7q_ckicwI,7858
18
+ python_wrap/periodic.py,sha256=D2LqO1EOuleHsESMrgOZDxLeq04YLF8SBO8oOrAVKjk,19095
19
+ python_wrap/policy.py,sha256=DcTPVIx2BSlPXsTt5PZWW_leNeQWH_pIZu4ocPK3a0M,9561
20
+ python_wrap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ python_wrap/retry.py,sha256=j9DjOF0SHTuEJnJQNtC_FRLeU0bYzOMCDA-I8VNxoik,49139
22
+ python_wrap/state.py,sha256=pM2fl8IdLw3EU3uGWjraGx9xPMTmPaDk3M7xMd7hXbE,18099
23
+ python_wrap/testing.py,sha256=YbYQ_NDvLq4UBU6MDD23XWAadslwM43Bx_BpNYuZmqc,21513
24
+ python_wrap/timing.py,sha256=Bnud5P0ZLEu1UMPr7_a5d6Co1Tawq3VZnl3bjEJSXU0,17067
25
+ python_wrap/validation.py,sha256=JC_7WZlHl4raTZPzfCDihLYseGME3c16n9Ia6Mm9bis,25774
26
+ python_wrap-0.2.1.dist-info/METADATA,sha256=PRxusaLTae9U0WVqGgMEpMtibyi41inO9y6iOvRz-xc,25036
27
+ python_wrap-0.2.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
28
+ python_wrap-0.2.1.dist-info/licenses/LICENSE,sha256=HJx7meo3u1AM4Ms6Q1QNNdwSSdstdmEpo_AAx_TycH0,1079
29
+ python_wrap-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PythonWrap Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.