API reference¶
Generated from the docstrings. Everything listed here is public API.
Policies¶
nopanic.retry
¶
Retry policy: re-run a failing call with configurable backoff.
RetryAttempt
dataclass
¶
Passed to before_sleep after a failed attempt, before the wait.
Attributes:
| Name | Type | Description |
|---|---|---|
attempt |
int
|
1-based index of the attempt that just failed. |
exception |
BaseException | None
|
The exception that attempt raised, or None when the
attempt returned a value rejected by |
delay |
float
|
Seconds that will be slept before the next attempt. |
result |
Any
|
The rejected return value when |
retry
¶
Bases: Policy
Retry a call when it raises a matching exception.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attempts
|
int
|
Total number of attempts, including the first call. |
3
|
on
|
ExcFilter
|
Exception type, tuple of types, or predicate selecting which exceptions trigger a retry. Anything else propagates immediately. |
Exception
|
backoff
|
float | Backoff | None
|
A backoff strategy (see :mod: |
None
|
giveup
|
Callable[[BaseException], bool] | None
|
Optional predicate; returning True for a raised exception
stops retrying even if it matches |
None
|
before_sleep
|
Callable[[RetryAttempt], None] | None
|
Optional callback invoked with a :class: |
None
|
honor_retry_after
|
bool
|
When True (default), an exception carrying a
numeric |
True
|
retry_after_cap
|
float
|
Upper bound on an honored |
60.0
|
max_time
|
float | None
|
Total time budget in seconds, measured from the start of
the first attempt. Retrying stops as soon as the next wait would
end past the budget, so no attempt ever starts after the
deadline. |
None
|
on_result
|
Callable[[Any], bool] | None
|
Optional predicate over the return value; returning True treats the result as a failure and retries (an HTTP client that returns a 503 response instead of raising). When retrying stops, the last result is returned as-is. Not applied to streams. |
None
|
budget
|
RetryBudget | None
|
Optional :class: |
None
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
clock
|
Callable[[], float]
|
Monotonic time source for |
monotonic
|
When attempts are exhausted the original last exception is re-raised
unchanged; there is no wrapper exception to unwrap. If the backoff
strategy stops yielding delays, or the max_time budget, deadline or
retry budget would be exceeded, retrying stops early the same way. An
exception raised inside before_sleep is logged and suppressed —
observability hooks are never allowed to break the call they observe.
On a generator or async generator function (an LLM token stream, say), the opening of the stream is retried: failures before the first item are retried like any call, while a failure after items were delivered propagates, since those items cannot be taken back.
nopanic.CircuitBreaker
¶
Bases: Policy
Failure-rate circuit breaker over a sliding time window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failure_threshold
|
float
|
Failure rate in [0, 1] that opens the breaker. |
0.5
|
min_calls
|
int
|
Minimum outcomes in the window before the rate is judged (prevents one early failure from opening a cold breaker). |
5
|
window
|
float
|
Look-back period for the failure rate, in seconds. This is not a per-call timeout: a slow call is recorded once it finishes. |
30.0
|
window_buckets
|
int
|
Number of fixed time buckets the window is divided into. Window memory is constant in the bucket count regardless of traffic; outcomes expire in bucket-sized steps, so more buckets means finer expiry precision. |
10
|
reset_timeout
|
float
|
Seconds to stay open before allowing probe calls. |
30.0
|
half_open_max_calls
|
int
|
Number of concurrent probe calls allowed while half-open; that many successes close the breaker. |
1
|
on
|
ExcFilter
|
Which exceptions count as failures. Exceptions not selected are propagated without being recorded at all. |
Exception
|
slow_call_duration
|
float | None
|
When set, a call taking at least this many
seconds counts as slow (whether it succeeded or failed); for a
stream, the time to its first item is what counts. A dependency
that answers, but too slowly, is often as harmful as one that
fails. |
None
|
slow_call_threshold
|
float
|
Slow-call rate in (0, 1] that opens the
breaker, judged like |
1.0
|
name
|
str | None
|
Diagnostic name (appears in |
None
|
on_state_change
|
Callable[[CircuitBreaker, str, str], None] | None
|
Optional callback |
None
|
clock
|
Callable[[], float]
|
Monotonic time source; injectable for tests. |
monotonic
|
Besides decorating functions (sync, async and generator functions), a
breaker guards a block of code via with breaker.guard(): or
async with breaker.guard():.
state
property
¶
Current state: "closed", "open", "half_open",
"forced_open" or "disabled".
Reading the state applies the open → half-open timeout transition, so it always reflects what the next call would see.
metrics
property
¶
A consistent snapshot of state and window counters.
reset()
¶
Force the breaker back to closed and clear the window.
force_open()
¶
Reject every call until :meth:reset (maintenance, kill switch).
disable()
¶
Let every call through and record nothing until :meth:reset.
guard()
¶
Context manager guarding a block: with breaker.guard(): ....
Works with with and async with. An exception leaving the
block is judged exactly like one raised by a decorated function.
nopanic.BreakerMetrics
dataclass
¶
Point-in-time view of a breaker, for health checks and dashboards.
Attributes:
| Name | Type | Description |
|---|---|---|
state |
str
|
Current state (see the module docstring). |
calls |
int
|
Outcomes currently inside the sliding window. |
failures |
int
|
Failed outcomes inside the window. |
slow_calls |
int
|
Slow outcomes inside the window (0 unless slow-call tracking is enabled). |
failure_rate |
float
|
|
slow_call_rate |
float
|
|
nopanic.timeout
¶
Timeout policy: bound how long a single call may take.
timeout
¶
Bases: Policy
Fail with :class:TimeoutError if the call takes longer than seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
float
|
Time limit per call. |
required |
first_item
|
float | None
|
For streams only: limit for the first item, which is
often slower than the rest (an LLM's time to first token).
Defaults to |
None
|
max_abandoned
|
int | None
|
Sync calls only: how many timed-out worker threads
may still be running before new calls fail fast with
|
64
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
Async functions are cancelled cleanly via the event loop. Sync functions
are executed on a daemon worker thread and abandoned on timeout —
Python cannot kill a running thread, so the underlying work may continue
in the background even after the caller has received TimeoutError.
Prefer the async path (or the callee's own timeout parameter, e.g. an
HTTP client's) when abandonment is not acceptable. The worker runs in a
copy of the caller's context, so contextvars (tracing spans, request
IDs, structured-logging fields) are visible inside the call.
Only an expiry of this timeout raises a new TimeoutError. A
TimeoutError raised by the wrapped function itself (its own I/O
timeout, say) propagates unchanged, like any other user exception.
Inside an active :class:~nopanic.deadline the limit is clamped to the
time remaining, and a call made after the deadline fails at once.
On an async generator function the limit applies to each item (a
stalled stream fails even if it started fine), with first_item for
the first one. Sync generators are not supported: a worker thread per
item would be the only way to bound them. async with
policy.guard(): bounds a block of async code.
nopanic.rate_limit
¶
Bases: _TokenBucket
Throttle calls to at most rate per per seconds (token bucket).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Number of calls allowed per period. |
required |
per
|
float
|
Period length in seconds. |
1.0
|
burst
|
float | None
|
Bucket capacity — how many calls may go through back-to-back
after an idle stretch. Defaults to |
None
|
max_wait
|
float | None
|
By default a throttled call waits for its token. If set,
calls that would wait longer than this raise :class: |
None
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
clock
|
Callable[[], float]
|
Monotonic time source; injectable for tests. |
monotonic
|
Waiting callers reserve their token up front (the bucket may go
negative), which keeps ordering roughly FIFO under contention; a waiter
that is cancelled gives its token back. A stream takes one token when it
opens; with limiter.guard(): takes one for a block.
available_tokens
property
¶
Tokens in the bucket right now; negative while callers are queued.
nopanic.adaptive_rate_limit
¶
Bases: _TokenBucket
Token bucket whose rate adapts to server pushback (AIMD).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Starting (and by default maximum) calls per |
required |
per
|
float
|
Period length in seconds. |
1.0
|
min_rate
|
float | None
|
Floor the rate never drops below (same unit as |
None
|
max_rate
|
float | None
|
Ceiling the rate never climbs above. Defaults to |
None
|
increase
|
float
|
Additive recovery per successful call, in calls-per- |
0.1
|
decrease
|
float
|
Multiplicative factor in (0, 1) applied on throttle errors. |
0.5
|
throttle_on
|
ExcFilter
|
Which exceptions mean "slow down". Defaults to
:func: |
looks_throttled
|
max_wait
|
float | None
|
As in |
None
|
max_block
|
float
|
Upper bound on how long a server-sent |
60.0
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
clock
|
Callable[[], float]
|
Monotonic time source; injectable for tests. |
monotonic
|
A retry_after attribute on a throttle exception additionally blocks
the bucket for that many seconds (capped at max_block), so the next
calls respect the server's explicit wish. Inspect the live rate via
current_rate and the bucket via available_tokens. Streams take
one token when they open and report their outcome when they end;
guard() does the same for a block.
nopanic.looks_throttled(exc)
¶
Best-effort default for "the server told us to slow down".
True when the exception carries a retry_after attribute, an HTTP
status of 429 or 503 (checked on the exception itself and on an attached
response object, which covers requests and httpx errors). Pass your
own predicate for anything smarter.
nopanic's own signals (CircuitOpen, RateLimited, ...) are never
pushback: they carry retry_after too, but they come from the local
process, and reading them as the server's voice would let a tripped
breaker collapse the adaptive rate.
nopanic.bulkhead
¶
Bulkhead policy: cap concurrent calls so one dependency can't sink the ship.
bulkhead
¶
Bases: Policy
Allow at most max_concurrent simultaneous calls through.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_concurrent
|
int
|
Concurrency ceiling. |
required |
max_wait
|
float | None
|
How long a call may wait for a slot. |
None
|
name
|
str | None
|
Diagnostic name used in error messages and events. |
None
|
One instance carries the shared capacity: decorate every call site that
should share the same slots with the same instance. Capacity is counted
separately for sync callers (one thread semaphore) and for async
callers on each event loop (one asyncio semaphore per loop): a bulkhead
used from sync code and from two event loops admits up to three times
max_concurrent in total. Use separate instances per flavour if that
matters to you.
Streams (generator functions) hold their slot until they finish. A
block of code is guarded with with bh.guard(): (sync capacity) or
async with bh.guard(): (the running loop's capacity).
nopanic.fallback
¶
Fallback policy: degrade gracefully instead of failing.
fallback
¶
Bases: Policy
Return a substitute result when the call raises a matching exception.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
handler
|
Any
|
Either a plain value to return, or a callable receiving the exception and returning the substitute (async callables are awaited when the wrapped function is async). |
required |
on
|
ExcFilter
|
Which exceptions trigger the fallback; others propagate. |
Exception
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
Typically the outermost policy in a stack: everything the inner layers could not absorb turns into a degraded-but-valid response.
nopanic.hedge
¶
Hedged requests: trade a little extra load for much better tail latency.
If a call hasn't finished after delay seconds, fire a second identical call and take whichever finishes first ("The Tail at Scale", Dean & Barroso, 2013). Ideal for high-variance backends — LLM APIs, cross-region reads, flaky CDNs.
Only meaningful for idempotent operations, and async-only by nature.
hedge
¶
Bases: Policy
Race duplicate calls against a slow first attempt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
delay
|
float
|
Seconds to wait before launching each additional attempt. |
required |
max_hedges
|
int
|
Maximum number of extra attempts (so total concurrent
attempts is at most |
1
|
on
|
ExcFilter
|
Which failures are worth hedging. An attempt failing with a
matching exception lets the others continue (and launches the
next hedge at once); any other exception is permanent: it
cancels the remaining attempts and propagates immediately, so a
400-style error is not repeated |
Exception
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
The first successful attempt wins and all others are cancelled. If every attempt fails, the last failure is re-raised.
nopanic.cache
¶
Result cache with a stale-while-failing safety net.
Fresh results are served from memory; when the value expires the call runs
again, and if that refresh fails, the stale value is served instead of the
error (up to stale_ttl past expiry). The dependency being down stops
being your outage for as long as your stale window allows.
Refreshes are single-flight: when many callers miss the same key at once, one of them calls the dependency and the rest share its outcome, so an expiring hot key does not stampede a backend at the moment it is weakest.
cache
¶
Bases: Policy
Cache results by arguments; serve stale on refresh failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ttl
|
float
|
Seconds a cached value counts as fresh (served without calling). |
required |
stale_ttl
|
float
|
Extra seconds past |
0.0
|
on
|
ExcFilter
|
Which refresh failures allow serving stale; others propagate. |
Exception
|
maxsize
|
int
|
Entry limit; least-recently-used entries are evicted. |
1024
|
key
|
Callable[..., Hashable] | None
|
Optional |
None
|
single_flight
|
bool
|
When True (default), concurrent misses of the same key share one refresh: a single caller runs the function and the others wait for and receive its outcome (value, stale value or the same exception). Sync and async callers are deduplicated separately, async ones per event loop (or trio run). |
True
|
name
|
str | None
|
Diagnostic name carried by this policy's events. |
None
|
clock
|
Callable[[], float]
|
Monotonic time source; injectable for tests. |
monotonic
|
One instance may decorate several functions; entries are keyed per
function. Arguments must be hashable unless key is given. Streams
(generator functions) cannot be cached and are rejected at decoration.
nopanic.deadline
¶
Deadline propagation: one time budget for a whole operation.
A timeout bounds one call; a deadline bounds everything inside a block, no matter how many retries, waits and nested calls it takes::
with deadline(5.0): # or: async with deadline(5.0)
profile = get_profile(uid) # retries, timeouts, rate-limit waits...
orders = get_orders(uid) # ...all share the same 5 seconds
The deadline is ambient (a contextvars value, so it follows the code
into awaited coroutines and timeout worker threads) and cooperative:
policies inside the block respect it, nothing is killed from outside.
timeoutclamps its own limit to the time remaining, and fails at once if the deadline has already passed.retrygives up instead of sleeping into a wait that would outlast the deadline.rate_limit,adaptive_rate_limitandbulkheadnever wait past it; they reject withRateLimited/BulkheadFullinstead.
Nested deadlines can only shorten the budget, never extend it. Your own code
can read the remainder with :func:nopanic.time_remaining and pass it on,
e.g. as an HTTP client's timeout. To enforce the budget on a call, put a
timeout inside; it will use whichever limit is tighter.
deadline is also a policy, so @deadline(5.0) or
compose(deadline(10.0), retry(...), timeout(2.0)) gives every call its
own fresh budget.
deadline
¶
Bases: Policy
Run a block, or a decorated call, under a time budget of seconds.
Use as with deadline(s): / async with deadline(s): around a
block, or as a decorator / compose member to give each call its own
budget. One instance must not be entered twice at the same time (create
a new one per block); as a decorator it can be shared freely. Generator
functions are not supported: a context value set inside a generator
would leak into the consumer between items.
nopanic.time_remaining()
¶
Seconds left before the innermost active :class:~nopanic.deadline.
None when no deadline is active. Can be negative once it has passed.
Handy for handing the remaining budget to a client's own timeout
parameter: client.get(url, timeout=time_remaining()).
nopanic.RetryBudget
¶
A shared allowance of retries; pass it to one or more retry policies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ratio
|
float
|
Retries allowed per first attempt inside the window (0.2 means at most one retry for every five calls). |
0.2
|
min_retries_per_sec
|
float
|
Floor that is always allowed, so a client with
little traffic can still retry. Over the window this amounts to
|
1.0
|
window
|
float
|
Sliding window in seconds over which calls and retries are counted. |
10.0
|
clock
|
Callable[[], float]
|
Monotonic time source; injectable for tests. |
monotonic
|
Thread-safe; memory is constant (bucketed counters, like the breaker).
nopanic.keyed
¶
Per-key policies: one breaker (or limiter, or bulkhead) per host, tenant, model...
A breaker guarding "the payments API" trips for everyone when one region is
down; a rate limit shared by all tenants lets one noisy tenant starve the
rest. keyed keeps a separate policy instance per key, created on first
use from a factory and bounded by an LRU::
per_host = keyed(
lambda host: circuit_breaker(name=f"api-{host}", reset_timeout=10.0),
key=lambda url, **_: urlsplit(url).hostname,
)
@per_host
async def fetch(url, **params): ...
per_host.get("eu.api.example.com").state # inspect one of them
keyed
¶
Bases: Policy, Generic[PolicyT]
Route each call to its own policy instance, chosen by a key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
factory
|
Callable[[Any], PolicyT]
|
|
required |
key
|
Callable[..., Hashable]
|
|
required |
maxsize
|
int
|
Most instances kept; the least recently used one is dropped beyond that (and with it its state: an evicted open breaker starts again closed). |
1024
|
Supports whatever the produced policies support: an unsupported stream
flavour raises TypeError when the stream is iterated, since the
policy type is only known once the factory has run.
nopanic.compose
¶
Combine several policies into a single decorator.
compose
¶
Stack policies into one decorator; the first argument is outermost.
compose(retry(...), timeout(2.0)) behaves exactly like writing::
@retry(...) # outermost: retries the whole timed call
@timeout(2.0) # innermost: bounds each individual attempt
def call(): ...
A typical production stack, outermost to innermost:
fallback → retry → circuit_breaker → timeout.
Like a single policy, a composed stack also runs one-off calls without
decorating anything: stack.call(fn, *args) / await
stack.acall(fn, *args). Those build the wrapper chain per call (a few
microseconds); decorate once for hot paths.
nopanic.Policy
¶
Bases: ABC
Base class for all resilience policies.
Subclasses implement _run_sync and _run_async; instances are
then usable as decorators on both sync and async callables, and can be
stacked or combined with :func:nopanic.compose. For a one-off call
there is nothing to decorate: policy.call(fn, *args) and
await policy.acall(fn, *args) run fn under the policy directly.
Policies that can guard a stream (a generator or async generator
function, e.g. an LLM token stream) also implement _run_sync_gen /
_run_async_gen and list the flavours in _STREAMS. Decorating a
generator function with a policy that cannot guard it raises
TypeError at decoration time instead of silently doing nothing.
Backoff¶
nopanic.backoff
¶
Backoff strategies.
A strategy is any iterable of floats: iterating it yields the delay (in seconds) to sleep before each successive retry. Strategies are plain frozen dataclasses, so they are cheap, hashable and safe to share between policies; each execution of a policy starts a fresh iterator.
full_jitter (capped exponential growth with full jitter, per the AWS
architecture blog) is the recommended default for anything that talks to a
shared service — it avoids retry stampedes.
Jitter deliberately uses the stdlib PRNG (random): it shapes load, it is
not a security boundary, and a CSPRNG would add cost for no benefit. The
corresponding lint rule (S311) is suppressed project-wide for this reason.
Backoff
¶
Bases: Protocol
Anything that can be iterated to produce per-retry delays in seconds.
fixed
dataclass
¶
The same delay before every retry.
exponential
dataclass
¶
Deterministic exponential growth: base, base*factor, ... capped at cap.
full_jitter
dataclass
¶
Exponential growth with full jitter: uniform(0, min(cap, base*factor**n)).
The recommended default against shared services; see "Exponential Backoff and Jitter" (AWS Architecture Blog, 2015).
decorrelated_jitter
dataclass
¶
Decorrelated jitter: each delay is uniform(base, previous*3), capped.
as_backoff(value)
¶
Coerce a bare number into a :class:fixed strategy.
Events¶
nopanic.events
¶
Lightweight observability events emitted by every policy.
Subscribe once, see everything your resilience layer does: retries, breaker transitions, rejected calls, throttle adjustments, stale cache serves.
from nopanic import events
cancel = events.subscribe(lambda e: log.info("%s %s %s", e.kind, e.name, e.data))
...
cancel() # stop listening
Or, for plain logging, just events.log_to(logging.getLogger("resilience")).
Listeners are process-global and called synchronously on the calling thread; keep them fast (hand off to a queue for heavy work). A listener that raises is logged and ignored: observability is never allowed to break the call it observes. When there are no listeners the emission cost is a single attribute read, so the hot path stays effectively free.
Every event carries the emitting policy type, the policy's name (when
one was given) and the qualified name of the guarded function. Event
kinds currently emitted:
retry.attempt_failed(attempt, delay, exception[, result])retry.gave_up(attempt, exception, reason[, result]); reason is one of attempts, giveup, backoff, max_time, deadline, budgetbreaker.state_change(old, new)breaker.rejected(retry_after)bulkhead.rejected(max_concurrent, max_wait)ratelimit.waited(seconds)ratelimit.rejected(would_wait, max_wait)ratelimit.throttled(old_rate, new_rate, retry_after) [adaptive]timeout.expired(seconds, function)timeout.saturated(abandoned, max_abandoned) [sync]fallback.used(exception)hedge.launched(delay)cache.stale_served(age, exception)
Event
dataclass
¶
One observable occurrence inside a policy.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
Dotted event type, e.g. |
policy |
str
|
Policy type that emitted it, e.g. |
name |
str | None
|
The policy instance's |
data |
Mapping[str, Any]
|
Event-specific payload; see the module docstring. |
function |
str | None
|
Qualified name of the guarded function, when known. |
subscribe(listener)
¶
Register a listener for all policy events; returns an unsubscribe callable.
unsubscribe(listener)
¶
Remove a previously registered listener (no-op if absent).
log_to(logger=None, level=logging.INFO)
¶
Log every event to logger (default nopanic); returns an unsubscribe callable.
One line per event, lazily formatted, with the event fields as
extra (nopanic_kind, nopanic_policy, nopanic_name,
nopanic_function) for structured-logging handlers.
emit(kind, policy, name=None, fn=None, /, **data)
¶
Deliver an event to all listeners.
Used by every policy, and public for policy authors via
:mod:nopanic.extend. name is the policy instance's name, fn the
guarded callable (its qualified name is computed only when someone is
listening); keyword arguments become Event.data.
Exceptions¶
nopanic.ResilienceError
¶
Bases: Exception
Base class for all errors raised by nopanic policies.
nopanic.CircuitOpen
¶
Bases: ResilienceError
Raised when a call is rejected because the circuit breaker is open.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
Name of the breaker that rejected the call. |
|
retry_after |
Seconds until the breaker will transition to half-open
and allow a probe call. |
nopanic.BulkheadFull
¶
Bases: ResilienceError
Raised when a bulkhead rejects a call because capacity is exhausted.
nopanic.RateLimited
¶
Bases: ResilienceError
Raised when a rate limiter rejects a call instead of waiting.
Only raised when max_wait is set and the required wait exceeds it;
by default the rate limiter waits instead of failing.
Attributes:
| Name | Type | Description |
|---|---|---|
retry_after |
Seconds until a token would have been available, i.e.
the wait that exceeded |
nopanic.RetryAttempt
dataclass
¶
Passed to before_sleep after a failed attempt, before the wait.
Attributes:
| Name | Type | Description |
|---|---|---|
attempt |
int
|
1-based index of the attempt that just failed. |
exception |
BaseException | None
|
The exception that attempt raised, or None when the
attempt returned a value rejected by |
delay |
float
|
Seconds that will be slept before the next attempt. |
result |
Any
|
The rejected return value when |
Testing¶
nopanic.testing
¶
Helpers for testing code that uses nopanic.
Resilience code is full of waiting: backoff between retries, rate-limit queues. Your test suite should not be::
from nopanic import testing
def test_checkout_survives_a_flaky_gateway():
with testing.instant() as sleeps:
assert checkout(order) == "paid"
assert len(sleeps) == 2 # it did back off twice, instantly
def test_breaker_reports_opening():
with testing.capture_events("breaker.") as seen:
...
assert [e.data["new"] for e in seen] == ["open"]
Both helpers act process-wide for the duration of the with block (all
threads), so do not run tests that use them in parallel threads of one
process (pytest-xdist uses processes and is fine).
instant()
¶
Skip every wait nopanic would sleep, recording the requested seconds.
Covers retry backoff (including honored retry_after hints) and
rate-limit waits, sync and async (async waits still yield to the event
loop once). Timeouts are not affected: they bound real work.
capture_events(prefix='')
¶
Collect every event whose kind starts with prefix (default: all).
Extending¶
nopanic.extend
¶
Public building blocks for writing your own policies.
nopanic's add-on packages (nopanic-otel, nopanic-redis) are built
only on this module, and so can yours. Everything here is covered by the
same compatibility promise as the rest of the public API.
A minimal policy::
from nopanic.extend import Policy, emit
class audit(Policy):
__slots__ = ()
def _run_sync(self, fn, args, kwargs):
emit("audit.call", "audit", None, fn)
return fn(*args, **kwargs)
async def _run_async(self, fn, args, kwargs):
emit("audit.call", "audit", None, fn)
return await fn(*args, **kwargs)
Rules your policy should follow to behave like the built-in ones: re-raise
user exceptions unchanged, never hold a lock while calling user code, wait
only through :func:sleep / :func:asleep (so testing.instant() and
deadlines work), respect :func:time_remaining in every wait, and declare
stream support in _STREAMS (implementing _run_sync_gen /
_run_async_gen) or let decoration of generators fail loudly.