Metadata-Version: 2.4
Name: baldur-framework
Version: 1.8.0
Summary: Baldur Reliability Layer for Python Applications
Project-URL: Homepage, https://github.com/baldurhq/baldur
Project-URL: Documentation, https://github.com/baldurhq/baldur#readme
Project-URL: Repository, https://github.com/baldurhq/baldur
Project-URL: Issues, https://github.com/baldurhq/baldur/issues
Author: Baldur Contributors
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: baldur,circuit-breaker,dead-letter-queue,dlq,fault-tolerance,resilience,retry
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Distributed Computing
Requires-Python: >=3.11
Requires-Dist: psutil>=5.9.0
Requires-Dist: pydantic-settings>=2.2.0
Requires-Dist: pydantic>=2.6.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.33.0
Requires-Dist: sqlparse>=0.5.4
Requires-Dist: structlog>=24.1.0
Requires-Dist: typer>=0.12
Provides-Extra: all
Requires-Dist: arq>=0.26; extra == 'all'
Requires-Dist: cachetools>=5.0.0; extra == 'all'
Requires-Dist: celery>=5.3; extra == 'all'
Requires-Dist: croniter>=1.3; extra == 'all'
Requires-Dist: croniter>=2.0; extra == 'all'
Requires-Dist: cryptography>=46.0.7; extra == 'all'
Requires-Dist: django-db-connection-pool>=1.2.0; extra == 'all'
Requires-Dist: django-redis>=5.4; extra == 'all'
Requires-Dist: django>=4.2; extra == 'all'
Requires-Dist: djangorestframework-simplejwt>=5.3; extra == 'all'
Requires-Dist: djangorestframework>=3.14; extra == 'all'
Requires-Dist: drf-spectacular>=0.27; extra == 'all'
Requires-Dist: fastapi>=0.100; extra == 'all'
Requires-Dist: flask>=2.3; extra == 'all'
Requires-Dist: httpx>=0.24.0; extra == 'all'
Requires-Dist: lmdb>=1.4.0; extra == 'all'
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'all'
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == 'all'
Requires-Dist: opentelemetry-exporter-prometheus>=0.41b0; extra == 'all'
Requires-Dist: opentelemetry-instrumentation-celery>=0.41b0; extra == 'all'
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == 'all'
Requires-Dist: opentelemetry-instrumentation-logging>=0.41b0; extra == 'all'
Requires-Dist: opentelemetry-instrumentation-requests>=0.41b0; extra == 'all'
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'all'
Requires-Dist: orjson>=3.9.0; extra == 'all'
Requires-Dist: prometheus-client>=0.17; extra == 'all'
Requires-Dist: psycopg2-binary>=2.9; extra == 'all'
Requires-Dist: pyarrow>=14.0; extra == 'all'
Requires-Dist: pymemcache>=4.0; extra == 'all'
Requires-Dist: redis>=4.2; extra == 'all'
Requires-Dist: rq-scheduler>=0.13; extra == 'all'
Requires-Dist: rq>=1.16; extra == 'all'
Requires-Dist: sqlalchemy>=2.0; extra == 'all'
Requires-Dist: starlette>=0.27; extra == 'all'
Requires-Dist: tenacity>=8.0.0; extra == 'all'
Requires-Dist: uvicorn[standard]>=0.23; extra == 'all'
Provides-Extra: arq
Requires-Dist: arq>=0.26; extra == 'arq'
Requires-Dist: croniter>=2.0; extra == 'arq'
Requires-Dist: redis>=4.2; extra == 'arq'
Provides-Extra: async-queue
Requires-Dist: arq>=0.26; extra == 'async-queue'
Requires-Dist: croniter>=2.0; extra == 'async-queue'
Requires-Dist: redis>=4.2; extra == 'async-queue'
Provides-Extra: caching
Requires-Dist: cachetools>=5.0.0; extra == 'caching'
Requires-Dist: pymemcache>=4.0; extra == 'caching'
Provides-Extra: celery
Requires-Dist: celery>=5.3; extra == 'celery'
Requires-Dist: redis>=4.2; extra == 'celery'
Provides-Extra: chaos
Requires-Dist: croniter>=1.3; extra == 'chaos'
Provides-Extra: crypto
Requires-Dist: cryptography>=46.0.7; extra == 'crypto'
Provides-Extra: databases
Requires-Dist: sqlalchemy>=2.0; extra == 'databases'
Provides-Extra: dev
Requires-Dist: arq>=0.26; extra == 'dev'
Requires-Dist: bandit>=1.7; extra == 'dev'
Requires-Dist: black>=26.3.1; extra == 'dev'
Requires-Dist: cachetools>=5.0.0; extra == 'dev'
Requires-Dist: celery>=5.3; extra == 'dev'
Requires-Dist: croniter>=1.3; extra == 'dev'
Requires-Dist: croniter>=2.0; extra == 'dev'
Requires-Dist: cryptography>=46.0.7; extra == 'dev'
Requires-Dist: django-db-connection-pool>=1.2.0; extra == 'dev'
Requires-Dist: django-redis>=5.4; extra == 'dev'
Requires-Dist: django-stubs[compatible-mypy]>=6.0; extra == 'dev'
Requires-Dist: django>=4.2; extra == 'dev'
Requires-Dist: djangorestframework-simplejwt>=5.3; extra == 'dev'
Requires-Dist: djangorestframework>=3.14; extra == 'dev'
Requires-Dist: drf-spectacular>=0.27; extra == 'dev'
Requires-Dist: factory-boy>=3.3; extra == 'dev'
Requires-Dist: fastapi>=0.100; extra == 'dev'
Requires-Dist: flask>=2.3; extra == 'dev'
Requires-Dist: freezegun>=1.2; extra == 'dev'
Requires-Dist: httpx>=0.24.0; extra == 'dev'
Requires-Dist: hypothesis>=6.0; extra == 'dev'
Requires-Dist: isort>=5.12; extra == 'dev'
Requires-Dist: lmdb>=1.4.0; extra == 'dev'
Requires-Dist: locust>=2.0; extra == 'dev'
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'dev'
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == 'dev'
Requires-Dist: opentelemetry-exporter-prometheus>=0.41b0; extra == 'dev'
Requires-Dist: opentelemetry-instrumentation-celery>=0.41b0; extra == 'dev'
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == 'dev'
Requires-Dist: opentelemetry-instrumentation-logging>=0.41b0; extra == 'dev'
Requires-Dist: opentelemetry-instrumentation-requests>=0.41b0; extra == 'dev'
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'dev'
Requires-Dist: orjson>=3.9.0; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pip-licenses>=5.0; extra == 'dev'
Requires-Dist: pre-commit>=4.0; extra == 'dev'
Requires-Dist: prometheus-client>=0.17; extra == 'dev'
Requires-Dist: psycopg2-binary>=2.9; extra == 'dev'
Requires-Dist: pyarrow>=14.0; extra == 'dev'
Requires-Dist: pymemcache>=4.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest-benchmark>=4.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest-django>=4.5; extra == 'dev'
Requires-Dist: pytest-mock>=3.10; extra == 'dev'
Requires-Dist: pytest-rerunfailures>=14.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.2; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: redis>=4.2; extra == 'dev'
Requires-Dist: rq-scheduler>=0.13; extra == 'dev'
Requires-Dist: rq>=1.16; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Requires-Dist: starlette>=0.27; extra == 'dev'
Requires-Dist: tenacity>=8.0.0; extra == 'dev'
Requires-Dist: toxiproxy-python>=0.1; extra == 'dev'
Requires-Dist: types-cachetools; extra == 'dev'
Requires-Dist: types-croniter; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Requires-Dist: types-requests; extra == 'dev'
Requires-Dist: uvicorn[standard]>=0.23; extra == 'dev'
Provides-Extra: disk-buffer
Requires-Dist: lmdb>=1.4.0; extra == 'disk-buffer'
Provides-Extra: django
Requires-Dist: django-db-connection-pool>=1.2.0; extra == 'django'
Requires-Dist: django-redis>=5.4; extra == 'django'
Requires-Dist: django>=4.2; extra == 'django'
Requires-Dist: djangorestframework-simplejwt>=5.3; extra == 'django'
Requires-Dist: djangorestframework>=3.14; extra == 'django'
Provides-Extra: docs
Requires-Dist: mkdocs-llmstxt>=0.1; extra == 'docs'
Requires-Dist: mkdocs-material[recommended]>=9.5; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
Provides-Extra: export
Requires-Dist: pyarrow>=14.0; extra == 'export'
Provides-Extra: fast-json
Requires-Dist: orjson>=3.9.0; extra == 'fast-json'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
Requires-Dist: httpx>=0.24.0; extra == 'fastapi'
Requires-Dist: starlette>=0.27; extra == 'fastapi'
Requires-Dist: uvicorn[standard]>=0.23; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask>=2.3; extra == 'flask'
Provides-Extra: openapi
Requires-Dist: drf-spectacular>=0.27; extra == 'openapi'
Provides-Extra: opentelemetry
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-exporter-prometheus>=0.41b0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-instrumentation-celery>=0.41b0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-instrumentation-logging>=0.41b0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-instrumentation-requests>=0.41b0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'opentelemetry'
Provides-Extra: postgres
Requires-Dist: django>=4.2; extra == 'postgres'
Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
Provides-Extra: prometheus
Requires-Dist: prometheus-client>=0.17; extra == 'prometheus'
Provides-Extra: redis
Requires-Dist: redis>=4.2; extra == 'redis'
Provides-Extra: rq
Requires-Dist: rq-scheduler>=0.13; extra == 'rq'
Requires-Dist: rq>=1.16; extra == 'rq'
Provides-Extra: tenacity
Requires-Dist: tenacity>=8.0.0; extra == 'tenacity'
Provides-Extra: test-e2e
Requires-Dist: playwright>=1.60; extra == 'test-e2e'
Description-Content-Type: text/markdown

