Relinker — a clear, modular, and debuggable retry/resilience library for Python.
Project description
Relinker
A retry library that shows you exactly what it is doing — and warns you when something looks risky.
from relinker import RetryPolicy
policy = (
RetryPolicy()
.attempts(5)
.on(TimeoutError, ConnectionError)
.exponential_delay(base=1, maximum=30)
.jitter(maximum=0.5)
)
result = policy.run(fetch_data)
Five total attempts. Retries only on network errors. Exponential backoff capped at 30 s. Jitter prevents synchronized retries under concurrency.
Before this runs against a real service, ask Relinker what it thinks:
print(policy.explain()) # plain-language description
print(policy.preview(5)) # estimated timing per attempt
print(policy.doctor()) # flagged risks, if any
Install
pip install relinker
Requires Python 3.10+. No runtime dependencies.
For local development:
git clone https://github.com/igors93/relinker.git
cd relinker
python -m venv .venv && source .venv/bin/activate
python -m pip install -e ".[dev]"
What it does
Describe the policy, not the loop. Relinker separates what should retry from how it runs. Policies are immutable objects — compose them, share them, inspect them.
Guidance built in. warnings() and doctor() flag risky configurations like infinite retry without delay or retrying all exceptions. You keep full control; Relinker just points things out.
Full visibility. RetryResult records every attempt, timing, and error type. simulate() and timeline() estimate behaviour before production. Structured logging excludes exception messages by default to avoid leaking sensitive data.
Sync and async, same API. Decorate a regular function or a coroutine function — the same policy works for both.
Zero required dependencies. The core package has no runtime requirements. HTTP helpers, presets, and testing utilities are included.
Features at a glance
| Category | What's included |
|---|---|
| Entry points | @retry decorator · fluent RetryPolicy builder · presets (network, database, fast, …) |
| Stop strategies | by attempt count · by elapsed time · forever · composable AND / OR |
| Retry conditions | by exception type · by returned value · custom callback · TryAgain signal |
| Delays | fixed · linear · exponential · random · chain · state-aware · jitter · custom |
| HTTP helpers | Retry-After support · status-code conditions · http_retry_policy() |
| Shared capacity | RetryBudget — process-local, per-key rolling window |
| Results | RetryResult · attempt history · per-function statistics |
| Observability | structured logging · event hooks · debug() |
| Guidance | warnings() · doctor() · explain() · simulate() · timeline() · preview() |
| Execution | sync run() · async run_async() · sync/async context managers |
| Testing | for_testing() · custom sleep injection · sleep capture |
Stability
Relinker 1.0 introduced a stable public API. Relinker 1.x continues that stability and currently supports Python 3.10 through Python 3.14.
Compatibility guarantees cover the documented exports and behaviors described in
the compatibility policy. Release history lives in
CHANGELOG.md.
See the migration guide when upgrading from an earlier version.
Quick Start
The smallest useful retry
from relinker import retry
@retry(attempts=3, delay=1, on=(TimeoutError,))
def fetch_data() -> str:
return call_external_service()
If fetch_data() raises TimeoutError, Relinker tries again up to 3 times and waits 1 second between attempts.
Use a preset
from relinker import network
@network()
def call_api() -> dict:
return client.get("/users/1")
Presets are regular policies. You can keep customizing them:
policy = network().attempts(8).fallback_value({"status": "offline"})
Share a retry budget
A retry budget limits additional attempts across executions that share the same budget object and key. The original attempt is never counted.
from relinker import RetryBudget, RetryPolicy
budget = RetryBudget(max_retries=20, per=60)
policy = (
RetryPolicy()
.attempts(5)
.on(TimeoutError)
.exponential_delay(base=1, maximum=30)
.with_retry_budget(budget, key="external-api")
)
RetryBudget is in-memory and process-local. Separate processes do not share
capacity. Normal policy delays and max_time() continue to apply. See
Retry budgets for the complete behavior and scope.
Use the full builder
from relinker import RetryPolicy
policy = (
RetryPolicy()
.attempts(5)
.on(TimeoutError, ConnectionError)
.exponential_delay(base=1, maximum=30)
.jitter(maximum=0.5)
.fallback_value({"status": "unavailable"})
)
result = policy.run(fetch_data)
Guidance
Relinker does not try to control your application. It lets you make your own choices, but it helps you notice risky retry policies.
from relinker import RetryPolicy
policy = RetryPolicy().forever().on(Exception).no_delay()
print(policy.doctor().describe())
Example output:
Relinker policy health
Risk level: risky
Warnings:
- forever: This policy can retry forever.
- no_delay: This policy has no delay between attempts.
- tight_loop_risk: This policy can retry forever without sleeping.
- broad_exception: This policy retries all Exception subclasses.
Use explain() when you want to understand a policy in plain language:
print(policy.explain())
Use preview() when you want to estimate timing before running real code:
print(policy.preview(attempts=5))
HTTP retry
Relinker includes dependency-free HTTP helpers. They work with any response object that exposes .status_code or a dictionary with a "status_code" key. Transport exceptions are opt-in so existing 1.x result-based HTTP policies keep their behavior.
from relinker import DEFAULT_RETRYABLE_TRANSPORT_EXCEPTIONS, http_retry_policy
policy = http_retry_policy(
attempts=5,
statuses={429, 500, 502, 503, 504},
transport_exceptions=DEFAULT_RETRYABLE_TRANSPORT_EXCEPTIONS,
respect_retry_after=True,
)
For lower-level control:
from relinker import RetryPolicy, retry_after_delay, retry_if_status
policy = (
RetryPolicy()
.attempts(5)
.retry_if_result(retry_if_status({429, 500, 502, 503, 504}))
.stateful_delay(retry_after_delay(default=1.0, maximum=60.0))
)
This is useful for APIs that return 429 Too Many Requests with a Retry-After header.
Observability
Human-readable logging
import logging
from relinker import RetryPolicy
logging.basicConfig(level=logging.INFO)
policy = RetryPolicy().attempts(3).on(TimeoutError).with_logging(level=logging.INFO)
Structured logging
policy = RetryPolicy().attempts(3).on(TimeoutError).with_structured_logging()
Structured logs exclude error messages by default because exception messages can contain tokens, URLs, payload fragments, or user data.
Events
from relinker import RetryPolicy
from relinker.event import RetryEvent
def on_retry(event: RetryEvent) -> None:
print(f"retrying after attempt {event.attempt_number}, delay={event.delay}")
policy = RetryPolicy().attempts(3).on(TimeoutError).on_retry(on_retry)
Results and statistics
Return a RetryResult when you want full visibility:
result = RetryPolicy().attempts(3).return_result().run(fetch_data)
print(result.summary())
print(result.story())
Decorated functions also receive retry statistics:
from relinker import network
@network()
def fetch_user() -> dict:
return {"id": 1}
fetch_user()
print(fetch_user.retry_stats.to_dict())
Examples
Run examples from the project root:
python -m examples.basic_retry
python -m examples.retry_with_policy
python -m examples.retry_policy_doctor
python -m examples.retry_preview_and_explain
python -m examples.retry_http_retry_after
python -m examples.retry_structured_logging
See examples/README.md for the full list.
Documentation
| Getting started | Install and write your first policy |
| Choosing a policy | Decision guide by situation |
| Feature map | Quick lookup: need → API |
| When not to retry | Idempotency, generators, permanent failures |
| Common mistakes | Risky patterns with safer alternatives |
| Troubleshooting | Symptom-by-symptom diagnosis |
| Production checklist | Review before deploying |
| Retry lifecycle | How one execution flows |
| Retry budgets | Shared capacity explained |
| HTTP retry | Status codes and Retry-After |
| Testing | Keep tests fast and deterministic |
| API reference | Full method and export reference |
| Compatibility policy | Stability guarantees |
Full index: docs/README.md
Contributing
Bug reports, questions, and pull requests are welcome.
- Read CONTRIBUTING.md for local setup, code principles, and the pull request process.
- Open an issue to report a bug or propose a feature before writing code.
- Keep changes small and focused — one behaviour change per pull request.
- Every bug fix needs a regression test. Coverage must not decrease.
Security
Relinker validates all numeric inputs at construction time and caps
Retry-After header values to prevent unexpectedly long sleeps.
To report a vulnerability, open a private security advisory on GitHub. Do not publish sensitive details publicly before the issue is reviewed.
See SECURITY.md for the full security policy.
License
MIT.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file relinker-1.2.0.tar.gz.
File metadata
- Download URL: relinker-1.2.0.tar.gz
- Upload date:
- Size: 188.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
835f17f63fa85a272503de96681fad82f0c7c088846bf12ae0756e6df444fad5
|
|
| MD5 |
a6253cc079d6a51b38ba3e8dfa90d165
|
|
| BLAKE2b-256 |
5b5ad350c6d634ede9a4ea130eb6ba21e51045d6300618eabcb87c217a85d69e
|
Provenance
The following attestation bundles were made for relinker-1.2.0.tar.gz:
Publisher:
publish.yml on igors93/relinker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relinker-1.2.0.tar.gz -
Subject digest:
835f17f63fa85a272503de96681fad82f0c7c088846bf12ae0756e6df444fad5 - Sigstore transparency entry: 1758772254
- Sigstore integration time:
-
Permalink:
igors93/relinker@80590cb54e030e86d1a2135d18991b6ab3efb726 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/igors93
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@80590cb54e030e86d1a2135d18991b6ab3efb726 -
Trigger Event:
release
-
Statement type:
File details
Details for the file relinker-1.2.0-py3-none-any.whl.
File metadata
- Download URL: relinker-1.2.0-py3-none-any.whl
- Upload date:
- Size: 76.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2fb6ed008310e80608b6b455cabf2b2613a32129dc7c30943f93aa0fbbbedc2
|
|
| MD5 |
7c6132deaea45f047088318e223b992f
|
|
| BLAKE2b-256 |
1826a64d1fdf52e18534fd7cccdbda372c180d6df2798f12c7e55bafef5d8478
|
Provenance
The following attestation bundles were made for relinker-1.2.0-py3-none-any.whl:
Publisher:
publish.yml on igors93/relinker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relinker-1.2.0-py3-none-any.whl -
Subject digest:
b2fb6ed008310e80608b6b455cabf2b2613a32129dc7c30943f93aa0fbbbedc2 - Sigstore transparency entry: 1758772393
- Sigstore integration time:
-
Permalink:
igors93/relinker@80590cb54e030e86d1a2135d18991b6ab3efb726 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/igors93
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@80590cb54e030e86d1a2135d18991b6ab3efb726 -
Trigger Event:
release
-
Statement type: