sturdytools 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Meenal Asesh
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,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: sturdytools
3
+ Version: 0.1.0
4
+ Summary: Retries, timeouts, rate limits and circuit breakers for AI agent tools
5
+ Author: Meenal Asesh
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ai-with-md/sturdytools
8
+ Project-URL: Issues, https://github.com/ai-with-md/sturdytools/issues
9
+ Keywords: ai agents,llm,tool calling,function calling,mcp,retry,timeout,circuit breaker,rate limit,resilience
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # sturdytools
23
+
24
+ Retries, timeouts, rate limits and circuit breakers for the tools an AI agent calls.
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/sturdytools)](https://pypi.org/project/sturdytools/)
27
+ [![tests](https://github.com/ai-with-md/sturdytools/actions/workflows/tests.yml/badge.svg)](https://github.com/ai-with-md/sturdytools/actions/workflows/tests.yml)
28
+ ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
29
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
30
+
31
+ An agent is only as reliable as its tools. A search API that drops one request in fifty, a database that hangs, or a service that hits its rate limit can end an agent run, or put a raw stack trace in front of the user. Most of these failures are temporary, and the rest can be handled if the model gets a clear message it can act on.
32
+
33
+ sturdytools is one decorator that handles this for any Python function, sync or async, with no dependencies.
34
+
35
+ ```python
36
+ from sturdytools import sturdy
37
+
38
+ @sturdy(timeout=10, retries=3, rate_limit="5/s", on_error="message")
39
+ def search_orders(customer_id: str) -> list:
40
+ """Find recent orders for a customer."""
41
+ return orders_api.search(customer_id)
42
+ ```
43
+
44
+ If the API drops a connection, the call is retried with backoff. If it hangs, the attempt is abandoned after 10 seconds. If it still fails after 3 retries, the model gets a short message instead of an exception:
45
+
46
+ ```
47
+ Error: the tool 'search_orders' failed after 4 attempts (ConnectionError: connection reset by peer).
48
+ Check the arguments, try a different approach, or tell the user it is unavailable.
49
+ ```
50
+
51
+ [![Demo: a plain tool crashes, the wrapped tool retries, times out, and stops calling a service that is down](https://raw.githubusercontent.com/ai-with-md/sturdytools/main/docs/demo.png)](https://github.com/ai-with-md/sturdytools/blob/main/examples/flaky_tools_demo.py)
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install sturdytools
57
+ ```
58
+
59
+ Python 3.9 or newer. No other packages are needed.
60
+
61
+ ## What it does
62
+
63
+ | Option | What it does | Default |
64
+ |---|---|---|
65
+ | `timeout` | Seconds before one attempt is abandoned with `ToolTimeout` | none |
66
+ | `retries` | Extra attempts after the first one fails | 0 |
67
+ | `retry_on` | Errors that count as temporary. Only these are retried | `Exception` |
68
+ | `backoff`, `max_backoff` | Wait before the first retry, doubling each time with jitter, up to the cap | 0.5s, 8s |
69
+ | `rate_limit` | `"5/s"`, `"60/min"`, `"1000/hour"`, or a shared `RateLimiter` | none |
70
+ | `breaker` | A `CircuitBreaker`, or `True` for one per tool | none |
71
+ | `fallback` | Value to return when the tool gives up. If callable, it gets the tool's arguments | none |
72
+ | `on_error` | `"raise"`, `"message"`, or your own function `(name, error, attempts)` | `"raise"` |
73
+ | `on_retry` | Called as `(attempt, error, delay)` before each retry, for logging or metrics | none |
74
+
75
+ Each call goes through the same steps:
76
+
77
+ 1. If the circuit breaker is open, stop right away.
78
+ 2. Wait for the rate limiter.
79
+ 3. Run the tool with the time limit.
80
+ 4. If it raised a temporary error and retries are left, wait and go back to step 1.
81
+ 5. Otherwise use the fallback, return a message, or raise, depending on the settings.
82
+
83
+ The wrapper keeps the original function name, docstring and signature, which is what agent frameworks read to build the tool description for the model. Put `@sturdy` closest to the function, under your framework's tool decorator.
84
+
85
+ ## Works with MCP servers and LangChain
86
+
87
+ The wrapper keeps the function's name, docstring and signature, so frameworks build the same tool description as before. Put `@sturdy` under the framework's decorator, closest to the function:
88
+
89
+ ```python
90
+ from mcp.server.mcpserver import MCPServer
91
+ from sturdytools import sturdy
92
+
93
+ server = MCPServer("weather")
94
+
95
+ @server.tool()
96
+ @sturdy(timeout=10, retries=2, on_error="message")
97
+ async def get_weather(city: str) -> str:
98
+ """Current weather for a city."""
99
+ ...
100
+ ```
101
+
102
+ ```python
103
+ from langchain_core.tools import tool
104
+ from sturdytools import sturdy
105
+
106
+ @tool
107
+ @sturdy(timeout=10, retries=2, rate_limit="5/s", on_error="message")
108
+ def search_orders(customer_id: str) -> list:
109
+ """Find recent orders for a customer."""
110
+ ...
111
+ ```
112
+
113
+ Checked with mcp 2.3 and langchain-core 1.8, for sync and async tools: the tool schema and description are unchanged, retries happen inside the tool call, and a tool that gives up returns the error message to the model as its result.
114
+
115
+ ## Sharing limits and breakers across tools
116
+
117
+ Several tools often call the same API. Give them one rate limiter and one breaker, so they respect the same limit and stop together when the service is down.
118
+
119
+ ```python
120
+ from sturdytools import CircuitBreaker, RateLimiter, sturdy
121
+
122
+ crm_limit = RateLimiter.parse("10/s")
123
+ crm_breaker = CircuitBreaker(failure_threshold=5, reset_after=30)
124
+
125
+ @sturdy(retries=2, rate_limit=crm_limit, breaker=crm_breaker, on_error="message")
126
+ def get_account(account_id: str) -> dict:
127
+ ...
128
+
129
+ @sturdy(retries=2, rate_limit=crm_limit, breaker=crm_breaker, on_error="message")
130
+ def list_contacts(account_id: str) -> list:
131
+ ...
132
+ ```
133
+
134
+ After 5 temporary failures in a row, both tools stop calling the service for 30 seconds and tell the model it is paused. Then one trial call goes through. If it works, normal calls resume.
135
+
136
+ ## Async tools
137
+
138
+ The same decorator works on `async def` functions. Timeouts cancel the task, and the rate limiter and retries wait without blocking the event loop.
139
+
140
+ ```python
141
+ @sturdy(timeout=5, retries=2, fallback=lambda city: "weather unavailable")
142
+ async def get_weather(city: str) -> str:
143
+ async with httpx.AsyncClient() as client:
144
+ response = await client.get(WEATHER_URL, params={"q": city})
145
+ response.raise_for_status()
146
+ return response.text
147
+ ```
148
+
149
+ ## Choosing what to retry
150
+
151
+ Retrying everything is rarely right. If the model passed a bad argument, retrying the same call wastes time and money. Limit retries to errors that are actually temporary:
152
+
153
+ ```python
154
+ @sturdy(retries=3, retry_on=(ConnectionError, TimeoutError), on_error="message")
155
+ def lookup_invoice(invoice_id: str) -> dict:
156
+ ...
157
+ ```
158
+
159
+ Here a `KeyError` for an unknown invoice goes straight back to the model as a message, so it can correct itself, and it does not count against the circuit breaker.
160
+
161
+ ## Try it
162
+
163
+ ```bash
164
+ python examples/flaky_tools_demo.py
165
+ ```
166
+
167
+ The [demo](https://github.com/ai-with-md/sturdytools/blob/main/examples/flaky_tools_demo.py) uses a fake weather service that drops connections, hangs, and then goes down, and prints what a plain tool and a wrapped tool do in each case. No API keys are needed.
168
+
169
+ ## How it compares
170
+
171
+ - **[tenacity](https://github.com/jd/tenacity)** and **[stamina](https://github.com/hynek/stamina)** are excellent general retry libraries. If retries are all you need, use one of them.
172
+ - sturdytools is narrower and aimed at agent tools: timeouts, retries, rate limits, a circuit breaker and a fallback in one decorator, plus an error message written for the model, so a failing tool becomes something the agent can reason about instead of a crash.
173
+ - Agent frameworks have their own retry options (LangChain's `with_retry`, retry policies on LangGraph nodes). Those retry a whole step. sturdytools works on the single tool function, so it behaves the same in any framework, or in none.
174
+
175
+ ## Limits
176
+
177
+ - Python cannot stop a running thread. When a sync tool times out, the caller gets `ToolTimeout` right away, but the original call keeps running in a background thread until it finishes on its own. For tools that can hang for a long time, prefer async, where the task is cancelled, or set a timeout in the client library as well.
178
+ - Rate limits and breakers live in memory, per process. Several workers calling the same API each keep their own count.
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ python -m venv .venv
184
+ source .venv/bin/activate
185
+ pip install -e ".[dev]"
186
+ pytest
187
+ ```
188
+
189
+ Tests run on Python 3.10, 3.12 and 3.13 on every push.
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,172 @@
1
+ # sturdytools
2
+
3
+ Retries, timeouts, rate limits and circuit breakers for the tools an AI agent calls.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/sturdytools)](https://pypi.org/project/sturdytools/)
6
+ [![tests](https://github.com/ai-with-md/sturdytools/actions/workflows/tests.yml/badge.svg)](https://github.com/ai-with-md/sturdytools/actions/workflows/tests.yml)
7
+ ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
8
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
9
+
10
+ An agent is only as reliable as its tools. A search API that drops one request in fifty, a database that hangs, or a service that hits its rate limit can end an agent run, or put a raw stack trace in front of the user. Most of these failures are temporary, and the rest can be handled if the model gets a clear message it can act on.
11
+
12
+ sturdytools is one decorator that handles this for any Python function, sync or async, with no dependencies.
13
+
14
+ ```python
15
+ from sturdytools import sturdy
16
+
17
+ @sturdy(timeout=10, retries=3, rate_limit="5/s", on_error="message")
18
+ def search_orders(customer_id: str) -> list:
19
+ """Find recent orders for a customer."""
20
+ return orders_api.search(customer_id)
21
+ ```
22
+
23
+ If the API drops a connection, the call is retried with backoff. If it hangs, the attempt is abandoned after 10 seconds. If it still fails after 3 retries, the model gets a short message instead of an exception:
24
+
25
+ ```
26
+ Error: the tool 'search_orders' failed after 4 attempts (ConnectionError: connection reset by peer).
27
+ Check the arguments, try a different approach, or tell the user it is unavailable.
28
+ ```
29
+
30
+ [![Demo: a plain tool crashes, the wrapped tool retries, times out, and stops calling a service that is down](https://raw.githubusercontent.com/ai-with-md/sturdytools/main/docs/demo.png)](https://github.com/ai-with-md/sturdytools/blob/main/examples/flaky_tools_demo.py)
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install sturdytools
36
+ ```
37
+
38
+ Python 3.9 or newer. No other packages are needed.
39
+
40
+ ## What it does
41
+
42
+ | Option | What it does | Default |
43
+ |---|---|---|
44
+ | `timeout` | Seconds before one attempt is abandoned with `ToolTimeout` | none |
45
+ | `retries` | Extra attempts after the first one fails | 0 |
46
+ | `retry_on` | Errors that count as temporary. Only these are retried | `Exception` |
47
+ | `backoff`, `max_backoff` | Wait before the first retry, doubling each time with jitter, up to the cap | 0.5s, 8s |
48
+ | `rate_limit` | `"5/s"`, `"60/min"`, `"1000/hour"`, or a shared `RateLimiter` | none |
49
+ | `breaker` | A `CircuitBreaker`, or `True` for one per tool | none |
50
+ | `fallback` | Value to return when the tool gives up. If callable, it gets the tool's arguments | none |
51
+ | `on_error` | `"raise"`, `"message"`, or your own function `(name, error, attempts)` | `"raise"` |
52
+ | `on_retry` | Called as `(attempt, error, delay)` before each retry, for logging or metrics | none |
53
+
54
+ Each call goes through the same steps:
55
+
56
+ 1. If the circuit breaker is open, stop right away.
57
+ 2. Wait for the rate limiter.
58
+ 3. Run the tool with the time limit.
59
+ 4. If it raised a temporary error and retries are left, wait and go back to step 1.
60
+ 5. Otherwise use the fallback, return a message, or raise, depending on the settings.
61
+
62
+ The wrapper keeps the original function name, docstring and signature, which is what agent frameworks read to build the tool description for the model. Put `@sturdy` closest to the function, under your framework's tool decorator.
63
+
64
+ ## Works with MCP servers and LangChain
65
+
66
+ The wrapper keeps the function's name, docstring and signature, so frameworks build the same tool description as before. Put `@sturdy` under the framework's decorator, closest to the function:
67
+
68
+ ```python
69
+ from mcp.server.mcpserver import MCPServer
70
+ from sturdytools import sturdy
71
+
72
+ server = MCPServer("weather")
73
+
74
+ @server.tool()
75
+ @sturdy(timeout=10, retries=2, on_error="message")
76
+ async def get_weather(city: str) -> str:
77
+ """Current weather for a city."""
78
+ ...
79
+ ```
80
+
81
+ ```python
82
+ from langchain_core.tools import tool
83
+ from sturdytools import sturdy
84
+
85
+ @tool
86
+ @sturdy(timeout=10, retries=2, rate_limit="5/s", on_error="message")
87
+ def search_orders(customer_id: str) -> list:
88
+ """Find recent orders for a customer."""
89
+ ...
90
+ ```
91
+
92
+ Checked with mcp 2.3 and langchain-core 1.8, for sync and async tools: the tool schema and description are unchanged, retries happen inside the tool call, and a tool that gives up returns the error message to the model as its result.
93
+
94
+ ## Sharing limits and breakers across tools
95
+
96
+ Several tools often call the same API. Give them one rate limiter and one breaker, so they respect the same limit and stop together when the service is down.
97
+
98
+ ```python
99
+ from sturdytools import CircuitBreaker, RateLimiter, sturdy
100
+
101
+ crm_limit = RateLimiter.parse("10/s")
102
+ crm_breaker = CircuitBreaker(failure_threshold=5, reset_after=30)
103
+
104
+ @sturdy(retries=2, rate_limit=crm_limit, breaker=crm_breaker, on_error="message")
105
+ def get_account(account_id: str) -> dict:
106
+ ...
107
+
108
+ @sturdy(retries=2, rate_limit=crm_limit, breaker=crm_breaker, on_error="message")
109
+ def list_contacts(account_id: str) -> list:
110
+ ...
111
+ ```
112
+
113
+ After 5 temporary failures in a row, both tools stop calling the service for 30 seconds and tell the model it is paused. Then one trial call goes through. If it works, normal calls resume.
114
+
115
+ ## Async tools
116
+
117
+ The same decorator works on `async def` functions. Timeouts cancel the task, and the rate limiter and retries wait without blocking the event loop.
118
+
119
+ ```python
120
+ @sturdy(timeout=5, retries=2, fallback=lambda city: "weather unavailable")
121
+ async def get_weather(city: str) -> str:
122
+ async with httpx.AsyncClient() as client:
123
+ response = await client.get(WEATHER_URL, params={"q": city})
124
+ response.raise_for_status()
125
+ return response.text
126
+ ```
127
+
128
+ ## Choosing what to retry
129
+
130
+ Retrying everything is rarely right. If the model passed a bad argument, retrying the same call wastes time and money. Limit retries to errors that are actually temporary:
131
+
132
+ ```python
133
+ @sturdy(retries=3, retry_on=(ConnectionError, TimeoutError), on_error="message")
134
+ def lookup_invoice(invoice_id: str) -> dict:
135
+ ...
136
+ ```
137
+
138
+ Here a `KeyError` for an unknown invoice goes straight back to the model as a message, so it can correct itself, and it does not count against the circuit breaker.
139
+
140
+ ## Try it
141
+
142
+ ```bash
143
+ python examples/flaky_tools_demo.py
144
+ ```
145
+
146
+ The [demo](https://github.com/ai-with-md/sturdytools/blob/main/examples/flaky_tools_demo.py) uses a fake weather service that drops connections, hangs, and then goes down, and prints what a plain tool and a wrapped tool do in each case. No API keys are needed.
147
+
148
+ ## How it compares
149
+
150
+ - **[tenacity](https://github.com/jd/tenacity)** and **[stamina](https://github.com/hynek/stamina)** are excellent general retry libraries. If retries are all you need, use one of them.
151
+ - sturdytools is narrower and aimed at agent tools: timeouts, retries, rate limits, a circuit breaker and a fallback in one decorator, plus an error message written for the model, so a failing tool becomes something the agent can reason about instead of a crash.
152
+ - Agent frameworks have their own retry options (LangChain's `with_retry`, retry policies on LangGraph nodes). Those retry a whole step. sturdytools works on the single tool function, so it behaves the same in any framework, or in none.
153
+
154
+ ## Limits
155
+
156
+ - Python cannot stop a running thread. When a sync tool times out, the caller gets `ToolTimeout` right away, but the original call keeps running in a background thread until it finishes on its own. For tools that can hang for a long time, prefer async, where the task is cancelled, or set a timeout in the client library as well.
157
+ - Rate limits and breakers live in memory, per process. Several workers calling the same API each keep their own count.
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ python -m venv .venv
163
+ source .venv/bin/activate
164
+ pip install -e ".[dev]"
165
+ pytest
166
+ ```
167
+
168
+ Tests run on Python 3.10, 3.12 and 3.13 on every push.
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sturdytools"
7
+ version = "0.1.0"
8
+ description = "Retries, timeouts, rate limits and circuit breakers for AI agent tools"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{name = "Meenal Asesh"}]
12
+ requires-python = ">=3.9"
13
+ dependencies = []
14
+ keywords = ["ai agents", "llm", "tool calling", "function calling", "mcp", "retry", "timeout", "circuit breaker", "rate limit", "resilience"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Framework :: AsyncIO",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/ai-with-md/sturdytools"
28
+ Issues = "https://github.com/ai-with-md/sturdytools/issues"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """Retries, timeouts, rate limits and circuit breakers for AI agent tools."""
2
+
3
+ from .breaker import CircuitBreaker
4
+ from .core import error_message, sturdy
5
+ from .errors import CircuitOpenError, ToolTimeout
6
+ from .ratelimit import RateLimiter
7
+
8
+ __all__ = [
9
+ "sturdy",
10
+ "CircuitBreaker",
11
+ "RateLimiter",
12
+ "ToolTimeout",
13
+ "CircuitOpenError",
14
+ "error_message",
15
+ ]
16
+
17
+ __version__ = "0.1.0"
@@ -0,0 +1,88 @@
1
+ """A small circuit breaker.
2
+
3
+ After ``failure_threshold`` failures in a row the breaker opens and calls are
4
+ rejected right away, without touching the failing service. Once
5
+ ``reset_after`` seconds have passed, a single trial call is let through. If it
6
+ works the breaker closes again; if it fails the breaker stays open for another
7
+ ``reset_after`` seconds.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ import time
14
+ from typing import Callable
15
+
16
+ from .errors import CircuitOpenError
17
+
18
+ CLOSED = "closed"
19
+ OPEN = "open"
20
+ HALF_OPEN = "half_open"
21
+
22
+
23
+ class CircuitBreaker:
24
+ def __init__(
25
+ self,
26
+ failure_threshold: int = 5,
27
+ reset_after: float = 30.0,
28
+ clock: Callable[[], float] = time.monotonic,
29
+ ):
30
+ if failure_threshold < 1:
31
+ raise ValueError("failure_threshold must be at least 1")
32
+ if reset_after <= 0:
33
+ raise ValueError("reset_after must be positive")
34
+ self.failure_threshold = failure_threshold
35
+ self.reset_after = reset_after
36
+ self._clock = clock
37
+ self._lock = threading.Lock()
38
+ self._state = CLOSED
39
+ self._failures = 0
40
+ self._opened_at = 0.0
41
+ self._trial_running = False
42
+
43
+ @property
44
+ def state(self) -> str:
45
+ return self._state
46
+
47
+ def before_call(self) -> None:
48
+ """Raise CircuitOpenError if the call should not go through."""
49
+ with self._lock:
50
+ if self._state == CLOSED:
51
+ return
52
+ waited = self._clock() - self._opened_at
53
+ if self._state == OPEN and waited >= self.reset_after:
54
+ self._state = HALF_OPEN
55
+ if self._state == HALF_OPEN and not self._trial_running:
56
+ self._trial_running = True
57
+ return
58
+ raise CircuitOpenError(retry_in=max(0.0, self.reset_after - waited))
59
+
60
+ def record_success(self) -> None:
61
+ with self._lock:
62
+ self._state = CLOSED
63
+ self._failures = 0
64
+ self._trial_running = False
65
+
66
+ def record_failure(self) -> None:
67
+ with self._lock:
68
+ self._trial_running = False
69
+ if self._state == HALF_OPEN:
70
+ self._open()
71
+ return
72
+ self._failures += 1
73
+ if self._failures >= self.failure_threshold:
74
+ self._open()
75
+
76
+ def reset(self) -> None:
77
+ """Close the breaker by hand, for example after fixing the service."""
78
+ self.record_success()
79
+
80
+ def _open(self) -> None:
81
+ self._state = OPEN
82
+ self._opened_at = self._clock()
83
+
84
+ def __repr__(self) -> str:
85
+ return (
86
+ f"CircuitBreaker(state={self._state!r}, failures={self._failures}, "
87
+ f"failure_threshold={self.failure_threshold}, reset_after={self.reset_after:g})"
88
+ )