# Baldur

[![CI](https://github.com/baldurhq/baldur/actions/workflows/ci-oss-mirror.yml/badge.svg)](https://github.com/baldurhq/baldur/actions/workflows/ci-oss-mirror.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
[![PyPI](https://img.shields.io/pypi/v/baldur-framework.svg)](https://pypi.org/project/baldur-framework/)
[![Docs](https://img.shields.io/badge/docs-baldur.sh-1f6feb.svg)](https://baldur.sh)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13522/badge)](https://www.bestpractices.dev/projects/13522)

**Baldur** is a self-healing reliability layer for Python applications. It puts
circuit breaker, retry, and fallback behind a single decorator, so a flaky
downstream stops cascading into your service — and it ships the operational
surface you need to actually run that in production: health checks, Prometheus
and OpenTelemetry metrics, graceful shutdown, and a built-in web console. The
core is framework-agnostic, with first-class adapters for Django, FastAPI,
Flask, and Celery.

![Terminal demo: the payment gateway dies mid-traffic — five failed charges are captured, the breaker trips, and on recovery Baldur replays all five. Zero lost.](https://raw.githubusercontent.com/baldurhq/baldur/main/.github/assets/demo-self-healing.gif)

*Real run of the shipped demo: the gateway dies mid-traffic — five charges
fail and are captured with their arguments, the circuit breaker opens and
shields the dying dependency, and the moment it closes again Baldur replays
all five for real. Zero lost. Reproduce it yourself:*

```bash
pip install "baldur-framework[celery]"
python -m baldur.scripts.demo_self_healing
```

*(The breaker states and DLQ tallies in the recording are read live from the
running framework. In your own service the same story surfaces as Baldur's
structured log events, live breaker state in the built-in web console, and
the Prometheus/OpenTelemetry metrics.)*

## Why Baldur?

- **One decorator, whole pipeline.** `@baldur.protected("name")` composes
  circuit breaker, retry with backoff, timeout, fallback, and idempotency into
  one ordered pipeline — instead of hand-wiring three separate libraries and
  hoping they interact correctly under failure.
- **Zero-config start, production path built in.** Out of the box everything
  runs on an in-memory backend — no Redis, no env vars, no Docker. When you
  move to multiple workers, add Redis and the same code shares state across
  the fleet. Call sites never change.
- **Operate it, don't just import it.** A built-in web console shows every
  breaker's live state and gives you runtime on/off controls; health checks
  tell your load balancer the truth; metrics come standard.
- **Framework-native.** Django, FastAPI, Flask, and Celery adapters wire the
  cache, metrics, and lifecycle hooks at startup, so protection works with
  your framework's idioms rather than around them.

## Install

The Python package is `baldur` (you `import baldur`); the PyPI distribution is
`baldur-framework`.

```bash
pip install baldur-framework                 # framework-agnostic core
pip install baldur-framework[django]         # Django integration
pip install baldur-framework[fastapi]        # FastAPI integration
pip install baldur-framework[flask]          # Flask integration
pip install baldur-framework[celery]         # Celery task protection
pip install baldur-framework[redis]          # Redis-backed shared state
pip install baldur-framework[prometheus]     # Prometheus metrics
```

## Quick example

```python
import baldur


@baldur.protected("charge-customer")
def charge(order_id: str) -> dict:
    # Wrapped in a circuit breaker by default. With zero configuration this
    # runs on an in-memory fallback — no Redis, no env vars, no Docker.
    return payment_gateway.charge(order_id)
```

When the payment gateway starts failing, the breaker opens and your service
answers fast instead of stacking up timeouts. Need more than the default?
Compose the pipeline declaratively:

```python
@baldur.protected(
    "charge-customer",
    retry=True,                              # retry with exponential backoff
    timeout=5.0,                             # per-call time budget
    fallback=lambda: {"status": "queued"},   # graceful answer while OPEN
    idempotency_key="order_id",              # dedupe concurrent duplicates
)
def charge(order_id: str) -> dict:
    return payment_gateway.charge(order_id)
```

Sync and async callables are both supported — the decorator auto-detects
coroutine functions.

## What's in the box (OSS, Apache-2.0)

| Capability | What it gives you |
|------------|-------------------|
| [Circuit breaker](docs/concepts/oss/circuit-breaker.md) | Stops cascading failure; bounded half-open probes on recovery |
| [Retry with backoff](docs/concepts/oss/retry.md) | Exponential backoff with jitter and bounded attempts |
| [Fallback & composition](docs/concepts/foundations/composition.md) | One ordered pipeline for all resilience patterns |
| [Idempotency](docs/concepts/oss/idempotency.md) | Concurrent duplicate calls execute the side effect exactly once |
| [Bulkhead isolation](docs/concepts/foundations/bulkhead.md) | Each dependency gets a fixed slice of concurrency, so one slow dependency can't drain every worker |
| [Dead-letter queue + replay](docs/concepts/foundations/dlq-replay.md) | A call that fails for good is captured with its context and replayed once the dependency recovers |
| [Health checks](docs/concepts/oss/health-check.md) | Liveness/readiness that reflect real dependency state |
| [Graceful shutdown](docs/concepts/oss/graceful-shutdown.md) | Drain in-flight work cleanly on restart and deploy |
| [Metrics](docs/concepts/oss/metrics.md) | Prometheus and OpenTelemetry, emitted by default |
| [System control](docs/concepts/oss/system-control.md) | Instant kill switch and dry-run mode for Baldur's automation — no redeploy |
| [Web console](docs/concepts/foundations/web-console.md) | Built-in operations console: live breaker state, controls, recovery |
| [Precomputed cache](docs/concepts/oss/precomputed-cache.md) | Health/status endpoints answer from a warm cache, so constant probing stays cheap |

The read path heals the same way. Here a Django app under live HTTP traffic
(recorded from a demo harness driving it) loses its network path to Redis for
21 seconds — every request keeps returning 200 off the in-memory cache tier,
and the Redis tier resyncs itself on recovery:

![Terminal demo: a Django app keeps serving 200s through a 21-second Redis outage](https://raw.githubusercontent.com/baldurhq/baldur/main/.github/assets/redis-dies-app-survives.gif)

## Baldur PRO

PRO adds the durable, fleet-level machinery on top of the same API — nothing in
the core gets relicensed or replaced. Highlights:
[DLQ at scale](docs/concepts/foundations/dlq-replay.md) (batch replay from the
console, success-rate-driven pacing, a disk-durable outbox, and archive/purge
retention), hash-chained [audit trail](docs/concepts/pro/audit.md),
[unified notifications](docs/concepts/pro/unified-notification.md),
[emergency mode](docs/concepts/pro/emergency-mode.md),
[bulkhead thread-pool isolation](docs/concepts/foundations/bulkhead.md),
[adaptive throttling](docs/concepts/pro/throttle.md),
[canary recovery](docs/concepts/pro/canary-recovery.md),
[governance gates](docs/concepts/pro/governance.md), and a
[meta-watchdog](docs/concepts/pro/meta-watchdog.md) that watches Baldur itself.

See the full [OSS vs PRO capability matrix](docs/concepts/oss-vs-pro.md) and
[pricing](https://baldur.sh/pricing/).

## Documentation

Full documentation lives at **<https://baldur.sh>**.

- [What is Baldur?](docs/what-is-baldur.md) — the problem it solves and how
- Getting started: [Django](docs/getting-started/django.md) ·
  [FastAPI](docs/getting-started/fastapi.md) ·
  [Flask](docs/getting-started/flask.md) ·
  [Celery](docs/getting-started/celery.md)
- [Concept guides](https://baldur.sh) — one page per capability, linked
  throughout this README
- [API reference](https://baldur.sh/reference/)
- [Troubleshooting](docs/troubleshooting.md)
- [Compatibility](docs/compatibility.md)

## Using Baldur with AI assistants

Building with an AI coding assistant (Claude Code, Cursor, Copilot, Codex)? Run
`baldur init-ai` in your repo to drop an `AGENTS.md` (read by Cursor, Copilot,
and Codex) plus a `CLAUDE.md` that imports it for Claude Code — together they
teach the assistant to reach for `@baldur.protected("name")` instead of
hand-rolling a circuit breaker. See
[Using Baldur with AI assistants](docs/getting-started/ai-assistants.md).

## Compatibility

| Component | Minimum | Tested in CI |
|-----------|---------|--------------|
| Python | 3.11 | 3.11 · 3.12 · 3.13 |
| Django | 4.2 | 4.2 LTS · 5.2 LTS · 6.0 |
| FastAPI | 0.100 | latest ≥ floor (smoke) |
| Flask | 2.3 | latest ≥ floor (smoke) |
| Celery | 5.3 | 5.4 |
| Redis server | — | 7.x |

See [Compatibility](docs/compatibility.md) for the full matrix, the
Python × Django test grid, and the version support policy.

## Early access

Baldur is early, and it is looking for a small number of teams already running
a Python service in production to work with directly. If that is you, the
details and how to reach me are in
[Discussions](https://github.com/baldurhq/baldur/discussions).

## License

Baldur is released under the Apache License 2.0 — see [LICENSE](LICENSE) and
[NOTICE](NOTICE).

## Contributing

Contributions are welcome under the Apache License 2.0. Pull requests are
accepted through a sign-off-based [DCO](https://developercertificate.org/) flow —
see [CONTRIBUTING.md](CONTRIBUTING.md) for the full model.

- **Ideas, or showing what you built** →
  [Discussions](https://github.com/baldurhq/baldur/discussions).
- **Bugs / feature requests / docs** → open an issue or a pull request.
- **Security** → see [SECURITY.md](SECURITY.md) (no public issues for vulnerabilities).
- **Usage questions / commercial** → `support@baldur.sh`.
