resilience-kit 0.1.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. resilience_kit-0.1.0rc1/.gitignore +54 -0
  2. resilience_kit-0.1.0rc1/CHANGELOG.md +101 -0
  3. resilience_kit-0.1.0rc1/LICENSE +21 -0
  4. resilience_kit-0.1.0rc1/PKG-INFO +429 -0
  5. resilience_kit-0.1.0rc1/README.md +347 -0
  6. resilience_kit-0.1.0rc1/pyproject.toml +156 -0
  7. resilience_kit-0.1.0rc1/src/resilience_kit/__init__.py +123 -0
  8. resilience_kit-0.1.0rc1/src/resilience_kit/_providers.py +94 -0
  9. resilience_kit-0.1.0rc1/src/resilience_kit/_version.py +7 -0
  10. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/__init__.py +13 -0
  11. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/__init__.py +27 -0
  12. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/apps.py +155 -0
  13. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/drf_throttles.py +163 -0
  14. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/exception_handler.py +85 -0
  15. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/fields.py +60 -0
  16. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/management/__init__.py +0 -0
  17. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/management/commands/__init__.py +0 -0
  18. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/management/commands/resilience_reset.py +60 -0
  19. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/management/commands/resilience_status.py +65 -0
  20. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/django/middleware.py +392 -0
  21. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/fastapi/__init__.py +27 -0
  22. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/fastapi/dependencies.py +137 -0
  23. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/fastapi/exception_handlers.py +102 -0
  24. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/fastapi/fields.py +72 -0
  25. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/fastapi/lifespan.py +145 -0
  26. resilience_kit-0.1.0rc1/src/resilience_kit/adapters/fastapi/middleware.py +124 -0
  27. resilience_kit-0.1.0rc1/src/resilience_kit/audit/__init__.py +29 -0
  28. resilience_kit-0.1.0rc1/src/resilience_kit/audit/backends/__init__.py +23 -0
  29. resilience_kit-0.1.0rc1/src/resilience_kit/audit/backends/base.py +81 -0
  30. resilience_kit-0.1.0rc1/src/resilience_kit/audit/backends/noop.py +30 -0
  31. resilience_kit-0.1.0rc1/src/resilience_kit/audit/backends/postgres.py +182 -0
  32. resilience_kit-0.1.0rc1/src/resilience_kit/audit/backends/stdlib_logging.py +59 -0
  33. resilience_kit-0.1.0rc1/src/resilience_kit/audit/decorators.py +237 -0
  34. resilience_kit-0.1.0rc1/src/resilience_kit/audit/dispatch.py +234 -0
  35. resilience_kit-0.1.0rc1/src/resilience_kit/audit/factory.py +110 -0
  36. resilience_kit-0.1.0rc1/src/resilience_kit/audit/sanitizers.py +105 -0
  37. resilience_kit-0.1.0rc1/src/resilience_kit/cache/__init__.py +11 -0
  38. resilience_kit-0.1.0rc1/src/resilience_kit/cache/base.py +85 -0
  39. resilience_kit-0.1.0rc1/src/resilience_kit/cache/memory_impl.py +139 -0
  40. resilience_kit-0.1.0rc1/src/resilience_kit/cache/provider.py +96 -0
  41. resilience_kit-0.1.0rc1/src/resilience_kit/cache/redis_impl.py +245 -0
  42. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/__init__.py +17 -0
  43. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/base.py +113 -0
  44. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/lua_scripts.py +96 -0
  45. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/memory_impl.py +183 -0
  46. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/provider.py +133 -0
  47. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/pybreaker_impl.py +135 -0
  48. resilience_kit-0.1.0rc1/src/resilience_kit/circuit_breaker/redis_impl.py +295 -0
  49. resilience_kit-0.1.0rc1/src/resilience_kit/context.py +62 -0
  50. resilience_kit-0.1.0rc1/src/resilience_kit/crypto/__init__.py +27 -0
  51. resilience_kit-0.1.0rc1/src/resilience_kit/crypto/exceptions.py +38 -0
  52. resilience_kit-0.1.0rc1/src/resilience_kit/crypto/fernet.py +166 -0
  53. resilience_kit-0.1.0rc1/src/resilience_kit/decorators.py +146 -0
  54. resilience_kit-0.1.0rc1/src/resilience_kit/dispatch/__init__.py +14 -0
  55. resilience_kit-0.1.0rc1/src/resilience_kit/dispatch/fire_and_forget.py +249 -0
  56. resilience_kit-0.1.0rc1/src/resilience_kit/exceptions/__init__.py +36 -0
  57. resilience_kit-0.1.0rc1/src/resilience_kit/exceptions/base.py +59 -0
  58. resilience_kit-0.1.0rc1/src/resilience_kit/exceptions/http_status.py +59 -0
  59. resilience_kit-0.1.0rc1/src/resilience_kit/exceptions/infrastructure.py +138 -0
  60. resilience_kit-0.1.0rc1/src/resilience_kit/exceptions/validation.py +92 -0
  61. resilience_kit-0.1.0rc1/src/resilience_kit/health.py +127 -0
  62. resilience_kit-0.1.0rc1/src/resilience_kit/http_client/__init__.py +33 -0
  63. resilience_kit-0.1.0rc1/src/resilience_kit/http_client/auth.py +161 -0
  64. resilience_kit-0.1.0rc1/src/resilience_kit/http_client/client.py +385 -0
  65. resilience_kit-0.1.0rc1/src/resilience_kit/http_client/dns_pin.py +134 -0
  66. resilience_kit-0.1.0rc1/src/resilience_kit/http_client/errors.py +137 -0
  67. resilience_kit-0.1.0rc1/src/resilience_kit/http_client/session.py +107 -0
  68. resilience_kit-0.1.0rc1/src/resilience_kit/metrics.py +207 -0
  69. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/__init__.py +29 -0
  70. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/_asgi.py +16 -0
  71. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/body_limit.py +92 -0
  72. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/exception_logging.py +122 -0
  73. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/rate_limit_headers.py +69 -0
  74. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/request_id.py +100 -0
  75. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/security_headers.py +74 -0
  76. resilience_kit-0.1.0rc1/src/resilience_kit/middleware/selective_cors.py +119 -0
  77. resilience_kit-0.1.0rc1/src/resilience_kit/py.typed +0 -0
  78. resilience_kit-0.1.0rc1/src/resilience_kit/recovery.py +236 -0
  79. resilience_kit-0.1.0rc1/src/resilience_kit/registry.py +206 -0
  80. resilience_kit-0.1.0rc1/src/resilience_kit/retry/__init__.py +11 -0
  81. resilience_kit-0.1.0rc1/src/resilience_kit/retry/backoff.py +71 -0
  82. resilience_kit-0.1.0rc1/src/resilience_kit/retry/decorator.py +362 -0
  83. resilience_kit-0.1.0rc1/src/resilience_kit/runtime.py +113 -0
  84. resilience_kit-0.1.0rc1/src/resilience_kit/settings.py +114 -0
  85. resilience_kit-0.1.0rc1/src/resilience_kit/ssrf/__init__.py +24 -0
  86. resilience_kit-0.1.0rc1/src/resilience_kit/ssrf/_ipchecks.py +68 -0
  87. resilience_kit-0.1.0rc1/src/resilience_kit/ssrf/guard.py +204 -0
  88. resilience_kit-0.1.0rc1/src/resilience_kit/tasks/__init__.py +22 -0
  89. resilience_kit-0.1.0rc1/src/resilience_kit/tasks/queue.py +100 -0
  90. resilience_kit-0.1.0rc1/src/resilience_kit/tasks/registry.py +80 -0
  91. resilience_kit-0.1.0rc1/src/resilience_kit/testing/__init__.py +53 -0
  92. resilience_kit-0.1.0rc1/src/resilience_kit/testing/fakes.py +129 -0
  93. resilience_kit-0.1.0rc1/src/resilience_kit/testing/reset.py +47 -0
  94. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/__init__.py +25 -0
  95. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/base.py +143 -0
  96. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/lua_scripts.py +71 -0
  97. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/memory_impl.py +94 -0
  98. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/provider.py +93 -0
  99. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/redis_impl.py +206 -0
  100. resilience_kit-0.1.0rc1/src/resilience_kit/throttle/scopes.py +80 -0
