Skip to main content

Framework-agnostic Python resilience + core-infrastructure kernel — retries, circuit breakers, throttles, cache, SSRF guard, DNS-pinned HTTP client, audit decorators, field crypto. Pluggable backends. Adapters for Django + FastAPI.

Project description

resilience-kit

Framework-agnostic Python resilience + core-infrastructure kernel. Retries, circuit breakers, throttles, cache, SSRF guard, DNS-pinned HTTP client, audit decorators, field crypto — one package, pluggable backends, two thin adapters for Django and FastAPI.

PyPI Python CI License

Status: 0.1.0 on PyPI (stable). M0–M8 complete (scaffold · primitives · Redis/Valkey + pybreaker backends · HTTP client + SSRF + crypto · audit + middleware + metrics · FastAPI + Django adapters · boilerplate migrations · v0.1.0 cut + pre-cut ergonomics bundle). See Tagging convention for milestone tags. Design is locked across six docs:

Doc What it answers
PRD.md What's in/out of scope, why, who it's for
ROADMAP.md Feature-level breakdown per milestone (M0–M8) with exit gates
LLD.md Protocols, sequence diagrams, concurrency model, settings schema
DIRECTORY-TREE.md Every file with arrival milestone and required extra
MIGRATION-from-boilerplate-embedded.md Stepwise port from embedded core/resilience/ to the kit
adr/ Numbered architecture decisions (Context / Decision / Consequences / Usage)

APIs marked locked in PRD §5.4 and LLD §2 will not break before 1.0.


Why

django_boilerplate and fastapi_boilerplate were shipping the same resilience kernel against two web frameworks — retry, circuit breaker, Valkey-backed throttles, SSRF guard, DNS-pinned HTTP client, Fernet field crypto, @log_inbound / @log_outbound audit decorators — and it had already drifted. This package extracts the kernel once, makes every backend swappable, and ships thin adapters for both frameworks.

You probably want this if you've ever:

  • Copy-pasted retry / circuit_breaker decorators between two services.
  • Hand-rolled SSRF protection and then wondered if it survives DNS rebinding (it usually doesn't).
  • Wrapped redis-py in a degrades-to-memory shim for the third time.
  • Reached for tenacity and pybreaker and a custom Lua script and a sanitizer and a ContextVar request-id — all to add resilience to one outbound HTTP call.

Install

pip install resilience-kit                        # core: pure-python, no I/O deps
pip install "resilience-kit[fastapi,redis,http]"  # FastAPI app on Valkey
pip install "resilience-kit[django,redis,http]"   # Django app on Valkey
pip install "resilience-kit[all]"                 # everything

Upgrading from 0.1.0rc1? See docs/MIGRATION-rc1-to-v0.1.0.md — quick-path pin bump plus the helper recipes that remove the M7 dogfooding blockers (request_id ContextVar bridging, single-envelope exception handling, legacy env-var translation, exception-bridge contract test).

Available extras

Extra Enables
(none) retry, in-memory breaker/throttle/cache, ssrf guard, audit decorators (noop sink), middleware factories, metrics shim, tasks queue, testing helpers
[redis] Valkey / Redis backends for breaker, throttle, cache
[pybreaker] pybreaker backend for the circuit breaker
[http] DNS-pinned AsyncAPIClient (httpx)
[requests] pinned_requests_session()
[crypto] FernetCipher for field-level encryption
[audit-postgres] Postgres audit-log backend
[django] Django + DRF adapter
[fastapi] FastAPI + Starlette adapter
[all] everything above
[dev] tooling: testcontainers, pytest-asyncio, mypy, ruff

Importing a backend whose extra isn't installed raises MissingExtraError("install resilience-kit[redis]") at import time — no confusing ModuleNotFoundError deep in a stack trace.


Quickstart — the one decorator you'll use the most

from resilience_kit import resilient

@resilient("partner_api")          # circuit breaker (outer) + retry (inner)
async def get_balance(account_id: str) -> Decimal:
    response = await http_client.get(f"/accounts/{account_id}/balance")
    return Decimal(response.json()["balance"])

That's it. Defaults from settings: 3 retries with exponential backoff + jitter, breaker opens after 5 failures, half-opens after 30s. Per-service overrides:

from resilience_kit import registry

registry.register_service("partner_api", {
    "retry":           {"max_attempts": 5, "wait_min": 2, "wait_max": 30},
    "circuit_breaker": {"fail_max": 3,     "reset_timeout": 60},
})

FastAPI

from fastapi import FastAPI, Depends
from resilience_kit.adapters.fastapi import (
    resilience_lifespan,
    install_exception_handlers,
    install_middleware_stack,
    rate_limit,
)

app = FastAPI(lifespan=resilience_lifespan)
install_middleware_stack(app)
install_exception_handlers(app)

@app.get("/accounts/{id}", dependencies=[Depends(rate_limit("ip", "60/min"))])
async def read_account(id: str):
    ...
  • resilience_lifespan starts the recovery monitor + audit dispatcher and mounts /readyz + /healthz.
  • rate_limit(scope, rate) is a FastAPI dependency over the kit's throttle.
  • install_exception_handlers(app) maps kit exceptions (ServiceUnavailableError, RateLimitError, …) to JSON responses.

Django

# settings.py
INSTALLED_APPS = [..., "resilience_kit.adapters.django"]

MIDDLEWARE = [
    "resilience_kit.adapters.django.middleware.RequestIdMiddleware",
    "resilience_kit.adapters.django.middleware.RateLimitHeadersMiddleware",
    ...
]

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "resilience_kit.adapters.django.drf_throttles.IPThrottle",
        "resilience_kit.adapters.django.drf_throttles.UserTierThrottle",
    ],
    "EXCEPTION_HANDLER": "resilience_kit.adapters.django.exception_handler.handle",
}

