Policies¶
A policy is a reusable, named bundle of decorators. You say which protections a call needs; the policy applies them in the recommended order, however you list them.
from python_wrap import CircuitBreaker, policy, retry, timeout
payments = policy(
timeout=timeout(2), # listed first on purpose
retry=retry(max_attempts=3, delay=0.01, on=ConnectionError),
breaker=CircuitBreaker(failure_threshold=5, on=ConnectionError),
name="payments",
)
@payments
def charge(amount: int) -> int:
return amount
assert charge(5) == 5
print(payments) # Policy('payments', breaker > retry > timeout)
When to use it¶
- Several calls need the same protections (every call to one service).
- You want the stacking order guaranteed instead of hand-checked.
- Not for a single call with one decorator: use the decorator directly.
Roles¶
Each decorator goes into a named role. The role decides the position, so the code cannot be mis-ordered, and the arguments are checked by the type checker. Guessing a role from a decorator object would be fragile (factories return plain closures), so python-wrap does not guess.
Outermost first:
| Role | Typical decorators | Why here |
|---|---|---|
observe |
timing, audit, log_args |
See the outcome the caller gets, retries included. |
validate |
validate_args, enforce_types |
Bad input is rejected before anything else runs. |
fallback |
fallback, suppress |
Catches what the layers below could not fix. |
cache |
memoize, once, singleflight |
A hit skips everything below. |
limiter |
rate_limit, throttle |
One token per logical call, not per attempt. |
bulkhead |
Bulkhead, limit_concurrency |
Caps concurrent logical calls. |
breaker |
CircuitBreaker, circuit_breaker |
Counts calls whose retries were exhausted. |
retry |
retry |
Repeats everything below. |
timeout |
timeout |
A deadline per attempt. |
guard |
revert_on_failure, transactional |
Each attempt starts from clean state. |
A role accepts one decorator or a list (outermost first). For a decorator with no role, outer= places it outside every role and inner= just above the function.
Deriving variants¶
Policies are immutable. with_() returns a changed copy; None removes a role.
from python_wrap import policy, retry, timeout
base = policy(retry=retry(max_attempts=3, delay=0), timeout=timeout(2), name="io")
patient = base.with_(timeout=timeout(30), name="io-patient")
no_retry = base.with_(retry=None)
assert base.order == ("retry", "timeout")
assert no_retry.order == ("timeout",)
Async and methods¶
A policy is a decorator for plain, async def, instance, static and class methods, and keeps the function's type. Combine it with wrap_methods to protect a whole class.
import asyncio
from python_wrap import policy, retry
p = policy(retry=retry(max_attempts=2, delay=0))
@p
async def load(x: int) -> int:
return x
assert asyncio.run(load(1)) == 1
Gotchas¶
Shared state
A CircuitBreaker instance is shared by everything the policy decorates, which is usually what you want for one dependency. Other decorators create state per decorated function.
python_wrap.policy is the function
The package attribute python_wrap.policy is the policy function, not the module.
See also¶
- Stacking for the reasoning behind the order
- Retry, Circuit breaker, Timeout, Fallback
- API reference