code-puppy 0.0.135__py3-none-any.whl → 0.0.136__py3-none-any.whl

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 (60) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/main.py +23 -17
  28. code_puppy/mcp/__init__.py +42 -16
  29. code_puppy/mcp/async_lifecycle.py +51 -49
  30. code_puppy/mcp/blocking_startup.py +125 -113
  31. code_puppy/mcp/captured_stdio_server.py +63 -70
  32. code_puppy/mcp/circuit_breaker.py +63 -47
  33. code_puppy/mcp/config_wizard.py +169 -136
  34. code_puppy/mcp/dashboard.py +79 -71
  35. code_puppy/mcp/error_isolation.py +147 -100
  36. code_puppy/mcp/examples/retry_example.py +55 -42
  37. code_puppy/mcp/health_monitor.py +152 -141
  38. code_puppy/mcp/managed_server.py +100 -93
  39. code_puppy/mcp/manager.py +168 -156
  40. code_puppy/mcp/registry.py +148 -110
  41. code_puppy/mcp/retry_manager.py +63 -61
  42. code_puppy/mcp/server_registry_catalog.py +271 -225
  43. code_puppy/mcp/status_tracker.py +80 -80
  44. code_puppy/mcp/system_tools.py +47 -52
  45. code_puppy/messaging/message_queue.py +20 -13
  46. code_puppy/messaging/renderers.py +30 -15
  47. code_puppy/state_management.py +103 -0
  48. code_puppy/tui/app.py +64 -7
  49. code_puppy/tui/components/chat_view.py +3 -3
  50. code_puppy/tui/components/human_input_modal.py +12 -8
  51. code_puppy/tui/screens/__init__.py +2 -2
  52. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  53. code_puppy/tui/tests/test_agent_command.py +3 -3
  54. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
  56. code_puppy/command_line/mcp_commands.py +0 -1789
  57. {code_puppy-0.0.135.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -6,20 +6,15 @@ stderr output and makes it available through proper logging channels.
6
6
  """
7
7
 
8
8
  import asyncio
9
- import io
10
9
  import logging
11
10
  import os
12
- import sys
13
- import tempfile
14
11
  from contextlib import asynccontextmanager
15
- from typing import AsyncIterator, Sequence, Optional, Any
16
- from threading import Thread
17
- from queue import Queue, Empty
12
+ from typing import AsyncIterator, Optional, Sequence
18
13
 
19
- from pydantic_ai.mcp import MCPServerStdio
20
- from mcp.client.stdio import StdioServerParameters, stdio_client
21
14
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
15
+ from mcp.client.stdio import StdioServerParameters, stdio_client
22
16
  from mcp.shared.session import SessionMessage
17
+ from pydantic_ai.mcp import MCPServerStdio
23
18
 
24
19
  logger = logging.getLogger(__name__)
25
20
 
@@ -28,11 +23,11 @@ class StderrCapture:
28
23
  """
29
24
  Captures stderr output using a pipe and background reader.
30
25
  """
31
-
26
+
32
27
  def __init__(self, name: str, handler: Optional[callable] = None):
33
28
  """
34
29
  Initialize stderr capture.
35
-
30
+
36
31
  Args:
37
32
  name: Name for this capture stream
38
33
  handler: Optional function to call with captured lines
@@ -43,75 +38,75 @@ class StderrCapture:
43
38
  self._reader_task = None
44
39
  self._pipe_r = None
45
40
  self._pipe_w = None
46
-
41
+
47
42
  def _default_handler(self, line: str):
48
43
  """Default handler that logs to Python logging."""
49
44
  if line.strip():
50
45
  logger.debug(f"[MCP {self.name}] {line.rstrip()}")
51
-
46
+
52
47
  async def start_capture(self):
53
48
  """Start capturing stderr by creating a pipe and reader task."""
54
49
  # Create a pipe for capturing stderr
55
50
  self._pipe_r, self._pipe_w = os.pipe()
56
-
51
+
57
52
  # Make the read end non-blocking
58
53
  os.set_blocking(self._pipe_r, False)
59
-
54
+
60
55
  # Start background task to read from pipe
61
56
  self._reader_task = asyncio.create_task(self._read_pipe())
62
-
57
+
63
58
  # Return the write end as the file descriptor for stderr
64
59
  return self._pipe_w
65
-
60
+
66
61
  async def _read_pipe(self):
67
62
  """Background task to read from the pipe."""
68
63
  loop = asyncio.get_event_loop()
69
- buffer = b''
70
-
64
+ buffer = b""
65
+
71
66
  try:
72
67
  while True:
73
68
  # Use asyncio's add_reader for efficient async reading
74
69
  future = asyncio.Future()
75
-
70
+
76
71
  def read_callback():
77
72
  try:
78
73
  data = os.read(self._pipe_r, 4096)
79
74
  future.set_result(data)
80
75
  except BlockingIOError:
81
- future.set_result(b'')
76
+ future.set_result(b"")
82
77
  except Exception as e:
83
78
  future.set_exception(e)
84
-
79
+
85
80
  loop.add_reader(self._pipe_r, read_callback)
86
81
  try:
87
82
  data = await future
88
83
  finally:
89
84
  loop.remove_reader(self._pipe_r)
90
-
85
+
91
86
  if not data:
92
87
  await asyncio.sleep(0.1)
93
88
  continue
94
-
89
+
95
90
  # Process the data
96
91
  buffer += data
97
-
92
+
98
93
  # Look for complete lines
99
- while b'\n' in buffer:
100
- line, buffer = buffer.split(b'\n', 1)
101
- line_str = line.decode('utf-8', errors='replace')
94
+ while b"\n" in buffer:
95
+ line, buffer = buffer.split(b"\n", 1)
96
+ line_str = line.decode("utf-8", errors="replace")
102
97
  if line_str:
103
98
  self._captured_lines.append(line_str)
104
99
  self.handler(line_str)
105
-
100
+
106
101
  except asyncio.CancelledError:
107
102
  # Process any remaining buffer
108
103
  if buffer:
109
- line_str = buffer.decode('utf-8', errors='replace')
104
+ line_str = buffer.decode("utf-8", errors="replace")
110
105
  if line_str:
111
106
  self._captured_lines.append(line_str)
112
107
  self.handler(line_str)
113
108
  raise
114
-
109
+
115
110
  async def stop_capture(self):
116
111
  """Stop capturing and clean up."""
117
112
  if self._reader_task:
@@ -120,12 +115,12 @@ class StderrCapture:
120
115
  await self._reader_task
121
116
  except asyncio.CancelledError:
122
117
  pass
123
-
118
+
124
119
  if self._pipe_r is not None:
125
120
  os.close(self._pipe_r)
126
121
  if self._pipe_w is not None:
127
122
  os.close(self._pipe_w)
128
-
123
+
129
124
  def get_captured_lines(self) -> list[str]:
130
125
  """Get all captured lines."""
131
126
  return self._captured_lines.copy()
@@ -134,11 +129,11 @@ class StderrCapture:
134
129
  class CapturedMCPServerStdio(MCPServerStdio):
135
130
  """
136
131
  Extended MCPServerStdio that captures and handles stderr output.
137
-
132
+
138
133
  This class captures stderr from the subprocess and makes it available
139
134
  through proper logging channels instead of letting it pollute the console.
140
135
  """
141
-
136
+
142
137
  def __init__(
143
138
  self,
144
139
  command: str,
@@ -146,11 +141,11 @@ class CapturedMCPServerStdio(MCPServerStdio):
146
141
  env: dict[str, str] | None = None,
147
142
  cwd: str | None = None,
148
143
  stderr_handler: Optional[callable] = None,
149
- **kwargs
144
+ **kwargs,
150
145
  ):
151
146
  """
152
147
  Initialize captured stdio server.
153
-
148
+
154
149
  Args:
155
150
  command: The command to run
156
151
  args: Arguments for the command
@@ -163,7 +158,7 @@ class CapturedMCPServerStdio(MCPServerStdio):
163
158
  self.stderr_handler = stderr_handler
164
159
  self._stderr_capture = None
165
160
  self._captured_lines = []
166
-
161
+
167
162
  @asynccontextmanager
168
163
  async def client_streams(
169
164
  self,
@@ -175,40 +170,40 @@ class CapturedMCPServerStdio(MCPServerStdio):
175
170
  ]:
176
171
  """Create the streams for the MCP server with stderr capture."""
177
172
  server = StdioServerParameters(
178
- command=self.command,
179
- args=list(self.args),
180
- env=self.env,
181
- cwd=self.cwd
173
+ command=self.command, args=list(self.args), env=self.env, cwd=self.cwd
182
174
  )
183
-
175
+
184
176
  # Create stderr capture
185
177
  def stderr_line_handler(line: str):
186
178
  """Handle captured stderr lines."""
187
179
  self._captured_lines.append(line)
188
-
180
+
189
181
  if self.stderr_handler:
190
182
  self.stderr_handler(line)
191
183
  else:
192
184
  # Default: log at DEBUG level to avoid console spam
193
185
  logger.debug(f"[MCP Server {self.command}] {line}")
194
-
186
+
195
187
  self._stderr_capture = StderrCapture(self.command, stderr_line_handler)
196
-
188
+
197
189
  # For now, use devnull for stderr to suppress output
198
190
  # We'll capture it through other means if needed
199
- with open(os.devnull, 'w') as devnull:
200
- async with stdio_client(server=server, errlog=devnull) as (read_stream, write_stream):
191
+ with open(os.devnull, "w") as devnull:
192
+ async with stdio_client(server=server, errlog=devnull) as (
193
+ read_stream,
194
+ write_stream,
195
+ ):
201
196
  yield read_stream, write_stream
202
-
197
+
203
198
  def get_captured_stderr(self) -> list[str]:
204
199
  """
205
200
  Get all captured stderr lines.
206
-
201
+
207
202
  Returns:
208
203
  List of captured stderr lines
209
204
  """
210
205
  return self._captured_lines.copy()
211
-
206
+
212
207
  def clear_captured_stderr(self):
213
208
  """Clear the captured stderr buffer."""
214
209
  self._captured_lines.clear()
@@ -217,56 +212,55 @@ class CapturedMCPServerStdio(MCPServerStdio):
217
212
  class StderrCollector:
218
213
  """
219
214
  A centralized collector for stderr from multiple MCP servers.
220
-
215
+
221
216
  This can be used to aggregate stderr from all MCP servers in one place.
222
217
  """
223
-
218
+
224
219
  def __init__(self):
225
220
  """Initialize the collector."""
226
221
  self.servers = {}
227
222
  self.all_lines = []
228
-
223
+
229
224
  def create_handler(self, server_name: str, emit_to_user: bool = False):
230
225
  """
231
226
  Create a handler function for a specific server.
232
-
227
+
233
228
  Args:
234
229
  server_name: Name to identify this server
235
230
  emit_to_user: If True, emit stderr lines to user via emit_info
236
-
231
+
237
232
  Returns:
238
233
  Handler function that can be passed to CapturedMCPServerStdio
239
234
  """
235
+
240
236
  def handler(line: str):
241
237
  # Store with server identification
242
238
  import time
243
- entry = {
244
- 'server': server_name,
245
- 'line': line,
246
- 'timestamp': time.time()
247
- }
248
-
239
+
240
+ entry = {"server": server_name, "line": line, "timestamp": time.time()}
241
+
249
242
  if server_name not in self.servers:
250
243
  self.servers[server_name] = []
251
-
244
+
252
245
  self.servers[server_name].append(line)
253
246
  self.all_lines.append(entry)
254
-
247
+
255
248
  # Emit to user if requested
256
249
  if emit_to_user:
257
250
  from code_puppy.messaging import emit_info
251
+
258
252
  emit_info(f"[MCP {server_name}] {line}", style="dim cyan")
259
-
253
+
260
254
  return handler
261
-
255
+
262
256
  def get_server_output(self, server_name: str) -> list[str]:
263
257
  """Get all output from a specific server."""
264
258
  return self.servers.get(server_name, []).copy()
265
-
259
+
266
260
  def get_all_output(self) -> list[dict]:
267
261
  """Get all output from all servers with metadata."""
268
262
  return self.all_lines.copy()
269
-
263
+
270
264
  def clear(self, server_name: Optional[str] = None):
271
265
  """Clear captured output."""
272
266
  if server_name:
@@ -274,9 +268,8 @@ class StderrCollector:
274
268
  self.servers[server_name].clear()
275
269
  # Also clear from all_lines
276
270
  self.all_lines = [
277
- entry for entry in self.all_lines
278
- if entry['server'] != server_name
271
+ entry for entry in self.all_lines if entry["server"] != server_name
279
272
  ]
280
273
  else:
281
274
  self.servers.clear()
282
- self.all_lines.clear()
275
+ self.all_lines.clear()
@@ -9,129 +9,137 @@ failures when MCP servers become unhealthy. The circuit breaker has three states
9
9
  """
10
10
 
11
11
  import asyncio
12
+ import logging
12
13
  import time
13
14
  from enum import Enum
14
15
  from typing import Any, Callable
15
- import logging
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
20
  class CircuitState(Enum):
21
21
  """Circuit breaker states."""
22
- CLOSED = "closed" # Normal operation
23
- OPEN = "open" # Blocking calls
22
+
23
+ CLOSED = "closed" # Normal operation
24
+ OPEN = "open" # Blocking calls
24
25
  HALF_OPEN = "half_open" # Testing recovery
25
26
 
26
27
 
27
28
  class CircuitOpenError(Exception):
28
29
  """Raised when circuit breaker is in OPEN state."""
30
+
29
31
  pass
30
32
 
31
33
 
32
34
  class CircuitBreaker:
33
35
  """
34
36
  Circuit breaker to prevent cascading failures in MCP servers.
35
-
37
+
36
38
  The circuit breaker monitors the success/failure rate of operations and
37
39
  transitions between states to protect the system from unhealthy dependencies.
38
-
40
+
39
41
  States:
40
42
  - CLOSED: Normal operation, all calls allowed
41
43
  - OPEN: Circuit is open, all calls fail fast with CircuitOpenError
42
44
  - HALF_OPEN: Testing recovery, limited calls allowed
43
-
45
+
44
46
  State Transitions:
45
47
  - CLOSED → OPEN: After failure_threshold consecutive failures
46
48
  - OPEN → HALF_OPEN: After timeout seconds
47
49
  - HALF_OPEN → CLOSED: After success_threshold consecutive successes
48
50
  - HALF_OPEN → OPEN: After any failure
49
51
  """
50
-
51
- def __init__(self, failure_threshold: int = 5, success_threshold: int = 2, timeout: int = 60):
52
+
53
+ def __init__(
54
+ self, failure_threshold: int = 5, success_threshold: int = 2, timeout: int = 60
55
+ ):
52
56
  """
53
57
  Initialize circuit breaker.
54
-
58
+
55
59
  Args:
56
60
  failure_threshold: Number of consecutive failures before opening circuit
57
61
  success_threshold: Number of consecutive successes needed to close circuit from half-open
58
62
  timeout: Seconds to wait before transitioning from OPEN to HALF_OPEN
59
63
  """
60
64
  self.failure_threshold = failure_threshold
61
- self.success_threshold = success_threshold
65
+ self.success_threshold = success_threshold
62
66
  self.timeout = timeout
63
-
67
+
64
68
  self._state = CircuitState.CLOSED
65
69
  self._failure_count = 0
66
70
  self._success_count = 0
67
71
  self._last_failure_time = None
68
72
  self._lock = asyncio.Lock()
69
-
73
+
70
74
  logger.info(
71
75
  f"Circuit breaker initialized: failure_threshold={failure_threshold}, "
72
76
  f"success_threshold={success_threshold}, timeout={timeout}s"
73
77
  )
74
-
78
+
75
79
  async def call(self, func: Callable, *args, **kwargs) -> Any:
76
80
  """
77
81
  Execute a function through the circuit breaker.
78
-
82
+
79
83
  Args:
80
84
  func: Function to execute
81
85
  *args: Positional arguments for the function
82
86
  **kwargs: Keyword arguments for the function
83
-
87
+
84
88
  Returns:
85
89
  Result of the function call
86
-
90
+
87
91
  Raises:
88
92
  CircuitOpenError: If circuit is in OPEN state
89
93
  Exception: Any exception raised by the wrapped function
90
94
  """
91
95
  async with self._lock:
92
96
  current_state = self._get_current_state()
93
-
97
+
94
98
  if current_state == CircuitState.OPEN:
95
99
  logger.warning("Circuit breaker is OPEN, failing fast")
96
100
  raise CircuitOpenError("Circuit breaker is open")
97
-
101
+
98
102
  if current_state == CircuitState.HALF_OPEN:
99
103
  # In half-open state, we're testing recovery
100
104
  logger.info("Circuit breaker is HALF_OPEN, allowing test call")
101
-
105
+
102
106
  # Execute the function outside the lock to avoid blocking other calls
103
107
  try:
104
- result = await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
108
+ result = (
109
+ await func(*args, **kwargs)
110
+ if asyncio.iscoroutinefunction(func)
111
+ else func(*args, **kwargs)
112
+ )
105
113
  await self._on_success()
106
114
  return result
107
115
  except Exception as e:
108
116
  await self._on_failure()
109
117
  raise e
110
-
118
+
111
119
  def record_success(self) -> None:
112
120
  """Record a successful operation."""
113
121
  asyncio.create_task(self._on_success())
114
-
122
+
115
123
  def record_failure(self) -> None:
116
124
  """Record a failed operation."""
117
125
  asyncio.create_task(self._on_failure())
118
-
126
+
119
127
  def get_state(self) -> CircuitState:
120
128
  """Get current circuit breaker state."""
121
129
  return self._get_current_state()
122
-
130
+
123
131
  def is_open(self) -> bool:
124
132
  """Check if circuit breaker is in OPEN state."""
125
133
  return self._get_current_state() == CircuitState.OPEN
126
-
134
+
127
135
  def is_half_open(self) -> bool:
128
136
  """Check if circuit breaker is in HALF_OPEN state."""
129
137
  return self._get_current_state() == CircuitState.HALF_OPEN
130
-
138
+
131
139
  def is_closed(self) -> bool:
132
140
  """Check if circuit breaker is in CLOSED state."""
133
141
  return self._get_current_state() == CircuitState.CLOSED
134
-
142
+
135
143
  def reset(self) -> None:
136
144
  """Reset circuit breaker to CLOSED state and clear counters."""
137
145
  logger.info("Resetting circuit breaker to CLOSED state")
@@ -139,13 +147,13 @@ class CircuitBreaker:
139
147
  self._failure_count = 0
140
148
  self._success_count = 0
141
149
  self._last_failure_time = None
142
-
150
+
143
151
  def force_open(self) -> None:
144
152
  """Force circuit breaker to OPEN state."""
145
153
  logger.warning("Forcing circuit breaker to OPEN state")
146
154
  self._state = CircuitState.OPEN
147
155
  self._last_failure_time = time.time()
148
-
156
+
149
157
  def force_close(self) -> None:
150
158
  """Force circuit breaker to CLOSED state and reset counters."""
151
159
  logger.info("Forcing circuit breaker to CLOSED state")
@@ -153,11 +161,11 @@ class CircuitBreaker:
153
161
  self._failure_count = 0
154
162
  self._success_count = 0
155
163
  self._last_failure_time = None
156
-
164
+
157
165
  def _get_current_state(self) -> CircuitState:
158
166
  """
159
167
  Get the current state, handling automatic transitions.
160
-
168
+
161
169
  This method handles the automatic transition from OPEN to HALF_OPEN
162
170
  after the timeout period has elapsed.
163
171
  """
@@ -165,54 +173,62 @@ class CircuitBreaker:
165
173
  logger.info("Timeout reached, transitioning from OPEN to HALF_OPEN")
166
174
  self._state = CircuitState.HALF_OPEN
167
175
  self._success_count = 0 # Reset success counter for half-open testing
168
-
176
+
169
177
  return self._state
170
-
178
+
171
179
  def _should_attempt_reset(self) -> bool:
172
180
  """Check if enough time has passed to attempt reset from OPEN to HALF_OPEN."""
173
181
  if self._last_failure_time is None:
174
182
  return False
175
-
183
+
176
184
  return time.time() - self._last_failure_time >= self.timeout
177
-
185
+
178
186
  async def _on_success(self) -> None:
179
187
  """Handle successful operation."""
180
188
  async with self._lock:
181
189
  current_state = self._get_current_state()
182
-
190
+
183
191
  if current_state == CircuitState.CLOSED:
184
192
  # Reset failure count on success in closed state
185
193
  if self._failure_count > 0:
186
194
  logger.debug("Resetting failure count after success")
187
195
  self._failure_count = 0
188
-
196
+
189
197
  elif current_state == CircuitState.HALF_OPEN:
190
198
  self._success_count += 1
191
- logger.debug(f"Success in HALF_OPEN state: {self._success_count}/{self.success_threshold}")
192
-
199
+ logger.debug(
200
+ f"Success in HALF_OPEN state: {self._success_count}/{self.success_threshold}"
201
+ )
202
+
193
203
  if self._success_count >= self.success_threshold:
194
- logger.info("Success threshold reached, transitioning from HALF_OPEN to CLOSED")
204
+ logger.info(
205
+ "Success threshold reached, transitioning from HALF_OPEN to CLOSED"
206
+ )
195
207
  self._state = CircuitState.CLOSED
196
208
  self._failure_count = 0
197
209
  self._success_count = 0
198
210
  self._last_failure_time = None
199
-
211
+
200
212
  async def _on_failure(self) -> None:
201
213
  """Handle failed operation."""
202
214
  async with self._lock:
203
215
  current_state = self._get_current_state()
204
-
216
+
205
217
  if current_state == CircuitState.CLOSED:
206
218
  self._failure_count += 1
207
- logger.debug(f"Failure in CLOSED state: {self._failure_count}/{self.failure_threshold}")
208
-
219
+ logger.debug(
220
+ f"Failure in CLOSED state: {self._failure_count}/{self.failure_threshold}"
221
+ )
222
+
209
223
  if self._failure_count >= self.failure_threshold:
210
- logger.warning("Failure threshold reached, transitioning from CLOSED to OPEN")
224
+ logger.warning(
225
+ "Failure threshold reached, transitioning from CLOSED to OPEN"
226
+ )
211
227
  self._state = CircuitState.OPEN
212
228
  self._last_failure_time = time.time()
213
-
229
+
214
230
  elif current_state == CircuitState.HALF_OPEN:
215
231
  logger.warning("Failure in HALF_OPEN state, transitioning back to OPEN")
216
232
  self._state = CircuitState.OPEN
217
233
  self._success_count = 0
218
- self._last_failure_time = time.time()
234
+ self._last_failure_time = time.time()