resilience-kit 0.1.0__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 (102) hide show
  1. resilience_kit-0.1.0/.gitignore +54 -0
  2. resilience_kit-0.1.0/CHANGELOG.md +133 -0
  3. resilience_kit-0.1.0/LICENSE +21 -0
  4. resilience_kit-0.1.0/PKG-INFO +440 -0
  5. resilience_kit-0.1.0/README.md +358 -0
  6. resilience_kit-0.1.0/pyproject.toml +160 -0
  7. resilience_kit-0.1.0/src/resilience_kit/__init__.py +123 -0
  8. resilience_kit-0.1.0/src/resilience_kit/_providers.py +104 -0
  9. resilience_kit-0.1.0/src/resilience_kit/_version.py +7 -0
  10. resilience_kit-0.1.0/src/resilience_kit/adapters/__init__.py +13 -0
  11. resilience_kit-0.1.0/src/resilience_kit/adapters/_envelope.py +135 -0
  12. resilience_kit-0.1.0/src/resilience_kit/adapters/django/__init__.py +27 -0
  13. resilience_kit-0.1.0/src/resilience_kit/adapters/django/apps.py +155 -0
  14. resilience_kit-0.1.0/src/resilience_kit/adapters/django/drf_throttles.py +163 -0
  15. resilience_kit-0.1.0/src/resilience_kit/adapters/django/exception_handler.py +94 -0
  16. resilience_kit-0.1.0/src/resilience_kit/adapters/django/fields.py +70 -0
  17. resilience_kit-0.1.0/src/resilience_kit/adapters/django/management/__init__.py +0 -0
  18. resilience_kit-0.1.0/src/resilience_kit/adapters/django/management/commands/__init__.py +0 -0
  19. resilience_kit-0.1.0/src/resilience_kit/adapters/django/management/commands/resilience_reset.py +60 -0
  20. resilience_kit-0.1.0/src/resilience_kit/adapters/django/management/commands/resilience_status.py +65 -0
  21. resilience_kit-0.1.0/src/resilience_kit/adapters/django/middleware.py +392 -0
  22. resilience_kit-0.1.0/src/resilience_kit/adapters/fastapi/__init__.py +27 -0
  23. resilience_kit-0.1.0/src/resilience_kit/adapters/fastapi/dependencies.py +137 -0
  24. resilience_kit-0.1.0/src/resilience_kit/adapters/fastapi/exception_handlers.py +114 -0
  25. resilience_kit-0.1.0/src/resilience_kit/adapters/fastapi/fields.py +82 -0
  26. resilience_kit-0.1.0/src/resilience_kit/adapters/fastapi/lifespan.py +145 -0
  27. resilience_kit-0.1.0/src/resilience_kit/adapters/fastapi/middleware.py +124 -0
  28. resilience_kit-0.1.0/src/resilience_kit/audit/__init__.py +29 -0
  29. resilience_kit-0.1.0/src/resilience_kit/audit/backends/__init__.py +23 -0
  30. resilience_kit-0.1.0/src/resilience_kit/audit/backends/base.py +81 -0
  31. resilience_kit-0.1.0/src/resilience_kit/audit/backends/noop.py +30 -0
  32. resilience_kit-0.1.0/src/resilience_kit/audit/backends/postgres.py +182 -0
  33. resilience_kit-0.1.0/src/resilience_kit/audit/backends/stdlib_logging.py +59 -0
  34. resilience_kit-0.1.0/src/resilience_kit/audit/decorators.py +237 -0
  35. resilience_kit-0.1.0/src/resilience_kit/audit/dispatch.py +234 -0
  36. resilience_kit-0.1.0/src/resilience_kit/audit/factory.py +110 -0
  37. resilience_kit-0.1.0/src/resilience_kit/audit/sanitizers.py +105 -0
  38. resilience_kit-0.1.0/src/resilience_kit/cache/__init__.py +11 -0
  39. resilience_kit-0.1.0/src/resilience_kit/cache/base.py +85 -0
  40. resilience_kit-0.1.0/src/resilience_kit/cache/memory_impl.py +139 -0
  41. resilience_kit-0.1.0/src/resilience_kit/cache/provider.py +96 -0
  42. resilience_kit-0.1.0/src/resilience_kit/cache/redis_impl.py +245 -0
  43. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/__init__.py +17 -0
  44. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/base.py +113 -0
  45. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/lua_scripts.py +96 -0
  46. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/memory_impl.py +183 -0
  47. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/provider.py +133 -0
  48. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/pybreaker_impl.py +135 -0
  49. resilience_kit-0.1.0/src/resilience_kit/circuit_breaker/redis_impl.py +295 -0
  50. resilience_kit-0.1.0/src/resilience_kit/context.py +90 -0
  51. resilience_kit-0.1.0/src/resilience_kit/crypto/__init__.py +27 -0
  52. resilience_kit-0.1.0/src/resilience_kit/crypto/exceptions.py +38 -0
  53. resilience_kit-0.1.0/src/resilience_kit/crypto/fernet.py +166 -0
  54. resilience_kit-0.1.0/src/resilience_kit/decorators.py +146 -0
  55. resilience_kit-0.1.0/src/resilience_kit/dispatch/__init__.py +14 -0
  56. resilience_kit-0.1.0/src/resilience_kit/dispatch/fire_and_forget.py +249 -0
  57. resilience_kit-0.1.0/src/resilience_kit/exceptions/__init__.py +36 -0
  58. resilience_kit-0.1.0/src/resilience_kit/exceptions/base.py +59 -0
  59. resilience_kit-0.1.0/src/resilience_kit/exceptions/http_status.py +59 -0
  60. resilience_kit-0.1.0/src/resilience_kit/exceptions/infrastructure.py +138 -0
  61. resilience_kit-0.1.0/src/resilience_kit/exceptions/validation.py +92 -0
  62. resilience_kit-0.1.0/src/resilience_kit/health.py +127 -0
  63. resilience_kit-0.1.0/src/resilience_kit/http_client/__init__.py +33 -0
  64. resilience_kit-0.1.0/src/resilience_kit/http_client/auth.py +161 -0
  65. resilience_kit-0.1.0/src/resilience_kit/http_client/client.py +385 -0
  66. resilience_kit-0.1.0/src/resilience_kit/http_client/dns_pin.py +134 -0
  67. resilience_kit-0.1.0/src/resilience_kit/http_client/errors.py +137 -0
  68. resilience_kit-0.1.0/src/resilience_kit/http_client/session.py +107 -0
  69. resilience_kit-0.1.0/src/resilience_kit/metrics.py +207 -0
  70. resilience_kit-0.1.0/src/resilience_kit/middleware/__init__.py +29 -0
  71. resilience_kit-0.1.0/src/resilience_kit/middleware/_asgi.py +16 -0
  72. resilience_kit-0.1.0/src/resilience_kit/middleware/body_limit.py +92 -0
  73. resilience_kit-0.1.0/src/resilience_kit/middleware/exception_logging.py +122 -0
  74. resilience_kit-0.1.0/src/resilience_kit/middleware/rate_limit_headers.py +69 -0
  75. resilience_kit-0.1.0/src/resilience_kit/middleware/request_id.py +100 -0
  76. resilience_kit-0.1.0/src/resilience_kit/middleware/security_headers.py +74 -0
  77. resilience_kit-0.1.0/src/resilience_kit/middleware/selective_cors.py +119 -0
  78. resilience_kit-0.1.0/src/resilience_kit/py.typed +0 -0
  79. resilience_kit-0.1.0/src/resilience_kit/recovery.py +236 -0
  80. resilience_kit-0.1.0/src/resilience_kit/registry.py +206 -0
  81. resilience_kit-0.1.0/src/resilience_kit/retry/__init__.py +11 -0
  82. resilience_kit-0.1.0/src/resilience_kit/retry/backoff.py +71 -0
  83. resilience_kit-0.1.0/src/resilience_kit/retry/decorator.py +362 -0
  84. resilience_kit-0.1.0/src/resilience_kit/runtime.py +201 -0
  85. resilience_kit-0.1.0/src/resilience_kit/settings.py +125 -0
  86. resilience_kit-0.1.0/src/resilience_kit/ssrf/__init__.py +24 -0
  87. resilience_kit-0.1.0/src/resilience_kit/ssrf/_ipchecks.py +68 -0
  88. resilience_kit-0.1.0/src/resilience_kit/ssrf/guard.py +204 -0
  89. resilience_kit-0.1.0/src/resilience_kit/tasks/__init__.py +22 -0
  90. resilience_kit-0.1.0/src/resilience_kit/tasks/queue.py +100 -0
  91. resilience_kit-0.1.0/src/resilience_kit/tasks/registry.py +80 -0
  92. resilience_kit-0.1.0/src/resilience_kit/testing/__init__.py +60 -0
  93. resilience_kit-0.1.0/src/resilience_kit/testing/contract.py +137 -0
  94. resilience_kit-0.1.0/src/resilience_kit/testing/fakes.py +129 -0
  95. resilience_kit-0.1.0/src/resilience_kit/testing/reset.py +58 -0
  96. resilience_kit-0.1.0/src/resilience_kit/throttle/__init__.py +25 -0
  97. resilience_kit-0.1.0/src/resilience_kit/throttle/base.py +143 -0
  98. resilience_kit-0.1.0/src/resilience_kit/throttle/lua_scripts.py +71 -0
  99. resilience_kit-0.1.0/src/resilience_kit/throttle/memory_impl.py +94 -0
  100. resilience_kit-0.1.0/src/resilience_kit/throttle/provider.py +93 -0
  101. resilience_kit-0.1.0/src/resilience_kit/throttle/redis_impl.py +206 -0
  102. resilience_kit-0.1.0/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,133 @@
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.0] - 2026-06-11
8
+
9
+ First public stable release. Pinned by both reference boilerplates (`fastapi_boilerplate`, `django_boilerplate`) post-M7 migration. Builds on `0.1.0rc1` with the §3.1 pre-cut ergonomics bundle (D1 helpers A–E) that closes every M7 dogfooding finding flagged as high-severity. Surface is additive — `rc1` users upgrade with a pin bump unless they hit the blocker recipes documented in [`docs/MIGRATION-rc1-to-v0.1.0.md`](./docs/MIGRATION-rc1-to-v0.1.0.md).
10
+
11
+ ### Added
12
+
13
+ - `.github/workflows/release.yml` — tag-driven release pipeline. Pushing a `v*` tag triggers: tag↔`_version.py` consistency check, `uv build` of sdist + wheel, `twine check --strict`, a smoke-import of the built wheel in a clean venv, PyPI publish via Trusted Publishing (no token in secrets), and a GitHub Release with notes extracted from this CHANGELOG. Pre-release tags (`*rc*` / `*a*` / `*b*` / `*dev*`) get the GitHub `--prerelease` flag automatically. **Requires a one-time PyPI Trusted Publisher configuration** at <https://pypi.org/manage/account/publishing/> (repo `prajwalmahajan101/resilience-kit`, workflow `release.yml`, environment `pypi`).
14
+ - `resilience_kit.context.bind_to(target)` — context manager that mirrors `resilience_kit.context.request_id` into a consumer-owned `ContextVar`. Lets a boilerplate that keeps its own `request_id_ctx` receive the value the kit's middleware seeded without rewriting its own context layer. Closes the M7 FastAPI dogfooding finding (kit-set `request_id` was `null` in the boilerplate's own logging / envelope paths). See MIGRATION §10.3.
15
+ - `resilience_kit.testing.reset_all_singletons_async()` — async ergonomic shim around `reset_all_singletons()`. The underlying reset is non-blocking; this wrapper exists so async test harnesses (`pytest-asyncio`, `pytest-trio`) can `await` it inline instead of routing through `asyncio.to_thread`.
16
+ - `resilience_kit.testing.verify_envelope_contract(handler, envelope_schema, exceptions=...)` — pytest helper that exercises every HTTP-reachable kit exception through an adopter's handler + envelope schema and collects every failure into a single `AssertionError`. Lets a project that layers its own exception handler on top of (or in place of) the kit's prove, in their own test suite, that the resulting wire shape stays valid for every kit exception class. Closes Django dogfooding §3.6 untested-bridge finding.
17
+ - `resilience_kit.adapters._envelope.from_exception(exc, *, envelope_cls=None, extra_headers=None)` — framework-agnostic envelope builder. Returns `(body, status, headers)` for any `ResilienceKitError`. When `envelope_cls` (a pydantic model) is supplied, the body is projected onto the consumer's chosen field aliases (`error_code | code | error`, `message | detail`, `details` dict or `errors` list, plus optional `request_id` and `success` flags). The FastAPI and Django adapters now delegate their envelope construction to this helper — wire shape is unchanged. Closes the M7 FastAPI two-envelope finding by giving consumers a one-call way to re-wrap kit exceptions into their own envelope. See MIGRATION §10.1 / §10.2.
18
+ - `resilience_kit.runtime.legacy_env_alias(env=None, aliases=DEFAULT_ALIASES, warn=True)` + `DEFAULT_ALIASES` — opt-in translator that copies legacy boilerplate env-var values (`FIELD_ENCRYPTION_KEY`, `RATE_LIMIT_*`, `CIRCUIT_BREAKER_*`, `REDIS_URL`, …) onto their `RESILIENCE_*` equivalents and emits a `DeprecationWarning`. The kit-prefixed name always wins on collision. Call once at the top of the settings module before `ResilienceSettings()` instantiates. Closes the Django dogfooding §3.3 deploy-time risk (silent tuning loss on the next prod deploy). See MIGRATION §10.5.
19
+
20
+ ### Changed (breaking inside 0.1.x)
21
+
22
+ - `ResilienceSettings` now uses `extra="forbid"` at the model root: unknown top-level keys in dict-shaped inputs (Django `settings.RESILIENCE = {...}`, programmatic `model_validate(...)`, JSON configs) raise `pydantic.ValidationError` instead of being silently dropped. Catches the legacy-key footgun (`CIRCUIT_BREAKER_CONFIG`, `RATE_LIMIT_CONFIG`, `FIELD_ENCRYPTION_KEY`) that the M7 boilerplate migration was about to import verbatim. Strictness on unknown `RESILIENCE_*` env vars is **not** included — pydantic-settings filters them at the source layer; tracked as a follow-up.
23
+
24
+ ### Fixed
25
+
26
+ - `EncryptedCharField.from_db_value` (Django adapter) and `EncryptedString.process_result_value` (FastAPI/SQLAlchemy adapter) now coerce `bytes` / `bytearray` inputs to `str` before handing them to `FernetCipher.decrypt`. Previously a `bytes` cipher round-trip from a custom driver, raw query, or non-default encoding raised `AttributeError` inside `FernetCipher.decrypt` (which calls `.encode("ascii")`). `None` and `str` continue to pass through unchanged. ISSUE-003.
27
+
28
+ ### Documentation
29
+
30
+ - `_providers.py` docstring and `docs/LLD.md` §3 now document that third-party entry points shadow same-named kit builtins by design (drop-in replacement use case) and that this is a footgun for accidental name collisions — operators should namespace third-party backend names. Cross-linked from [ADR 0004](./docs/adr/0004-entry-points-for-third-party-backends.md). ISSUE-005.
31
+ - `pyproject.toml`: the empty `[project.entry-points."resilience_kit.settings_sources"]` group now carries an inline comment explaining it is an intentional extension hook (LLD §3) so a future "remove dead config" pass doesn't strip it. ISSUE-004.
32
+
33
+ ### Documentation — M7 dogfooding patterns (no behavior change)
34
+
35
+ - `docs/MIGRATION-from-boilerplate-embedded.md` §10 — new "Patterns from the M7 dogfooding reports" section covering the four traps the FastAPI + Django boilerplate migrations hit on the fly: the `BaseCustomError(ResilienceKitError)` exception-bridge pattern, the two-handler envelope-collision footgun, request-id `ContextVar` interop, the provider-API rename table, and the operator env-var translation table. **Operator action required before promoting any v0.1.0 migration past staging** — audit every `.env*` file against the env-var translation table in §10.5; the kit does not read the legacy `RATE_LIMIT_*` / `CIRCUIT_BREAKER_*` / `FIELD_ENCRYPTION_KEY` names.
36
+ - `adapters/fastapi.exception_handlers.install` and `adapters/django.exception_handler.handle` docstrings now warn that installing the kit handlers alongside a project's own exception handlers can silently change the wire shape for kit-raised exceptions (the M7 FastAPI report §0.2 footgun) and cross-link the migration-doc remediation.
37
+
38
+ ## [0.1.0rc1] - 2026-06-10
39
+
40
+ 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.
41
+
42
+ ### Added
43
+
44
+ - 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.
45
+
46
+ <!-- m5-placeholder: feat/m5-fastapi-adapter replaces this line -->
47
+ - M5: FastAPI adapter (`resilience_kit.adapters.fastapi`).
48
+ - `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.
49
+ - `install_health_routes(app)` — mounts `GET /healthz` (always 200) and `GET /readyz` (reads `health_snapshot`, propagates 200 / 503). Excluded from the OpenAPI schema.
50
+ - `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.
51
+ - `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.
52
+ - `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.
53
+ - `request_id_dep()` — returns the active `request_id` ContextVar.
54
+ - `EncryptedString` — SQLAlchemy 2.x `TypeDecorator[str]` over `FernetCipher`; `cache_ok = True`. None passes through.
55
+ - `[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.
56
+ - `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.
57
+ - ADR 0010 (FastAPI adapter shape).
58
+ <!-- m6-placeholder: feat/m6-django-adapter replaces this line -->
59
+ - M6: Django adapter (`resilience_kit.adapters.django`).
60
+ - `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.
61
+ - 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.
62
+ - 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.
63
+ - `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.
64
+ - `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.
65
+ - Management commands — `resilience_status` (with `--json`) prints overall + per-backend + per-service breaker state; `resilience_reset <service|--all>` force-closes breakers.
66
+ - `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.
67
+ - `[dependency-groups] test-integration` gains `pytest-django` + `psycopg[binary]`.
68
+ - `docs/sync-vs-async.md` + ADR 0011 (Django sync/async bridge).
69
+
70
+ - M4: Audit + middleware + metrics + entry-point wiring.
71
+ - `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.
72
+ - `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.
73
+ - `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.
74
+ - `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.
75
+ - `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).
76
+ - `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.
77
+ - `AsyncAPIClient` now routes its default `on_outbound` audit through `audit.get_dispatcher()` — caller-supplied callbacks still win.
78
+ - 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).
79
+ - `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.
80
+ - Top-level re-exports: `log_inbound`, `log_outbound`, `AuditEvent`, `health_snapshot`, `HealthAggregate`, `HealthStatus`.
81
+ - `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.
82
+ - ADRs: 0005 (fire-and-forget audit), 0009 (entry-point precedence chain).
83
+
84
+ - M3: HTTP client + SSRF + crypto.
85
+ - `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.
86
+ - `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.
87
+ - `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.
88
+ - `pinned(host_to_ips)` context manager — token-restoring ContextVar set/reset; isolates DNS pins per asyncio task (LLD §9).
89
+ - Auth helpers — `BearerAuth`, `BasicAuth`, `HMACAuth` (HMAC-SHA256 over `METHOD\nPATH\nTS\nBODY`, with optional `X-Signature-Key-Id`).
90
+ - `pinned_httpx_client(**kwargs)` and `pinned_requests_session()` factories — extras-gated; raise `MissingExtraError` at import / first call when the optional dep is missing.
91
+ - `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`.
92
+ - `CryptoSettings.environment: Literal["prod","dev","test"]` field on `ResilienceSettings`.
93
+ - 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`.
94
+ - Contract suite additions for SSRF allow-list shapes and httpx error mapping.
95
+ - `resilience_kit.__init__` re-exports `AsyncAPIClient`, `pinned`, `FernetCipher` via lazy `__getattr__` so `import resilience_kit` does not require the `[http]` / `[crypto]` extras.
96
+ - `.importlinter` extended with the L3 modules; `testing.reset_all_singletons` now clears the Fernet cache.
97
+ - ADRs: 0007 (DNS pin via ContextVar), 0008 (Fernet env-guard).
98
+
99
+ - M2: Redis / Valkey + pybreaker backends.
100
+ - Shared provider-resolution chain (`resilience_kit._providers.resolve_provider`) implementing LLD §3: explicit → importable string → entry point → builtin → `UnknownBackendError` with the list of options.
101
+ - `PyBreakerAsyncBreaker` — async wrapper over the synchronous `pybreaker` library using `CircuitBreaker.calling()` so the contextmanager protocol lets the breaker observe both sync and async upstreams.
102
+ - `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.
103
+ - `RedisAsyncThrottle` — sliding-window Lua + in-call recovery probe (30 s gate so quiet workers don't keep PINGing). Fail-open to memory throttle.
104
+ - `RedisAsyncCache` — plain Redis ops, fail-open for everything except `incr` (which raises rather than diverge on the authoritative counter).
105
+ - `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.
106
+ - `MissingExtraError` raised at module-import time for every backend gated behind a pip extra. The error message carries the exact `pip install` hint.
107
+ - `auto` backend picker — chooses `redis` when `RESILIENCE_REDIS_URL` is set + the extra is importable, else `memory`.
108
+ - Contract suite parametrized over `memory + pybreaker + redis` with testcontainers-backed Redis. Backend-N/A combinations (e.g. FakeClock + Redis TTL) skip cleanly.
109
+ - Integration test proving the ROADMAP M2 exit gate: paused container → fail-open → unpause → recovery in < 5 s.
110
+
111
+ ### Fixed
112
+
113
+ - `reset_settings_cache` now restores the default `EnvSettingsSource` so tests that swap in a `FixedSource` don't leak it into following tests.
114
+
115
+ - M1: core primitives, in-memory only.
116
+ - Public decorators: `@retry`, `@retry_on_failure(name)`, `@circuit_breaker(name)`, `@resilient(name)` — sync + async, breaker-outer / retry-inner composition.
117
+ - Per-service `ResilienceRegistry` with defaults overlay and cached breaker instances.
118
+ - Exception hierarchy with stable `error_code` and structured `details`: `TransientError`, `ExternalTimeoutError`, `ExternalServiceError`, `ServiceUnavailableError`, `RepositoryError`, `DecryptionError`, `MissingExtraError`, `UnknownBackendError`, `ValidationError`, `RateLimitError` (with `response_headers()`).
119
+ - `ResilienceSettings` (pydantic v2) loaded from env via `RESILIENCE_*`, plus pluggable `SettingsSource` indirection.
120
+ - `request_id` / `correlation_id` ContextVars (LLD §9).
121
+ - Protocols: `AsyncBreaker`, `AsyncThrottle`, `AsyncCache`, `MetricsSink`, `Clock`, `SettingsSource` (LLD §2, locked at v0.1).
122
+ - In-memory backends for breaker (state machine with fake-clock support), throttle (sliding-window deque), cache (TTL + lazy eviction, atomic `incr`).
123
+ - Throttle scope keys: `IP`, `ENDPOINT`, `USER_TIER`, `GLOBAL`, `BURST`, `AUTH`; `Rate.parse("60/min")` parser.
124
+ - `MetricsSink` protocol with `NoopMetricsSink` (default) and `StdlibLoggingMetricsSink`.
125
+ - Decorrelated-jitter, exponential, and constant backoff strategies; jitter selectable via settings.
126
+ - Testing helpers: `FakeClock`, `FakeAuditSink`, `reset_all_singletons`.
127
+ - Contract test suite parametrized over backends — currently `memory` only; M2 wires `redis` and `pybreaker`.
128
+ - Activated full layered-architecture contract in `.importlinter`.
129
+ - 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.
130
+
131
+ [Unreleased]: https://github.com/prajwalmahajan101/resilience-kit/compare/v0.1.0...HEAD
132
+ [0.1.0]: https://github.com/prajwalmahajan101/resilience-kit/compare/v0.1.0rc1...v0.1.0
133
+ [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.