Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Retry (with `Retry-After`), circuit breaker, timeout, cache, rate limit, bulkhea

[**Documentation**](https://python-wrap.readthedocs.io) ·
[Cookbook](https://python-wrap.readthedocs.io/en/latest/cookbook/) ·
[Performance](https://python-wrap.readthedocs.io/en/latest/performance/) ·
[Migration guide](https://python-wrap.readthedocs.io/en/latest/migration/) ·
[Performance](https://python-wrap.readthedocs.io/en/latest/project/performance/) ·
[Migration guide](https://python-wrap.readthedocs.io/en/latest/project/migration/) ·
[Changelog](https://github.com/LitePy/PythonWrap/blob/main/CHANGELOG.md)

</div>
Expand All @@ -33,7 +33,7 @@ Retry (with `Retry-After`), circuit breaker, timeout, cache, rate limit, bulkhea
- **Zero dependencies.** Pure Python, nothing to audit or pin.
- **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`.
- **Sync and async, one API.** Works on `def`, `async def` and methods. Async wrappers stay coroutine functions and never block the event loop.
- **Fast.** Cache hits and validation skip signature binding on the hot path ([numbers](https://python-wrap.readthedocs.io/en/latest/performance/)).
- **Fast.** Cache hits and validation skip signature binding on the hot path ([numbers](https://python-wrap.readthedocs.io/en/latest/project/performance/)).
- **Observable and testable.** One event stream for retries, cache, circuit, limiters and timeouts; `python_wrap.testing` makes tests instant and deterministic.
- **Never configures your logging**, is thread-safe, and is tested on Python 3.10 to 3.14 on Linux, macOS and Windows.

Expand Down Expand Up @@ -113,7 +113,7 @@ print(f"{time.monotonic() - start:.2f}s")

## What is in the box

Everything below is importable from `python_wrap` unless a module is shown. [Choose a decorator](https://python-wrap.readthedocs.io/en/latest/choosing/) is the quick way in.
Everything below is importable from `python_wrap` unless a module is shown. [Choose a decorator](https://python-wrap.readthedocs.io/en/latest/get-started/choosing/) is the quick way in.

| Area | Name | What it does |
|---|---|---|
Expand Down Expand Up @@ -232,7 +232,7 @@ except TimeoutExpired:
print("out of time")
```

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://python-wrap.readthedocs.io/en/latest/limitations/)).
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://python-wrap.readthedocs.io/en/latest/project/limitations/)).

### Caching

Expand Down Expand Up @@ -359,7 +359,7 @@ print(names) # retry.attempt_failed, ..., retry.succeed
print(work.stats.calls, work.stats.p95)
```

Subscribe once and forward events to Prometheus, OpenTelemetry or statsd (recipes in the [guide](https://python-wrap.readthedocs.io/en/latest/guides/metrics/)). Listeners cannot break the decorated function.
Subscribe once and forward events to Prometheus, OpenTelemetry or statsd (recipes in the [guide](https://python-wrap.readthedocs.io/en/latest/features/observability/metrics/)). Listeners cannot break the decorated function.

### Validation and logging with redaction

Expand Down Expand Up @@ -411,7 +411,7 @@ Decorators are applied bottom-up and run top-down, so order matters:
| `log_args`, `timing`, `audit` | everything | They observe what callers actually experience |
| `enforce_types`, `validate_args` | the rest | Reject bad input before doing any work |

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://python-wrap.readthedocs.io/en/latest/stacking/) and [Policies](https://python-wrap.readthedocs.io/en/latest/guides/policies/).
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://python-wrap.readthedocs.io/en/latest/get-started/stacking/) and [Policies](https://python-wrap.readthedocs.io/en/latest/features/resilience/policies/).

## How it compares

Expand All @@ -433,14 +433,14 @@ None of them cover bounded `gather`, bulkheads, batching, hedging, policies or t
## Learn more

- [Cookbook](https://python-wrap.readthedocs.io/en/latest/cookbook/): resilient HTTP client, async crawler, FastAPI, Django, CLI tools, DataLoader batching, testing, observability.
- [Performance](https://python-wrap.readthedocs.io/en/latest/performance/): per-call overhead and how to benchmark it yourself.
- [Performance](https://python-wrap.readthedocs.io/en/latest/project/performance/): per-call overhead and how to benchmark it yourself.
- [Testing](https://python-wrap.readthedocs.io/en/latest/testing/): `fast_mode`, `FakeClock`, `spy` and pytest fixtures.
- [Migration guide](https://python-wrap.readthedocs.io/en/latest/migration/) and [CHANGELOG](https://github.com/LitePy/PythonWrap/blob/main/CHANGELOG.md).
- [Migration guide](https://python-wrap.readthedocs.io/en/latest/project/migration/) and [CHANGELOG](https://github.com/LitePy/PythonWrap/blob/main/CHANGELOG.md).
- Runnable [examples](https://github.com/LitePy/PythonWrap/tree/main/examples).

## Upgrading from 0.1

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://python-wrap.readthedocs.io/en/latest/migration/).
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://python-wrap.readthedocs.io/en/latest/project/migration/).

## Contributing

Expand Down
59 changes: 49 additions & 10 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
# API reference

Everything is importable from the top-level `python_wrap` package. These pages are generated from the docstrings, so they always match the code.
These pages are generated from the docstrings, so they always match the code. Almost
everything is importable from the top-level `python_wrap` package; the exceptions are the
helpers that live in a submodule (`python_wrap.testing`, `python_wrap.events`, and
`remaining`/`check` in `python_wrap.deadline`).

For explanations and examples, start from the matching [feature page](../features/index.md).

## Resilience

| Module | Contents |
| --- | --- |
| [errors](errors.md) | `PythonWrapError`, `TimeoutExpired`, `RateLimitExceeded`, `CircuitOpenError`, `ValidationError`, `ArgumentTypeError`, `BulkheadFull` |
| [retry](retry.md) | `retry`, `fallback`, `suppress`, `retry_on_exception` |
| [circuit](circuit.md) | `CircuitBreaker`, `circuit_breaker`, `CircuitState`, `CircuitStats` |
| [retry](retry.md) | `retry`, `retrying`, `retry_stats`, `retry_after_header`, `fallback`, `suppress`, `retry_on_exception`, `Attempt`, `RetryState`, `RetryStats`, `Retrying` |
| [circuit](circuit.md) | `CircuitBreaker`, `circuit_breaker`, `CircuitState`, `CircuitStats`, `CircuitProtected` |
| [deadline](deadline.md) | `deadline`, `remaining`, `check`, `Deadline` |
| [caching](caching.md) | `memoize`, `cache`, `once` |
| [concurrency](concurrency.md) | `timeout`, `Timeout`, `rate_limit`, `RateLimiter`, `throttle`, `debounce`, `singleflight`, `run_in_thread` |
| [hedging](hedging.md) | `hedge` |
| [policy](policy.md) | `policy`, `Policy`, `ROLES` |

## Caching and batching

| Module | Contents |
| --- | --- |
| [caching](caching.md) | `memoize`, `cache`, `once`, `cached_property`, `invalidate`, `CacheInfo`, `Memoized`, `Once`, `CachedProperty` |
| [batching](batching.md) | `batch_calls`, `Batched`, `AsyncBatched` |

## Concurrency and limits

| Module | Contents |
| --- | --- |
| [concurrency](concurrency.md) | `timeout`, `Timeout`, `rate_limit`, `RateLimiter`, `Limit`, `limiter_of`, `throttle`, `debounce`, `singleflight`, `run_in_thread` |
| [aio](aio.md) | `gather_limit`, `map_async`, `amap_iter`, `race`, `RaceFailed`, `run_sync` |
| [bulkhead](bulkhead.md) | `Bulkhead`, `limit_concurrency`, `Limited` |
| [timing](timing.md) | `timing`, `benchmark`, `benchmark_async`, `measure_memory`, `profile` |
| [logs](logs.md) | `log_args`, `log_return`, `audit`, `trace`, `DEFAULT_REDACT` |

## Observability

| Module | Contents |
| --- | --- |
| [logs](logs.md) | `log_args`, `log_return`, `trace`, `audit`, `AuditRecord`, `log_context`, `current_log_context`, `DEFAULT_REDACT` |
| [timing](timing.md) | `timing`, `benchmark`, `benchmark_async`, `measure_memory`, `profile`, `TimingRecord`, `BenchmarkResult`, `MemoryRecord` |
| [metrics](metrics.md) | `count_calls`, `CallStats`, `Counted` |
| [events](events.md) | `subscribe`, `listening`, `filter_events`, `Event`, `EVENT_NAMES` |

## Correctness

| Module | Contents |
| --- | --- |
| [validation](validation.md) | `validate_args`, `check_type`, `enforce_types` |
| [state](state.md) | `revert_on_failure`, `Transaction`, `transactional` |
| [devtools](devtools.md) | `deprecate`, `mock_data`, `compose`, `no_debug` |
| [state](state.md) | `revert_on_failure`, `Transaction`, `transactional`, `INJECTED`, `current_transaction`, `require_transaction` |

## Everything else

| Module | Contents |
| --- | --- |
| [config](config.md) | `configure`, `configured`, `get_config`, `reset_config` |
| [devtools](devtools.md) | `deprecate`, `mock_data`, `compose`, `wrap_methods`, `no_debug` |
| [testing](testing.md) | `fast_mode`, `fake_clock`, `FakeClock`, `disable_retries`, `disable_rate_limits`, `disable_caching`, `overrides`, `spy`, `spy_on`, `eventually`, `eventually_sync` |
| [errors](errors.md) | `PythonWrapError`, `TimeoutExpired`, `RateLimitExceeded`, `CircuitOpenError`, `BulkheadFull`, `BatchError`, `ValidationError`, `ArgumentTypeError` |
2 changes: 1 addition & 1 deletion docs/cookbook/dataloader-batching.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ for post in posts:

This is the "N+1" query problem, and it appears everywhere: ORMs, GraphQL resolvers, templates, serializers. Fixing it by hand means restructuring the code so someone collects all ids first, loads them in one query and passes the results back down, and that couples every layer to the loading strategy.

[`batch_calls`](../guides/batching.md) is the DataLoader pattern: write **one bulk function** that takes a list of ids, and call it **one item at a time** from anywhere. Calls made close together are collected into a single bulk call, and each caller gets its own result.
[`batch_calls`](../features/caching/batching.md) is the DataLoader pattern: write **one bulk function** that takes a list of ids, and call it **one item at a time** from anywhere. Calls made close together are collected into a single bulk call, and each caller gets its own result.

## The code

Expand Down
4 changes: 2 additions & 2 deletions docs/cookbook/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The guides explain one decorator at a time. The cookbook shows how they combine
2. **The code**, complete and runnable. Every code block on these pages is executed by the docs test suite, so what you read works.
3. **How it works**, one decorator at a time.
4. **Pitfalls**, the mistakes that cost a day.
5. **Test it**, a runnable test file that uses [`python_wrap.testing`](../testing.md), so the decorators never make your tests slow or flaky.
5. **Test it**, a runnable test file that uses [`python_wrap.testing`](../testing/index.md), so the decorators never make your tests slow or flaky.

Every recipe that matters has a standalone script in the [`examples/`](https://github.com/LitePy/PythonWrap/tree/main/examples) directory that you can run with `python examples/<name>.py`. No third-party packages are needed; framework code (FastAPI, Django, httpx) is shown but marked as such.

Expand All @@ -29,7 +29,7 @@ New to python-wrap? Start with the [resilient HTTP client](resilient-http-client

## Ground rules the recipes follow

* **Stack deliberately.** The decorator closest to the function runs first. `@retry` above `@rate_limit` means every retry counts against the limit; the other way round, the whole retry loop counts once. See [Stacking decorators](../stacking.md).
* **Stack deliberately.** The decorator closest to the function runs first. `@retry` above `@rate_limit` means every retry counts against the limit; the other way round, the whole retry loop counts once. See [Stacking decorators](../get-started/stacking.md).
* **State belongs to the function.** Caches, breakers and rate-limit windows are created once per decorated function and shared by every caller in the process. That is the point, and also the first thing to reset in tests.
* **Libraries do not configure logging.** Recipes that print or collect logs set up their own handlers, as your application should.
* **Be explicit about what is safe to repeat.** `retry`, `hedge` and `race` may run an operation more than once. Use them for reads and idempotent writes.
2 changes: 1 addition & 1 deletion docs/cookbook/resilient-http-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ For `httpx.AsyncClient` make `get` and `_send` `async def` and use `await breake

## Test it

The decorators wait and remember, so tests need the helpers from [`python_wrap.testing`](../testing.md): `fake_clock` makes sleeps instant and records them, `spy_on` counts transport calls and `disable_rate_limits` lets calls through limiters. (`fast_mode` skips waits without recording them; see [Testing code that uses python-wrap](testing-your-code.md).)
The decorators wait and remember, so tests need the helpers from [`python_wrap.testing`](../testing/index.md): `fake_clock` makes sleeps instant and records them, `spy_on` counts transport calls and `disable_rate_limits` lets calls through limiters. (`fast_mode` skips waits without recording them; see [Testing code that uses python-wrap](testing-your-code.md).)

```python
import threading
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook/testing-your-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Decorators that wait and remember make tests slow and order-dependent:
- A `rate_limit` or circuit breaker keeps counting across tests, so a test passes alone and fails in the suite.
- A TTL cache or throttle window needs real time to pass.

[`python_wrap.testing`](../testing.md) is built for this. It is standard library only (pytest is optional), and the helpers change python-wrap's behaviour from the outside, so production code stays unchanged.
[`python_wrap.testing`](../testing/index.md) is built for this. It is standard library only (pytest is optional), and the helpers change python-wrap's behaviour from the outside, so production code stays unchanged.

| Helper | Use it to |
| --- | --- |
Expand Down
Loading
Loading