RESILIENCE = {
    "BACKEND": "redis",
    "REDIS_URL": env("REDIS_URL"),
    "CRYPTO": {"FIELD_ENCRYPTION_KEY": env("FIELD_ENCRYPTION_KEY")},
}

./manage.py resilience_status shows per-service breaker state. ./manage.py resilience_reset partner_api force-closes one.


What's in the box

Resilience primitives

Primitive Sync Async Backends
@retry(...) / @retry_on_failure(name) n/a (pure logic)
@circuit_breaker(name) memory (default), pybreaker, redis (atomic Lua)
@resilient(name) — breaker ∘ retry
Throttle — rate_limit(scope, rate) memory, redis (global Lua)
Cache — get_cache(alias) memory (TTL), redis
Recovery monitor — auto re-probe degraded backends

Scopes: ip · endpoint · user_tier · global · burst · auth. Rate syntax: "60/min", "10/sec", "1000/hour".

Security

from resilience_kit.http_client import AsyncAPIClient
from resilience_kit.crypto import FernetCipher

async with AsyncAPIClient(service="partner_api") as client:
    # SSRF guard + DNS pin + outbound allow-list + breaker + retry + audit — all composed.
    data = await client.get("https://partner.example.com/v1/users/42")

token = FernetCipher.encrypt("very secret")
plaintext = FernetCipher.decrypt(token)

The DNS pin closes the classic validate→connect TOCTOU: the URL is validated and the resolved IPs are pinned into the same task-local ContextVar that the custom httpx resolver returns at dispatch time. A malicious zone that returns a public IP at validation and a private IP at request time gets blocked.

Audit — @log_inbound / @log_outbound

from resilience_kit.audit import log_outbound

@log_outbound(service="partner_api", redact=["card_number", "cvv"])
async def charge(card_number: str, cvv: str, amount: Decimal):
    ...

Pluggable sink: stdlib logging (default), Postgres ([audit-postgres]), Django ORM (via the Django adapter), or your own — see Pluggability below.

Framework-agnostic middleware factories

request_id, body_limit, security_headers, selective_cors, rate_limit_headers, exception_logging — exposed as ASGI/WSGI factories. The Django and FastAPI adapters wrap them; you can also mount them directly in any Starlette / WSGI app.


Pluggability

Every swappable subsystem is a typing.Protocol plus a provider that resolves implementations from (in order):

  1. An explicit callable / instance you pass in.
  2. A RESILIENCE_<SUBSYSTEM>_BACKEND="myapp.module:MyBackend" settings string.
  3. An entry point named in your pyproject.toml.
  4. A builtin (memory, redis, …).

Swappable subsystems at v0.1: cache backend · circuit-breaker backend · throttle backend · audit sink · audit sanitizer · metrics sink · settings source · clock · audit dispatcher.

A third-party entry point whose name matches a kit builtin (e.g. memory) shadows the builtin by design — useful for drop-in replacement, footgun for collisions. Namespace your backend names. See ADR 0004 and LLD §3.

Shipping your own backend

# in your own package's pyproject.toml
[project.entry-points."resilience_kit.cache_backends"]
memcached = "rk_memcached:MemcachedCache"
# rk_memcached.py
from resilience_kit.cache.base import AsyncCache

class MemcachedCache(AsyncCache):
    async def get(self, key): ...
    async def set(self, key, value, ttl=None): ...
    async def incr(self, key, amount=1): ...
    async def delete(self, key): ...
    async def health_check(self): ...
pip install rk-memcached
export RESILIENCE_CACHE_BACKEND=memcached

No fork. No monkey-patching. Same @resilient decorator everywhere.


Configuration

Single ResilienceSettings model (pydantic v2). Resolved through get_settings() indirection so callers never import a global. Loaded from env with the RESILIENCE_ prefix, or from settings.RESILIENCE in Django.

