Python Circuit Breaker Box
A Python implementation of the Circuit Breaker pattern.
Features
- 🚀 Implementations:
- Redis-based
- In-memory
3.10-3.13 support.
- ⚡ Asynchronous API
- 🔧 Configurable parameters
- 🔄 Retries by tenacity
- 🛠️ FastAPI integration through custom exceptions
Installation
pip install circuit-breaker-box
Usage
Direct usage
import asyncio
import logging
from circuit_breaker_box import CircuitBreakerInMemory
MAX_RETRIES = 4
MAX_CACHE_SIZE = 256
CIRCUIT_BREAKER_MAX_FAILURE_COUNT = 1
RESET_TIMEOUT_IN_SECONDS = 10
SOME_HOST = "http://example.com/"
async def main() -> None:
"""Define CircuitBreakerInMemory or CircuitBreakerRedis and use in your application directly"""
logging.basicConfig(level=logging.DEBUG)
circuit_breaker = CircuitBreakerInMemory(
reset_timeout_in_seconds=RESET_TIMEOUT_IN_SECONDS,
max_failure_count=CIRCUIT_BREAKER_MAX_FAILURE_COUNT,
max_cache_size=MAX_CACHE_SIZE,
)
# circuit_breaker is open state for SOME_HOST
assert await circuit_breaker.is_host_available(host=SOME_HOST)
for _ in range(MAX_RETRIES):
# circuit_breaker is half-open for SOME_HOST
await circuit_breaker.increment_failures_count(host=SOME_HOST)
# after failure count more then CIRCUIT_BREAKER_MAX_FAILURE_COUNT value circuit_breaker is closed
assert await circuit_breaker.is_host_available(host=SOME_HOST) is False
# close state reset to open state after RESET_TIMEOUT_IN_SECONDS delay
await asyncio.sleep(RESET_TIMEOUT_IN_SECONDS)
assert await circuit_breaker.is_host_available(host=SOME_HOST) is True
if __name__ == "__main__":
asyncio.run(main())
>>> circuit_breaker_box.circuit_breaker_in_memory:host: 'http://example.com/', failures_count: '0', self.max_failure_count: '1', is_available: 'True'
>>> circuit_breaker_box.circuit_breaker_in_memory:Added host: http://example.com/, errors: 1
>>> circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'http://example.com/', errors: 2
>>> circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'http://example.com/', errors: 3
>>> circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'http://example.com/', errors: 4
>>> circuit_breaker_box.circuit_breaker_in_memory:host: 'http://example.com/', failures_count: '4', self.max_failure_count: '1', is_available: 'False'
>>> circuit_breaker_box.circuit_breaker_in_memory:host: 'http://example.com/', failures_count: '0', self.max_failure_count: '1', is_available: 'True'
Retrier
import asyncio
import logging
import httpx
import tenacity
from circuit_breaker_box.retrier import Retrier
MAX_RETRIES = 4
SOME_HOST = "http://example.com/"
async def main() -> None:
"""
Use Retrier with tenacity adjustments to automatically retry failed operations raising specific exceptions like:
stop_rule
retry_cause
wait_strategy
`foo` as example function will be retried immediately (no wait) when it raises ZeroDivisionError up to MAX_RETRIES
After exceeding MAX_RETRIES attempts, the exception will propagate.
"""
logging.basicConfig(level=logging.DEBUG)
retryer = Retrier[httpx.Response](
stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),
retry_cause=tenacity.retry_if_exception_type(ZeroDivisionError),
wait_strategy=tenacity.wait_none(),
)
example_request = httpx.Request("GET", httpx.URL(SOME_HOST))
async def foo(request: httpx.Request) -> httpx.Response:
raise ZeroDivisionError(request)
await retryer.retry(foo, request=example_request)
if __name__ == "__main__":
asyncio.run(main())
>> > INFO: circuit_breaker_box.retryer:Attempt: attempt_number: 1, outcome_timestamp: None
>> > INFO: circuit_breaker_box.retryer:Attempt: attempt_number: 2, outcome_timestamp: None
>> > INFO: circuit_breaker_box.retryer:Attempt: attempt_number: 3, outcome_timestamp: None
>> > INFO: circuit_breaker_box.retryer:Attempt: attempt_number: 4, outcome_timestamp: None
>> > Traceback(most
recent
call
last):
>> > ...
>> > ZeroDivisionError: < Request('GET', 'http://example.com/') >
Retrier with CircuitBreaker
import asyncio
import logging
import typing
import fastapi
import httpx
import tenacity
from circuit_breaker_box import CircuitBreakerInMemory, Retrier
MAX_RETRIES = 4
MAX_CACHE_SIZE = 256
CIRCUIT_BREAKER_MAX_FAILURE_COUNT = 1
RESET_TIMEOUT_IN_SECONDS = 10
SOME_HOST = "http://example.com/"
class CustomCircuitBreakerInMemory(CircuitBreakerInMemory):
async def raise_host_unavailable_error(self, host: str) -> typing.NoReturn:
raise fastapi.HTTPException(status_code=500, detail=f"Host: {host} is unavailable")
async def main() -> None:
"""Use Retrier with CustomCircuitBreakerInMemory or CircuitBreakerRedis.
coordinated retry/circuit breaking logic,
also you can redefine raise_host_unavailable_error to raise some custom error in your application.
"""
logging.basicConfig(level=logging.DEBUG)
circuit_breaker = CustomCircuitBreakerInMemory(
reset_timeout_in_seconds=RESET_TIMEOUT_IN_SECONDS,
max_failure_count=CIRCUIT_BREAKER_MAX_FAILURE_COUNT,
max_cache_size=MAX_CACHE_SIZE,
)
retryer = Retrier[httpx.Response](
circuit_breaker=circuit_breaker,
wait_strategy=tenacity.wait_exponential_jitter(),
retry_cause=tenacity.retry_if_exception_type((ZeroDivisionError, httpx.RequestError)),
stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),
)
example_request = httpx.Request("GET", httpx.URL("http://example.com"))
async def foo(request: httpx.Request) -> httpx.Response: # noqa: ARG001
raise ZeroDivisionError
# will raise exception from circuit_breaker.raise_host_unavailable_error
await retryer.retry(foo, example_request.url.host, example_request)
if __name__ == "__main__":
asyncio.run(main())
>>> INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 1, outcome_timestamp: None
>>> DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '0', self.max_failure_count: '1', is_available: 'True'
>>> INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 2, outcome_timestamp: None
>>> DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '0', self.max_failure_count: '1', is_available: 'True'
>>> DEBUG:circuit_breaker_box.circuit_breaker_in_memory:Added host: example.com, errors: 1
>>> INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 3, outcome_timestamp: None
>>> DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '1', self.max_failure_count: '1', is_available: 'True'
>>> DEBUG:circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'example.com', errors: 2
>>> INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 4, outcome_timestamp: None
>>> DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '2', self.max_failure_count: '1', is_available: 'False'
>>> Traceback (most recent call last):
>>> ...
>>> fastapi.exceptions.HTTPException: 500: Host: example.com is unavailable
See -> Examples
Development
Commands
Use -> Justfile
Metadata
Release files for circuit-breaker-box 0.3.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| circuit_breaker_box-0.3.9.tar.gz | 5.6 kB | Without provenance | Details for circuit_breaker_box-0.3.9.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| circuit_breaker_box-0.3.9-py3-none-any.whl | Python 3 | none | any | Details for circuit_breaker_box-0.3.9-py3-none-any.whl |
Total release size: 13.6 kB
Release files / circuit_breaker_box-0.3.9.tar.gz
| Download URL | circuit_breaker_box-0.3.9.tar.gz |
|---|---|
| Size | 5.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cb2560128af773b38e1853afbd06376271d9c2a22fb17399cdd789bedeeceab7
|
|
BLAKE2b-256 checksum How to use checksums |
ad12a05c2dfbd7157d5867c6d283d47c1b83478cde9a6d5bd28e8c5227edb067
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / circuit_breaker_box-0.3.9-py3-none-any.whl
| Download URL | circuit_breaker_box-0.3.9-py3-none-any.whl |
|---|---|
| Size | 8.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
781bd8fddaa3f3352f38a8d375fb0eed6bd5d49dfae4fe095b8c799186f66226
|
|
BLAKE2b-256 checksum How to use checksums |
98d70d2e04d128709ea66289187c0a69bca391c0796446c8f3415fe1d53d6fa5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for circuit-breaker-box 0.3.9
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.