{"id":25615948,"url":"https://github.com/community-of-python/circuit-breaker-box","last_synced_at":"2025-04-13T21:40:38.834Z","repository":{"id":277387461,"uuid":"932201714","full_name":"community-of-python/circuit-breaker-box","owner":"community-of-python","description":"python implementation of the circuit breaker pattern","archived":false,"fork":false,"pushed_at":"2025-04-07T12:48:38.000Z","size":20,"stargazers_count":14,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-07T13:38:14.248Z","etag":null,"topics":["circuit-breaker"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/community-of-python.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-13T14:36:35.000Z","updated_at":"2025-04-07T12:44:52.000Z","dependencies_parsed_at":"2025-04-07T13:42:37.131Z","dependency_job_id":null,"html_url":"https://github.com/community-of-python/circuit-breaker-box","commit_stats":null,"previous_names":["nikitakozlovtcev/circuit-breaker","community-of-python/circuit-breaker-box"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/community-of-python%2Fcircuit-breaker-box","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/community-of-python%2Fcircuit-breaker-box/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/community-of-python%2Fcircuit-breaker-box/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/community-of-python%2Fcircuit-breaker-box/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/community-of-python","download_url":"https://codeload.github.com/community-of-python/circuit-breaker-box/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248788859,"owners_count":21161726,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["circuit-breaker"],"created_at":"2025-02-22T03:32:15.366Z","updated_at":"2025-04-13T21:40:38.827Z","avatar_url":"https://github.com/community-of-python.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Python Circuit Breaker Box\n\nA Python implementation of the Circuit Breaker pattern.\n\n## Features\n\n- 🚀 Implementations:\n  - **Redis-based**\n  - **In-memory**\n- [![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge\u0026logo=python\u0026logoColor=FFD43B)](https://python.org) 3.10-3.13 support.\n- ⚡ Asynchronous API\n- 🔧 Configurable parameters\n- 🔄 Retries by [tenacity](https://tenacity.readthedocs.io/en/latest/)\n- 🛠️ FastAPI integration through custom exceptions\n\n## Installation\n```bash\npip install circuit-breaker-box\n```\n\n## Usage\n### Direct usage\n```python\nimport asyncio\nimport logging\n\nfrom circuit_breaker_box import CircuitBreakerInMemory\n\n\nMAX_RETRIES = 4\nMAX_CACHE_SIZE = 256\nCIRCUIT_BREAKER_MAX_FAILURE_COUNT = 1\nRESET_TIMEOUT_IN_SECONDS = 10\nSOME_HOST = \"http://example.com/\"\n\n\nasync def main() -\u003e None:\n    \"\"\"Define CircuitBreakerInMemory or CircuitBreakerRedis and use in your application directly\"\"\"\n    logging.basicConfig(level=logging.DEBUG)\n    circuit_breaker = CircuitBreakerInMemory(\n        reset_timeout_in_seconds=RESET_TIMEOUT_IN_SECONDS,\n        max_failure_count=CIRCUIT_BREAKER_MAX_FAILURE_COUNT,\n        max_cache_size=MAX_CACHE_SIZE,\n    )\n    # circuit_breaker is open state for SOME_HOST\n    assert await circuit_breaker.is_host_available(host=SOME_HOST)\n\n    for _ in range(MAX_RETRIES):\n        # circuit_breaker is half-open for SOME_HOST\n        await circuit_breaker.increment_failures_count(host=SOME_HOST)\n\n    # after failure count more then CIRCUIT_BREAKER_MAX_FAILURE_COUNT value circuit_breaker is closed\n    assert await circuit_breaker.is_host_available(host=SOME_HOST) is False\n\n    # close state reset to open state after RESET_TIMEOUT_IN_SECONDS delay\n    await asyncio.sleep(RESET_TIMEOUT_IN_SECONDS)\n    assert await circuit_breaker.is_host_available(host=SOME_HOST) is True\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n\n\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:host: 'http://example.com/', failures_count: '0', self.max_failure_count: '1', is_available: 'True'\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:Added host: http://example.com/, errors: 1\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'http://example.com/', errors: 2\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'http://example.com/', errors: 3\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'http://example.com/', errors: 4\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:host: 'http://example.com/', failures_count: '4', self.max_failure_count: '1', is_available: 'False'\n\u003e\u003e\u003e circuit_breaker_box.circuit_breaker_in_memory:host: 'http://example.com/', failures_count: '0', self.max_failure_count: '1', is_available: 'True'\n```\n\n### Retrier\n```python\nimport asyncio\nimport logging\n\nimport httpx\nimport tenacity\n\nfrom circuit_breaker_box.retryer import Retrier\n\n\nMAX_RETRIES = 4\nSOME_HOST = \"http://example.com/\"\n\n\nasync def main() -\u003e None:\n    \"\"\"\n    Use Retrier with tenacity adjustments to automatically retry failed operations raising specific exceptions like:\n     stop_rule\n     retry_cause\n     wait_strategy\n\n    `foo` as example function will be retried immediately (no wait) when it raises ZeroDivisionError up to MAX_RETRIES\n        After exceeding MAX_RETRIES attempts, the exception will propagate.\n    \"\"\"\n    logging.basicConfig(level=logging.DEBUG)\n    retryer = Retrier[httpx.Response](\n        stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),\n        retry_cause=tenacity.retry_if_exception_type(ZeroDivisionError),\n        wait_strategy=tenacity.wait_none(),\n    )\n    example_request = httpx.Request(\"GET\", httpx.URL(SOME_HOST))\n\n    async def foo(request: httpx.Request) -\u003e httpx.Response:\n        raise ZeroDivisionError(request)\n\n    await retryer.retry(foo, request=example_request)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n\n\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 1, outcome_timestamp: None\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 2, outcome_timestamp: None\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 3, outcome_timestamp: None\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 4, outcome_timestamp: None\n\u003e\u003e\u003e Traceback (most recent call last):\n\u003e\u003e\u003e     ...\n\u003e\u003e\u003e ZeroDivisionError: \u003cRequest('GET', 'http://example.com/')\u003e\n```\n\n### Retrier with CircuitBreaker\n```python\nimport asyncio\nimport logging\nimport typing\n\nimport fastapi\nimport httpx\nimport tenacity\n\nfrom circuit_breaker_box import CircuitBreakerInMemory, Retrier\n\n\nMAX_RETRIES = 4\nMAX_CACHE_SIZE = 256\nCIRCUIT_BREAKER_MAX_FAILURE_COUNT = 1\nRESET_TIMEOUT_IN_SECONDS = 10\nSOME_HOST = \"http://example.com/\"\n\n\nclass CustomCircuitBreakerInMemory(CircuitBreakerInMemory):\n    async def raise_host_unavailable_error(self, host: str) -\u003e typing.NoReturn:\n        raise fastapi.HTTPException(status_code=500, detail=f\"Host: {host} is unavailable\")\n\n\nasync def main() -\u003e None:\n    \"\"\"Use Retrier with CustomCircuitBreakerInMemory or CircuitBreakerRedis.\n\n    coordinated retry/circuit breaking logic,\n    also you can redefine raise_host_unavailable_error to raise some custom error in your application.\n    \"\"\"\n    logging.basicConfig(level=logging.DEBUG)\n    circuit_breaker = CustomCircuitBreakerInMemory(\n        reset_timeout_in_seconds=RESET_TIMEOUT_IN_SECONDS,\n        max_failure_count=CIRCUIT_BREAKER_MAX_FAILURE_COUNT,\n        max_cache_size=MAX_CACHE_SIZE,\n    )\n    retryer = Retrier[httpx.Response](\n        circuit_breaker=circuit_breaker,\n        wait_strategy=tenacity.wait_exponential_jitter(),\n        retry_cause=tenacity.retry_if_exception_type((ZeroDivisionError, httpx.RequestError)),\n        stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),\n    )\n    example_request = httpx.Request(\"GET\", httpx.URL(\"http://example.com\"))\n\n    async def foo(request: httpx.Request) -\u003e httpx.Response:  # noqa: ARG001\n        raise ZeroDivisionError\n\n    # will raise exception from circuit_breaker.raise_host_unavailable_error\n    await retryer.retry(foo, example_request.url.host, example_request)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 1, outcome_timestamp: None\n\u003e\u003e\u003e DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '0', self.max_failure_count: '1', is_available: 'True'\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 2, outcome_timestamp: None\n\u003e\u003e\u003e DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '0', self.max_failure_count: '1', is_available: 'True'\n\u003e\u003e\u003e DEBUG:circuit_breaker_box.circuit_breaker_in_memory:Added host: example.com, errors: 1\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 3, outcome_timestamp: None\n\u003e\u003e\u003e DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '1', self.max_failure_count: '1', is_available: 'True'\n\u003e\u003e\u003e DEBUG:circuit_breaker_box.circuit_breaker_in_memory:Incremented error for host: 'example.com', errors: 2\n\u003e\u003e\u003e INFO:circuit_breaker_box.retryer:Attempt: attempt_number: 4, outcome_timestamp: None\n\u003e\u003e\u003e DEBUG:circuit_breaker_box.circuit_breaker_in_memory:host: 'example.com', failures_count: '2', self.max_failure_count: '1', is_available: 'False'\n\u003e\u003e\u003e Traceback (most recent call last):\n\u003e\u003e\u003e     ...\n\u003e\u003e\u003e fastapi.exceptions.HTTPException: 500: Host: example.com is unavailable\n```\n\nSee -\u003e [Examples](examples/)\n\n## Development\n### Commands\nUse -\u003e [Justfile](Justfile)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcommunity-of-python%2Fcircuit-breaker-box","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcommunity-of-python%2Fcircuit-breaker-box","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcommunity-of-python%2Fcircuit-breaker-box/lists"}