@@ -0,0 +1,54 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+ pip-wheel-metadata/
13
+
14
+ # Virtual envs
15
+ .venv/
16
+ venv/
17
+ env/
18
+
19
+ # Tooling caches
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+ .pytest_cache/
23
+ .tox/
24
+ .coverage
25
+ .coverage.*
26
+ htmlcov/
27
+ coverage.xml
28
+ .hypothesis/
29
+
30
+ # uv / poetry
31
+ .uv/
32
+ poetry.lock
33
+
34
+ # IDE / editor
35
+ .idea/
36
+ .vscode/
37
+ *.swp
38
+ *.swo
39
+ .DS_Store
40
+
41
+ # Local secrets
42
+ .env
43
+ .env.*
44
+ !.env.example
45
+
46
+ # Build artefacts
47
+ *.log
48
+
49
+ # Local Claude Code rules — project-private
50
+ CLAUDE.md
51
+ .claude/
52
+
53
+ # Personal blog drafts — never committed; live local only
54
+ docs/blog/
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ All notable changes to `resilience-kit` are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0rc1] - 2026-06-10
8
+
9
+ First public release candidate. Cut to PyPI as a packaging smoke-test before the M7 boilerplate migration. Pre-release per [PEP 440](https://peps.python.org/pep-0440/) — `pip` will not install it without an explicit version pin or `--pre`. The 0.1.0 final ships at M8b after both boilerplates depend on the kit.
10
+
11
+ ### Added
12
+
13
+ - Top-level re-exports of `http_status_for` and `HTTP_STATUS_MAP` (the LLD §11 exception↔HTTP mapping). Adapters and callers no longer need to reach into `resilience_kit.exceptions` for the locked-contract status table.
14
+
15
+ <!-- m5-placeholder: feat/m5-fastapi-adapter replaces this line -->
16
+ - M5: FastAPI adapter (`resilience_kit.adapters.fastapi`).
17
+ - `resilience_lifespan(inner=None)` — factory returning a FastAPI lifespan that starts `recovery.monitor` on enter and drains the audit dispatcher + stops the monitor on exit. Composes with an optional inner lifespan.
18
+ - `install_health_routes(app)` — mounts `GET /healthz` (always 200) and `GET /readyz` (reads `health_snapshot`, propagates 200 / 503). Excluded from the OpenAPI schema.
19
+ - `install_exception_handlers(app)` — maps every `ResilienceKitError` to the LLD §11 envelope via `exceptions.http_status_for`; `RateLimitError` gets a dedicated handler so the 429 response carries `Retry-After` + `X-RateLimit-*` headers without an extra branch.
20
+ - `install_middleware_stack(app, **opts)` — mounts the kit's six ASGI middleware in the LLD §11 outer→inner order. `SelectiveCorsMiddleware` is only added when both `cors_allow_origins` and `cors_path_prefixes` are passed so apps that handle CORS upstream are never double-wrapped.
21
+ - `rate_limit(scope, rate, *, attr_from_request=None)` — FastAPI dependency factory backed by `throttle.provider.get_throttle`. Parses the rate spec once at build time; raises `RateLimitError` on deny.
22
+ - `request_id_dep()` — returns the active `request_id` ContextVar.
23
+ - `EncryptedString` — SQLAlchemy 2.x `TypeDecorator[str]` over `FernetCipher`; `cache_ok = True`. None passes through.
24
+ - `[fastapi]` extra now pulls `sqlalchemy>=2.0` and `httpx>=0.27,<0.29` so a single `pip install resilience-kit[fastapi]` wires the whole adapter.
25
+ - `tests/integration/fastapi_app/` — minimal example + e2e suite against `testcontainers[postgresql]`. Asserts the M5 exit gate: health routes serve 200, 3rd `/limited` returns 429 with `Retry-After`, `EncryptedString` round-trips (Fernet token on disk, plaintext through the ORM), `AsyncAPIClient` reaches a fake upstream via injected transport.
26
+ - ADR 0010 (FastAPI adapter shape).
27
+ <!-- m6-placeholder: feat/m6-django-adapter replaces this line -->
28
+ - M6: Django adapter (`resilience_kit.adapters.django`).
29
+ - `ResilienceConfig` AppConfig — reads `settings.RESILIENCE['services']`, registers each per-service override, and spawns a daemon thread that owns a private asyncio loop driving `recovery.monitor` for the worker lifetime. atexit hook drains the audit dispatcher + stops the monitor on graceful exit. Idempotent across Django's autoreloader.
30
+ - Six middleware classes mirroring the kit's ASGI stack — `RequestIdMiddleware`, `BodyLimitMiddleware`, `SecurityHeadersMiddleware`, `SelectiveCorsMiddleware`, `RateLimitHeadersMiddleware`, `ExceptionLoggingMiddleware`. Both `sync_capable` + `async_capable`; the last two implement `process_exception` so view-raised kit errors are caught regardless of WSGI / ASGI mode.
31
+ - Five DRF throttle classes — `IPThrottle`, `UserTierThrottle`, `EndpointThrottle`, `BurstThrottle`, `AuthThrottle`. Subclass `BaseThrottle`, derive scope-specific keys via `throttle.scopes.build_key`, delegate to `throttle.provider.get_throttle()`. Rates resolve from `RESILIENCE_THROTTLE_RATES` (Django setting), with per-scope defaults. Deny raises `RateLimitError` so the response carries the LLD §11 envelope + the canonical `X-RateLimit-*` headers rather than DRF's `Throttled` shape.
32
+ - `handle(exc, context)` DRF exception handler — install via `REST_FRAMEWORK['EXCEPTION_HANDLER']`. Maps every `ResilienceKitError` through `exceptions.http_status_for`; non-kit exceptions fall through to DRF's default handler.
33
+ - `EncryptedCharField` — Django model field mirroring the FastAPI adapter's `EncryptedString`. `get_prep_value` + `from_db_value` over `FernetCipher`. Default `max_length=512`. `None` passes through.
34
+ - Management commands — `resilience_status` (with `--json`) prints overall + per-backend + per-service breaker state; `resilience_reset <service|--all>` force-closes breakers.
35
+ - `tests/integration/django_app/` — minimal Django + DRF project + e2e suite against `testcontainers postgres:16`. Asserts the M6 exit gate: middleware echoes X-Request-Id + X-Content-Type-Options; IPThrottle('2/min') denies the 3rd request with the LLD §11 envelope + Retry-After + X-RateLimit-Limit=2; EncryptedCharField round-trips (Fernet token on disk, plaintext through the ORM); management commands run.
36
+ - `[dependency-groups] test-integration` gains `pytest-django` + `psycopg[binary]`.
37
+ - `docs/sync-vs-async.md` + ADR 0011 (Django sync/async bridge).
38
+
39
+ - M4: Audit + middleware + metrics + entry-point wiring.
40
+ - `resilience_kit.dispatch.fire_and_forget` — shared bounded queue + background worker + graceful drain. Drop-newest (default) / drop-oldest overflow with `dispatch.dropped` metric. Worker spawned in `contextvars.copy_context()` to isolate per-request pins.
41
+ - `resilience_kit.health.health_snapshot()` — `/readyz` aggregator that walks `recovery.registered_backends()`, runs `health_check()` in parallel with per-probe timeout, and reduces to `ok` / `degraded_but_serving` / `degraded` with the matching Kubernetes-style HTTP status.
42
+ - `resilience_kit.metrics.get_metrics()` — settings-driven sink factory. Builtins `noop` and `stdlib_logging` published as entry points; unknown sink names log a warning and fall back to no-op so observability misconfiguration cannot crash the kit.
43
+ - `resilience_kit.audit` — full subsystem: `AuditEvent` (LLD §7 shape), `AuditBackend` protocol, `NoopAuditBackend` + `StdlibLoggingAuditBackend` builtins, `PostgresAuditBackend` (asyncpg + batched executemany; extra `[audit-postgres]`). `Sanitizer` protocol + `DefaultRedactor` deep-walking dicts/lists. `FireAndForgetDispatcher` (LLD §7: bounded queue + batched flush + backend retry x3 + stdlib_logging fallback) and `InlineDispatcher` (tests). `@log_inbound` / `@log_outbound` decorators capturing timing, outcome, error class+code, and ContextVar request_id / correlation_id; optional `payload_factory` extracts the audit payload from call args.
44
+ - `resilience_kit.middleware` — framework-agnostic ASGI middleware: `RequestIdMiddleware` (seeds + echoes request_id / correlation_id), `BodyLimitMiddleware` (413 on oversize Content-Length or streamed body), `SecurityHeadersMiddleware` (conservative default header set + overrides/extras), `SelectiveCorsMiddleware` (CORS only on configured path prefixes), `RateLimitHeadersMiddleware` (catches `RateLimitError` → canonical 429 + `X-RateLimit-*`), `ExceptionLoggingMiddleware` (maps every kit exception onto its locked LLD §11 HTTP status + `{error_code,message,details}` envelope; non-kit raises become a generic 500 with no stack leakage).
45
+ - `resilience_kit.tasks` — in-process fire-and-forget task queue on top of `dispatch.fire_and_forget`. `register(name)` decorator + `submit(name, *args, **kwargs)` API; missing handler raises at submit time, handler failures are logged + metered via `tasks.failed` without breaking the worker.
46
+ - `AsyncAPIClient` now routes its default `on_outbound` audit through `audit.get_dispatcher()` — caller-supplied callbacks still win.
47
+ - Entry-point discovery: every kit-shipped builtin is also published under the kit's groups (`resilience_kit.{cache,breaker,throttle,audit,metrics}_*`) so the provider chain has one shape end-to-end. `tests/fixtures/fake_third_party/` proves the chain via `tests/contract/test_provider_chain.py` (installs the fixture with `uv pip install -e`, asserts `_providers.resolve_provider` finds it).
48
+ - `tests/integration/test_audit_postgres.py` — testcontainers Postgres + `@log_outbound` lands a sanitized row in `resilience_kit_audit`. `tests/integration/test_readyz_degraded.py` — paused redis container → `health_snapshot()` reports non-OK.
49
+ - Top-level re-exports: `log_inbound`, `log_outbound`, `AuditEvent`, `health_snapshot`, `HealthAggregate`, `HealthStatus`.
50
+ - `testing.reset_all_singletons` now clears the audit dispatcher, tasks queue, and tasks-handler registry so pytest-asyncio per-test loops don't bind queues to closed loops.
51
+ - ADRs: 0005 (fire-and-forget audit), 0009 (entry-point precedence chain).
52
+
53
+ - M3: HTTP client + SSRF + crypto.
54
+ - `resilience_kit.ssrf` — `resolve_and_validate(url, strict=)`, `assert_public_url`, `assert_allowed_url`. Rejects non-http(s), private / loopback / link-local / multicast / reserved / unspecified addresses; allow-list supports exact host and `.suffix` matching.
55
+ - `resilience_kit.http_client` — `AsyncAPIClient(service)` composes `resolve_and_validate → assert_allowed_url → pinned(host→ips) → @resilient(service) → httpx.AsyncClient.request → map_httpx_errors → on_outbound audit hook` (LLD §5). Verb shortcuts (`get` / `post` / `put` / `patch` / `delete`). Sync mirror `request_sync` drives a private event loop only when no loop is running; raises `RuntimeError` from inside a running loop.
56
+ - `PinnedHTTPTransport` — `httpx.AsyncHTTPTransport` subclass that reads `pinned_dns` ContextVar and rewrites the request URL host to the pinned IP, preserving `Host` header + TLS SNI for cert verification.
57
+ - `pinned(host_to_ips)` context manager — token-restoring ContextVar set/reset; isolates DNS pins per asyncio task (LLD §9).
58
+ - Auth helpers — `BearerAuth`, `BasicAuth`, `HMACAuth` (HMAC-SHA256 over `METHOD\nPATH\nTS\nBODY`, with optional `X-Signature-Key-Id`).
59
+ - `pinned_httpx_client(**kwargs)` and `pinned_requests_session()` factories — extras-gated; raise `MissingExtraError` at import / first call when the optional dep is missing.
60
+ - `resilience_kit.crypto.FernetCipher.encrypt/decrypt` — SHA-256-of-secret key derivation, `lru_cache(maxsize=1)` instance. `settings.crypto.environment="prod"` refuses to start without `field_encryption_key`; `"dev"` / `"test"` fall back to an insecure-on-purpose constant with a one-time warning. Wrong key / corrupted token raises `DecryptionError`.
61
+ - `CryptoSettings.environment: Literal["prod","dev","test"]` field on `ResilienceSettings`.
62
+ - Exit-gate tests: TOCTOU DNS-rebinding under `tests/integration/test_dns_rebinding.py`; Fernet round-trip + prod-without-key refusal under `tests/unit/crypto/test_fernet.py`.
63
+ - Contract suite additions for SSRF allow-list shapes and httpx error mapping.
64
+ - `resilience_kit.__init__` re-exports `AsyncAPIClient`, `pinned`, `FernetCipher` via lazy `__getattr__` so `import resilience_kit` does not require the `[http]` / `[crypto]` extras.
65
+ - `.importlinter` extended with the L3 modules; `testing.reset_all_singletons` now clears the Fernet cache.
66
+ - ADRs: 0007 (DNS pin via ContextVar), 0008 (Fernet env-guard).
67
+
68
+ - M2: Redis / Valkey + pybreaker backends.
69
+ - Shared provider-resolution chain (`resilience_kit._providers.resolve_provider`) implementing LLD §3: explicit → importable string → entry point → builtin → `UnknownBackendError` with the list of options.
70
+ - `PyBreakerAsyncBreaker` — async wrapper over the synchronous `pybreaker` library using `CircuitBreaker.calling()` so the contextmanager protocol lets the breaker observe both sync and async upstreams.
71
+ - `RedisAsyncBreaker` — Redis/Valkey-backed breaker with an atomic Lua state machine (CLOSED ↔ OPEN ↔ HALF_OPEN in one EVALSHA). Fail-open delegation to `InMemoryAsyncBreaker` on any Redis error, self-registers with the recovery monitor, `NoScriptError` triggers script reload.
72
+ - `RedisAsyncThrottle` — sliding-window Lua + in-call recovery probe (30 s gate so quiet workers don't keep PINGing). Fail-open to memory throttle.
73
+ - `RedisAsyncCache` — plain Redis ops, fail-open for everything except `incr` (which raises rather than diverge on the authoritative counter).
74
+ - `RecoveryMonitor` singleton — settings-driven probe interval (default 10 s production, tunable to 0.2 s in tests), warm hooks fire after any backend recovers, graceful start/stop from adapters.
75
+ - `MissingExtraError` raised at module-import time for every backend gated behind a pip extra. The error message carries the exact `pip install` hint.
76
+ - `auto` backend picker — chooses `redis` when `RESILIENCE_REDIS_URL` is set + the extra is importable, else `memory`.
77
+ - Contract suite parametrized over `memory + pybreaker + redis` with testcontainers-backed Redis. Backend-N/A combinations (e.g. FakeClock + Redis TTL) skip cleanly.
78
+ - Integration test proving the ROADMAP M2 exit gate: paused container → fail-open → unpause → recovery in < 5 s.
79
+
80
+ ### Fixed
81
+
82
+ - `reset_settings_cache` now restores the default `EnvSettingsSource` so tests that swap in a `FixedSource` don't leak it into following tests.
83
+
84
+ - M1: core primitives, in-memory only.
85
+ - Public decorators: `@retry`, `@retry_on_failure(name)`, `@circuit_breaker(name)`, `@resilient(name)` — sync + async, breaker-outer / retry-inner composition.
86
+ - Per-service `ResilienceRegistry` with defaults overlay and cached breaker instances.
87
+ - Exception hierarchy with stable `error_code` and structured `details`: `TransientError`, `ExternalTimeoutError`, `ExternalServiceError`, `ServiceUnavailableError`, `RepositoryError`, `DecryptionError`, `MissingExtraError`, `UnknownBackendError`, `ValidationError`, `RateLimitError` (with `response_headers()`).
88
+ - `ResilienceSettings` (pydantic v2) loaded from env via `RESILIENCE_*`, plus pluggable `SettingsSource` indirection.
89
+ - `request_id` / `correlation_id` ContextVars (LLD §9).
90
+ - Protocols: `AsyncBreaker`, `AsyncThrottle`, `AsyncCache`, `MetricsSink`, `Clock`, `SettingsSource` (LLD §2, locked at v0.1).
91
+ - In-memory backends for breaker (state machine with fake-clock support), throttle (sliding-window deque), cache (TTL + lazy eviction, atomic `incr`).
92
+ - Throttle scope keys: `IP`, `ENDPOINT`, `USER_TIER`, `GLOBAL`, `BURST`, `AUTH`; `Rate.parse("60/min")` parser.
93
+ - `MetricsSink` protocol with `NoopMetricsSink` (default) and `StdlibLoggingMetricsSink`.
94
+ - Decorrelated-jitter, exponential, and constant backoff strategies; jitter selectable via settings.
95
+ - Testing helpers: `FakeClock`, `FakeAuditSink`, `reset_all_singletons`.
96
+ - Contract test suite parametrized over backends — currently `memory` only; M2 wires `redis` and `pybreaker`.
97
+ - Activated full layered-architecture contract in `.importlinter`.
98
+ - M0: repo scaffold — `pyproject.toml` with extras matrix, source layout with `py.typed`, ruff + mypy + import-linter + pydocstyle + darglint configs, pre-commit, GitHub Actions CI (lint / types / imports / tests on Python 3.11–3.13), CodeQL workflow, PR template, CODEOWNERS, dependabot, issue templates, smoke test.
99
+
100
+ [Unreleased]: https://github.com/prajwalmahajan101/resilience-kit/compare/v0.1.0rc1...HEAD
101
+ [0.1.0rc1]: https://github.com/prajwalmahajan101/resilience-kit/releases/tag/v0.1.0rc1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prajwal Mahajan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,429 @@
1
+ Metadata-Version: 2.4
2
+ Name: resilience-kit
3
+ Version: 0.1.0rc1
4
+ Summary: 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.
5
+ Project-URL: Homepage, https://github.com/prajwalmahajan101/resilience-kit
6
+ Project-URL: Documentation, https://github.com/prajwalmahajan101/resilience-kit/tree/main/docs
7
+ Project-URL: Repository, https://github.com/prajwalmahajan101/resilience-kit
8
+ Project-URL: Issues, https://github.com/prajwalmahajan101/resilience-kit/issues
9
+ Project-URL: Changelog, https://github.com/prajwalmahajan101/resilience-kit/blob/main/CHANGELOG.md
10
+ Author: Prajwal Mahajan
11
+ License: MIT License
12
+
13
+ Copyright (c) 2026 Prajwal Mahajan
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: circuit-breaker,django,fastapi,rate-limit,resilience,retry,ssrf,throttle
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Framework :: Django
36
+ Classifier: Framework :: FastAPI
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Programming Language :: Python
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Classifier: Programming Language :: Python :: 3.11
44
+ Classifier: Programming Language :: Python :: 3.12
45
+ Classifier: Programming Language :: Python :: 3.13
46
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
47
+ Classifier: Typing :: Typed
48
+ Requires-Python: >=3.11
49
+ Requires-Dist: pybreaker>=1.2
50
+ Requires-Dist: pydantic-settings>=2.2
51
+ Requires-Dist: pydantic>=2.6
52
+ Provides-Extra: all
53
+ Requires-Dist: asyncpg>=0.29; extra == 'all'
54
+ Requires-Dist: cryptography>=42.0; extra == 'all'
55
+ Requires-Dist: django<7,>=4.2; extra == 'all'
56
+ Requires-Dist: djangorestframework>=3.14; extra == 'all'
57
+ Requires-Dist: fastapi>=0.110; extra == 'all'
58
+ Requires-Dist: httpx<0.29,>=0.27; extra == 'all'
59
+ Requires-Dist: redis>=5.0; extra == 'all'
60
+ Requires-Dist: requests>=2.31; extra == 'all'
61
+ Requires-Dist: sqlalchemy>=2.0; extra == 'all'
62
+ Requires-Dist: starlette>=0.36; extra == 'all'
63
+ Provides-Extra: audit-postgres
64
+ Requires-Dist: asyncpg>=0.29; extra == 'audit-postgres'
65
+ Provides-Extra: crypto
66
+ Requires-Dist: cryptography>=42.0; extra == 'crypto'
67
+ Provides-Extra: django
68
+ Requires-Dist: django<7,>=4.2; extra == 'django'
69
+ Requires-Dist: djangorestframework>=3.14; extra == 'django'
70
+ Provides-Extra: fastapi
71
+ Requires-Dist: fastapi>=0.110; extra == 'fastapi'
72
+ Requires-Dist: httpx<0.29,>=0.27; extra == 'fastapi'
73
+ Requires-Dist: sqlalchemy>=2.0; extra == 'fastapi'
74
+ Requires-Dist: starlette>=0.36; extra == 'fastapi'
75
+ Provides-Extra: http
76
+ Requires-Dist: httpx<0.29,>=0.27; extra == 'http'
77
+ Provides-Extra: redis
78
+ Requires-Dist: redis>=5.0; extra == 'redis'
79
+ Provides-Extra: requests
80
+ Requires-Dist: requests>=2.31; extra == 'requests'
81
+ Description-Content-Type: text/markdown
82
+
83
+ # resilience-kit
84
+
85
+ > Framework-agnostic Python resilience + core-infrastructure kernel.
86
+ > 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.
87
+
88
+ [![PyPI](https://img.shields.io/pypi/v/resilience-kit.svg)](https://pypi.org/project/resilience-kit/)
89
+ [![Python](https://img.shields.io/pypi/pyversions/resilience-kit.svg)](https://pypi.org/project/resilience-kit/)
90
+ [![CI](https://github.com/prajwalmahajan101/resilience-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/prajwalmahajan101/resilience-kit/actions)
91
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
92
+
93
+ > **Status:** v0.1 — pre-release. **M0–M3 merged** (scaffold · in-memory primitives · Redis/pybreaker backends · HTTP client + SSRF + crypto); latest dev checkpoint is [`milestone/m3`](https://github.com/prajwalmahajan101/resilience-kit/tree/milestone/m3). M4–M8 outstanding; no PyPI release yet — the first installable version will be `v0.1.0` (see [Tagging convention](./docs/ROADMAP.md#tagging-convention)). Design is locked across four docs:
94
+ >
95
+ > | Doc | What it answers |
96
+ > |---|---|
97
+ > | [PRD.md](./docs/PRD.md) | What's in/out of scope, why, who it's for |
98
+ > | [ROADMAP.md](./docs/ROADMAP.md) | Feature-level breakdown per milestone (M0–M8) with exit gates |
99
+ > | [LLD.md](./docs/LLD.md) | Protocols, sequence diagrams, concurrency model, settings schema |
100
+ > | [DIRECTORY-TREE.md](./docs/DIRECTORY-TREE.md) | Every file with arrival milestone and required extra |
101
+ >
102
+ > APIs marked *locked* in PRD §5.4 and LLD §2 will not break before 1.0.
103
+
104
+ ---
105
+
106
+ ## Why
107
+
108
+ `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.
109
+
110
+ You probably want this if you've ever:
111
+
112
+ - Copy-pasted `retry` / `circuit_breaker` decorators between two services.
113
+ - Hand-rolled SSRF protection and then wondered if it survives DNS rebinding (it usually doesn't).
114
+ - Wrapped `redis-py` in a degrades-to-memory shim for the third time.
115
+ - 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.
116
+
117
+ ---
118
+
119
+ ## Install
120
+
121
+ ```bash
122
+ pip install resilience-kit # core: pure-python, no I/O deps
123
+ pip install "resilience-kit[fastapi,redis,http]" # FastAPI app on Valkey
124
+ pip install "resilience-kit[django,redis,http]" # Django app on Valkey
125
+ pip install "resilience-kit[all]" # everything
126
+ ```
127
+
128
+ ### Available extras
129
+
130
+ | Extra | Enables |
131
+ |---|---|
132
+ | *(none)* | retry, in-memory breaker/throttle/cache, ssrf guard, audit decorators (noop sink), middleware factories, metrics shim, tasks queue, testing helpers |
133
+ | `[redis]` | Valkey / Redis backends for breaker, throttle, cache |
134
+ | `[pybreaker]` | `pybreaker` backend for the circuit breaker |
135
+ | `[http]` | DNS-pinned `AsyncAPIClient` (httpx) |
136
+ | `[requests]` | `pinned_requests_session()` |
137
+ | `[crypto]` | `FernetCipher` for field-level encryption |
138
+ | `[audit-postgres]` | Postgres audit-log backend |
139
+ | `[django]` | Django + DRF adapter |
140
+ | `[fastapi]` | FastAPI + Starlette adapter |
141
+ | `[all]` | everything above |
142
+ | `[dev]` | tooling: testcontainers, pytest-asyncio, mypy, ruff |
143
+
144
+ 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.
145
+
146
+ ---
147
+
148
+ ## Quickstart — the one decorator you'll use the most
149
+
150
+ ```python
151
+ from resilience_kit import resilient
152
+
153
+ @resilient("partner_api") # circuit breaker (outer) + retry (inner)
154
+ async def get_balance(account_id: str) -> Decimal:
155
+ response = await http_client.get(f"/accounts/{account_id}/balance")
156
+ return Decimal(response.json()["balance"])
157
+ ```
158
+
159
+ That's it. Defaults from settings: 3 retries with exponential backoff + jitter, breaker opens after 5 failures, half-opens after 30s. Per-service overrides:
160
+
161
+ ```python
162
+ from resilience_kit import registry
163
+
164
+ registry.register_service("partner_api", {
165
+ "retry": {"max_attempts": 5, "wait_min": 2, "wait_max": 30},
166
+ "circuit_breaker": {"fail_max": 3, "reset_timeout": 60},
167
+ })
168
+ ```
169
+
170
+ ---
171
+
172
+ ## FastAPI
173
+
174
+ ```python
175
+ from fastapi import FastAPI, Depends
176
+ from resilience_kit.adapters.fastapi import lifespan, rate_limit, exception_handlers
177
+
178
+ app = FastAPI(lifespan=lifespan)
179
+ exception_handlers.install(app)
180
+
181
+ @app.get("/accounts/{id}", dependencies=[Depends(rate_limit("ip", "60/min"))])
182
+ async def read_account(id: str):
183
+ ...
184
+ ```
185
+
186
+ - `lifespan` starts the recovery monitor and mounts `/readyz`.
187
+ - `rate_limit(scope, rate)` is a FastAPI dependency over the kit's throttle.
188
+ - `exception_handlers.install(app)` maps kit exceptions (`ServiceUnavailableError`, `RateLimitError`, …) to JSON responses.
189
+
190
+ ## Django
191
+
192
+ ```python
193
+ # settings.py
194
+ INSTALLED_APPS = [..., "resilience_kit.adapters.django"]
195
+
196
+ MIDDLEWARE = [
197
+ "resilience_kit.adapters.django.middleware.RequestIdMiddleware",
198
+ "resilience_kit.adapters.django.middleware.RateLimitHeadersMiddleware",
199
+ ...
200
+ ]
201
+
202
+ REST_FRAMEWORK = {
203
+ "DEFAULT_THROTTLE_CLASSES": [
204
+ "resilience_kit.adapters.django.drf_throttles.IPThrottle",
205
+ "resilience_kit.adapters.django.drf_throttles.UserTierThrottle",
206
+ ],
207
+ "EXCEPTION_HANDLER": "resilience_kit.adapters.django.exception_handler.handle",
208
+ }
209
+
210
+ RESILIENCE = {
211
+ "BACKEND": "redis",
212
+ "REDIS_URL": env("REDIS_URL"),
213
+ "CRYPTO": {"FIELD_ENCRYPTION_KEY": env("FIELD_ENCRYPTION_KEY")},
214
+ }
215
+ ```
216
+
217
+ `./manage.py resilience_status` shows per-service breaker state. `./manage.py resilience_reset partner_api` force-closes one.
218
+
219
+ ---
220
+
221
+ ## What's in the box
222
+
223
+ ### Resilience primitives
224
+
225
+ | Primitive | Sync | Async | Backends |
226
+ |---|---|---|---|
227
+ | `@retry(...)` / `@retry_on_failure(name)` | ✅ | ✅ | n/a (pure logic) |
228
+ | `@circuit_breaker(name)` | ✅ | ✅ | `memory` (default), `pybreaker`, `redis` (atomic Lua) |
229
+ | `@resilient(name)` — breaker ∘ retry | ✅ | ✅ | — |
230
+ | Throttle — `rate_limit(scope, rate)` | ✅ | ✅ | `memory`, `redis` (global Lua) |
231
+ | Cache — `get_cache(alias)` | ✅ | ✅ | `memory` (TTL), `redis` |
232
+ | Recovery monitor — auto re-probe degraded backends | — | ✅ | — |
233
+
234
+ Scopes: `ip` · `endpoint` · `user_tier` · `global` · `burst` · `auth`. Rate syntax: `"60/min"`, `"10/sec"`, `"1000/hour"`.
235
+
236
+ ### Security
237
+
238
+ ```python
239
+ from resilience_kit.http_client import AsyncAPIClient
240
+ from resilience_kit.crypto import FernetCipher
241
+
242
+ async with AsyncAPIClient(service="partner_api") as client:
243
+ # SSRF guard + DNS pin + outbound allow-list + breaker + retry + audit — all composed.
244
+ data = await client.get("https://partner.example.com/v1/users/42")
245
+
246
+ token = FernetCipher.encrypt("very secret")
247
+ plaintext = FernetCipher.decrypt(token)
248
+ ```
249
+
250
+ 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.
251
+
252
+ ### Audit — `@log_inbound` / `@log_outbound`
253
+
254
+ ```python
255
+ from resilience_kit.audit import log_outbound
256
+
257
+ @log_outbound(service="partner_api", redact=["card_number", "cvv"])
258
+ async def charge(card_number: str, cvv: str, amount: Decimal):
259
+ ...
260
+ ```
261
+
262
+ Pluggable sink: stdlib logging (default), Postgres (`[audit-postgres]`), Django ORM (via the Django adapter), or your own — see *Pluggability* below.
263
+
264
+ ### Framework-agnostic middleware factories
265
+
266
+ `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.
267
+
268
+ ---
269
+
270
+ ## Pluggability
271
+
272
+ Every swappable subsystem is a `typing.Protocol` plus a provider that resolves implementations from (in order):
273
+
274
+ 1. An explicit callable / instance you pass in.
275
+ 2. A `RESILIENCE_<SUBSYSTEM>_BACKEND="myapp.module:MyBackend"` settings string.
276
+ 3. An entry point named in your `pyproject.toml`.
277
+ 4. A builtin (`memory`, `redis`, …).
278
+
279
+ Swappable subsystems at v0.1: **cache backend · circuit-breaker backend · throttle backend · audit sink · audit sanitizer · metrics sink · settings source · clock · audit dispatcher**.
280
+
281
+ ### Shipping your own backend
282
+
283
+ ```toml
284
+ # in your own package's pyproject.toml
285
+ [project.entry-points."resilience_kit.cache_backends"]
286
+ memcached = "rk_memcached:MemcachedCache"
287
+ ```
288
+
289
+ ```python
290
+ # rk_memcached.py
291
+ from resilience_kit.cache.base import AsyncCache
292
+
293
+ class MemcachedCache(AsyncCache):
294
+ async def get(self, key): ...
295
+ async def set(self, key, value, ttl=None): ...
296
+ async def incr(self, key, amount=1): ...
297
+ async def delete(self, key): ...
298
+ async def health_check(self): ...
299
+ ```
300
+
301
+ ```bash
302
+ pip install rk-memcached
303
+ export RESILIENCE_CACHE_BACKEND=memcached
304
+ ```
305
+
306
+ No fork. No monkey-patching. Same `@resilient` decorator everywhere.
307
+
308
+ ---
309
+
310
+ ## Configuration
311
+
312
+ 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.
313
+
314
+ | Key | Default | Notes |
315
+ |---|---|---|
316
+ | `backend` | `auto` | `auto` / `redis` / `memory` / `pybreaker` |
317
+ | `redis_url` | `None` | when set, `[redis]` backends become available |
318
+ | `defaults.retry.max_attempts` | `3` | |
319
+ | `defaults.retry.wait_min` / `wait_max` | `1` / `10` | seconds |
320
+ | `defaults.circuit_breaker.fail_max` | `5` | |
321
+ | `defaults.circuit_breaker.reset_timeout` | `30` | seconds |
322
+ | `defaults.circuit_breaker.success_threshold` | `2` | half-open → closed |
323
+ | `defaults.throttle.auth_rate` | `5/min` | applied to `/auth/*` |
324
+ | `ssrf.block_private_ips` | `True` | |
325
+ | `ssrf.outbound_allowlist` | `["*"]` | exact host or `.suffix` |
326
+ | `crypto.field_encryption_key` | `None` | required outside dev/test |
327
+ | `audit.sink` | stdlib logging | importable string, callable, or entry-point name |
328
+ | `audit.redact_fields` | `["password", "token", "secret", "authorization"]` | |
329
+
330
+ Full pydantic schema in [LLD.md §10](./docs/LLD.md). Settings keys are loaded with the `RESILIENCE_` prefix and `__` nested delimiter (e.g. `RESILIENCE_DEFAULTS__RETRY__MAX_ATTEMPTS=5`).
331
+
332
+ ---
333
+
334
+ ## Exceptions you might catch
335
+
336
+ ```python
337
+ from resilience_kit.exceptions import (
338
+ TransientError, # retryable, transport-layer
339
+ ExternalTimeoutError, # subtype of TransientError
340
+ ExternalServiceError, # upstream returned non-success
341
+ ServiceUnavailableError, # breaker is OPEN — adapter maps to 503
342
+ RateLimitError, # throttle tripped — adapter maps to 429
343
+ DecryptionError, # FernetCipher failed (key rotation?)
344
+ ValidationError, # SSRF guard / config-time validation
345
+ )
346
+ ```
347
+
348
+ The Django and FastAPI adapters map these to the right HTTP responses out of the box.
349
+
350
+ ---
351
+
352
+ ## Testing your code that uses the kit
353
+
354
+ ```python
355
+ from resilience_kit.testing import reset_all_singletons, FakeClock, FakeAuditSink
356
+
357
+ @pytest.fixture(autouse=True)
358
+ async def _reset():
359
+ await reset_all_singletons()
360
+ ```
361
+
362
+ Integration tests against real backends use `testcontainers-redis`:
363
+
364
+ ```python
365
+ @pytest.mark.integration
366
+ async def test_throttle_under_load(redis_url):
367
+ settings.RESILIENCE_REDIS_URL = redis_url
368
+ # ... hammer the throttle, assert exact counts.
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Compatibility
374
+
375
+ - **Python**: 3.11, 3.12, 3.13
376
+ - **Django**: 4.2 LTS, 5.x
377
+ - **FastAPI**: 0.110+ (Starlette 0.36+)
378
+ - **httpx**: `>=0.27, <0.29`
379
+ - **Redis / Valkey**: Redis 7+, Valkey 8+ (verified in CI against both Docker images)
380
+
381
+ ---
382
+
383
+ ## Roadmap
384
+
385
+ **v0.1** — everything in this README, both adapters, both boilerplates migrated to depend on it. Nine milestones:
386
+
387
+ | | Milestone | Status |
388
+ |---|---|---|
389
+ | M0 | Repo scaffold | ⬜ pending |
390
+ | M1 | Core primitives, in-memory only | ⬜ pending |
391
+ | M2 | Redis/Valkey + pybreaker backends | ⬜ pending |
392
+ | M3 | HTTP client + SSRF + crypto | ⬜ pending |
393
+ | M4 | Audit + middleware + metrics + entry-point wiring | ⬜ pending |
394
+ | M5 | FastAPI adapter | ⬜ pending |
395
+ | M6 | Django adapter | ⬜ pending |
396
+ | M7 | Boilerplate migrations | ⬜ pending |
397
+ | M8 | v0.1.0 PyPI release | ⬜ pending |
398
+
399
+ **v0.2+** — Flask adapter · Celery adapter · Litestar adapter · `resilience_kit doctor` CLI · Sphinx site.
400
+
401
+ Per-milestone feature list and exit gates: [ROADMAP.md](./docs/ROADMAP.md). Final file tree with arrival milestones per file: [DIRECTORY-TREE.md](./docs/DIRECTORY-TREE.md).
402
+
403
+ ---
404
+
405
+ ## Contributing
406
+
407
+ This is a portfolio project — issues and PRs welcome but I'm the only maintainer. The contract test suite under `tests/contract/` is the source of truth: any new backend must pass it, parametrized in. See [LLD.md §12](./docs/LLD.md) for the test strategy and [DIRECTORY-TREE.md](./docs/DIRECTORY-TREE.md) for where new code lands.
408
+
409
+ ```bash
410
+ uv sync --all-extras --dev
411
+ uv run pytest tests/contract -q # contract suite, all backends
412
+ uv run pytest tests/integration -q # adapter + testcontainers
413
+ uv run ruff check . && uv run ruff format --check .
414
+ uv run mypy --strict src
415
+ ```
416
+
417
+ ---
418
+
419
+ ## License
420
+
421
+ MIT. See [LICENSE](./LICENSE).
422
+
423
+ ---
424
+
425
+ ## Related
426
+
427
+ - [`prajwalmahajan101/fastapi_boilerplate`](https://github.com/prajwalmahajan101/fastapi_boilerplate) — async FastAPI starter, will depend on this kit from its next release.
428
+ - [`prajwalmahajan101/django_boilerplate`](https://github.com/prajwalmahajan101/django_boilerplate) — Django 6 + DRF starter, ditto.
429
+ - Blog: *Circuit-breaker placement is different in async than sync — here's why.* (forthcoming on Hashnode)