Key Default Notes
backend auto auto / redis / memory / pybreaker
redis_url None when set, [redis] backends become available
defaults.retry.max_attempts 3
defaults.retry.wait_min / wait_max 1 / 10 seconds
defaults.circuit_breaker.fail_max 5
defaults.circuit_breaker.reset_timeout 30 seconds
defaults.circuit_breaker.success_threshold 2 half-open → closed
defaults.throttle.auth_rate 5/min applied to /auth/*
ssrf.block_private_ips True
ssrf.outbound_allowlist ["*"] exact host or .suffix
crypto.field_encryption_key None required outside dev/test
audit.sink stdlib logging importable string, callable, or entry-point name
audit.redact_fields ["password", "token", "secret", "authorization"]

Full pydantic schema in LLD.md §10. Settings keys are loaded with the RESILIENCE_ prefix and __ nested delimiter (e.g. RESILIENCE_DEFAULTS__RETRY__MAX_ATTEMPTS=5).


Exceptions you might catch

from resilience_kit.exceptions import (
    TransientError,           # retryable, transport-layer
    ExternalTimeoutError,     # subtype of TransientError
    ExternalServiceError,     # upstream returned non-success
    ServiceUnavailableError,  # breaker is OPEN — adapter maps to 503
    RateLimitError,           # throttle tripped — adapter maps to 429
    DecryptionError,          # FernetCipher failed (key rotation?)
    ValidationError,          # SSRF guard / config-time validation
)

The Django and FastAPI adapters map these to the right HTTP responses out of the box.


Testing your code that uses the kit

from resilience_kit.testing import reset_all_singletons, FakeClock, FakeAuditSink

@pytest.fixture(autouse=True)
async def _reset():
    await reset_all_singletons()

Integration tests against real backends use testcontainers-redis:

@pytest.mark.integration
async def test_throttle_under_load(redis_url):
    settings.RESILIENCE_REDIS_URL = redis_url
    # ... hammer the throttle, assert exact counts.

Compatibility

  • Python: 3.11, 3.12, 3.13
  • Django: 4.2 LTS, 5.x
  • FastAPI: 0.110+ (Starlette 0.36+)
  • httpx: >=0.27, <0.29
  • Redis / Valkey: Redis 7+, Valkey 8+ (verified in CI against both Docker images)

Roadmap

v0.1 — everything in this README, both adapters, both boilerplates migrated to depend on it. Nine milestones:

Milestone Status
M0 Repo scaffold ✅ shipped
M1 Core primitives, in-memory only ✅ shipped
M2 Redis/Valkey + pybreaker backends ✅ shipped
M3 HTTP client + SSRF + crypto ✅ shipped
M4 Audit + middleware + metrics + entry-point wiring ✅ shipped
M5 FastAPI adapter ✅ shipped
M6 Django adapter ✅ shipped
M7 Boilerplate migrations ✅ shipped
M8a 0.1.0rc1 on PyPI ✅ shipped
M8b Release-prep + v0.1.0 final ✅ shipped

v0.2+ — Flask adapter · Celery adapter · Litestar adapter · resilience_kit doctor CLI · Sphinx site.

Per-milestone feature list and exit gates: ROADMAP.md. Final file tree with arrival milestones per file: DIRECTORY-TREE.md.


Security

Report vulnerabilities privately via GitHub Security Advisories — full policy in SECURITY.md.


Contributing

Dev setup, the full local CI gate, contract-suite expectations, and the step-by-step for shipping a third-party backend live in CONTRIBUTING.md. The contract test suite under tests/contract/ is the source of truth: any new backend must pass it, parametrized in.


License

MIT. See LICENSE.


Related

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

resilience_kit-0.1.0.tar.gz (111.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

resilience_kit-0.1.0-py3-none-any.whl (155.4 kB view details)

Uploaded Python 3

File details

Details for the file resilience_kit-0.1.0.tar.gz.

File metadata

  • Download URL: resilience_kit-0.1.0.tar.gz
  • Upload date:
  • Size: 111.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for resilience_kit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 15ff31f3b9b1a82b44d9413b94129aa16b10b44df76ce2282c6a3f5e92b4052b
MD5 4d40d8041a8d0af8635674e3a47a14be
BLAKE2b-256 f669ef8f33ef66187a87a325d387f1cdc87946de543eb767fa2744eca0b517c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for resilience_kit-0.1.0.tar.gz:

Publisher: release.yml on prajwalmahajan101/resilience-kit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file resilience_kit-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: resilience_kit-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 155.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for resilience_kit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e77213820f26ce7ef9e7dbb8cdcd37bc12659be9c18222757516ea9fc326ff54
MD5 70dbbec5f1e87d4ddf3b5b3224669ad8
BLAKE2b-256 8b029ae87e6cf77a6b7e0de5b13f609601b15c8e6eb0b7e96c663c52821e760f

See more details on using hashes here.

Provenance

The following attestation bundles were made for resilience_kit-0.1.0-py3-none-any.whl:

Publisher: release.yml on prajwalmahajan101/resilience-kit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page