proxilion 0.0.1__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 (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,428 @@
1
+ """
2
+ Circuit breaker implementation for Proxilion.
3
+
4
+ This module provides the circuit breaker pattern to prevent
5
+ cascading failures when external services or tools fail.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import threading
12
+ import time
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ from typing import Any, TypeVar
17
+
18
+ from proxilion.exceptions import CircuitOpenError
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ class CircuitState(Enum):
26
+ """Circuit breaker states."""
27
+ CLOSED = "closed" # Normal operation, requests pass through
28
+ OPEN = "open" # Failing, requests rejected
29
+ HALF_OPEN = "half_open" # Testing if service recovered
30
+
31
+
32
+ @dataclass
33
+ class CircuitStats:
34
+ """Statistics for a circuit breaker."""
35
+ failures: int = 0
36
+ successes: int = 0
37
+ consecutive_failures: int = 0
38
+ consecutive_successes: int = 0
39
+ last_failure_time: float | None = None
40
+ last_success_time: float | None = None
41
+ last_failure_error: str | None = None
42
+ state_change_time: float = field(default_factory=time.monotonic)
43
+
44
+
45
+ class CircuitBreaker:
46
+ """
47
+ Circuit breaker for protecting against cascading failures.
48
+
49
+ The circuit breaker has three states:
50
+ - CLOSED: Normal operation. Requests pass through and failures are tracked.
51
+ - OPEN: The circuit is tripped. Requests are rejected immediately.
52
+ - HALF_OPEN: Testing recovery. Limited requests are allowed through.
53
+
54
+ State Transitions:
55
+ - CLOSED -> OPEN: When failures exceed the threshold.
56
+ - OPEN -> HALF_OPEN: After the reset timeout expires.
57
+ - HALF_OPEN -> CLOSED: When a request succeeds.
58
+ - HALF_OPEN -> OPEN: When a request fails.
59
+
60
+ Thread Safety:
61
+ All operations are thread-safe.
62
+
63
+ Example:
64
+ >>> breaker = CircuitBreaker(
65
+ ... failure_threshold=5,
66
+ ... reset_timeout=30.0,
67
+ ... )
68
+ >>>
69
+ >>> try:
70
+ ... result = breaker.call(external_api_call, arg1, arg2)
71
+ ... except CircuitOpenError:
72
+ ... # Circuit is open, use fallback
73
+ ... result = fallback_response()
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ failure_threshold: int = 5,
79
+ reset_timeout: float = 30.0,
80
+ half_open_max: int = 1,
81
+ success_threshold: int = 1,
82
+ excluded_exceptions: tuple[type[Exception], ...] | None = None,
83
+ exponential_backoff: bool = True,
84
+ max_backoff: float = 300.0,
85
+ ) -> None:
86
+ """
87
+ Initialize the circuit breaker.
88
+
89
+ Args:
90
+ failure_threshold: Number of failures before opening circuit.
91
+ reset_timeout: Seconds to wait before trying half-open.
92
+ half_open_max: Max concurrent requests in half-open state.
93
+ success_threshold: Successes needed to close circuit from half-open.
94
+ excluded_exceptions: Exceptions that don't count as failures.
95
+ exponential_backoff: If True, increase timeout on repeated failures.
96
+ max_backoff: Maximum backoff timeout in seconds.
97
+ """
98
+ self.failure_threshold = failure_threshold
99
+ self.reset_timeout = reset_timeout
100
+ self.half_open_max = half_open_max
101
+ self.success_threshold = success_threshold
102
+ self.excluded_exceptions = excluded_exceptions or ()
103
+ self.exponential_backoff = exponential_backoff
104
+ self.max_backoff = max_backoff
105
+
106
+ self._state = CircuitState.CLOSED
107
+ self._stats = CircuitStats()
108
+ self._lock = threading.RLock()
109
+ self._half_open_count = 0
110
+ self._backoff_multiplier = 1.0
111
+
112
+ @property
113
+ def state(self) -> CircuitState:
114
+ """Get current circuit state."""
115
+ with self._lock:
116
+ self._maybe_transition_to_half_open()
117
+ return self._state
118
+
119
+ @property
120
+ def stats(self) -> CircuitStats:
121
+ """Get circuit statistics."""
122
+ with self._lock:
123
+ return CircuitStats(
124
+ failures=self._stats.failures,
125
+ successes=self._stats.successes,
126
+ consecutive_failures=self._stats.consecutive_failures,
127
+ consecutive_successes=self._stats.consecutive_successes,
128
+ last_failure_time=self._stats.last_failure_time,
129
+ last_success_time=self._stats.last_success_time,
130
+ last_failure_error=self._stats.last_failure_error,
131
+ state_change_time=self._stats.state_change_time,
132
+ )
133
+
134
+ def _maybe_transition_to_half_open(self) -> None:
135
+ """Check if we should transition from OPEN to HALF_OPEN."""
136
+ if self._state != CircuitState.OPEN:
137
+ return
138
+
139
+ current_timeout = self.reset_timeout * self._backoff_multiplier
140
+ elapsed = time.monotonic() - self._stats.state_change_time
141
+
142
+ if elapsed >= current_timeout:
143
+ logger.info(
144
+ f"Circuit transitioning from OPEN to HALF_OPEN "
145
+ f"after {elapsed:.1f}s"
146
+ )
147
+ self._state = CircuitState.HALF_OPEN
148
+ self._stats.state_change_time = time.monotonic()
149
+ self._half_open_count = 0
150
+
151
+ def _set_state(self, new_state: CircuitState) -> None:
152
+ """Set circuit state and log the transition."""
153
+ old_state = self._state
154
+ if old_state != new_state:
155
+ self._state = new_state
156
+ self._stats.state_change_time = time.monotonic()
157
+ logger.info(f"Circuit state: {old_state.value} -> {new_state.value}")
158
+
159
+ def _record_success(self) -> None:
160
+ """Record a successful call."""
161
+ self._stats.successes += 1
162
+ self._stats.consecutive_successes += 1
163
+ self._stats.consecutive_failures = 0
164
+ self._stats.last_success_time = time.monotonic()
165
+
166
+ if self._state == CircuitState.HALF_OPEN:
167
+ if self._stats.consecutive_successes >= self.success_threshold:
168
+ self._set_state(CircuitState.CLOSED)
169
+ self._backoff_multiplier = 1.0 # Reset backoff
170
+ self._stats.consecutive_successes = 0
171
+
172
+ def _record_failure(self, error: Exception) -> None:
173
+ """Record a failed call."""
174
+ self._stats.failures += 1
175
+ self._stats.consecutive_failures += 1
176
+ self._stats.consecutive_successes = 0
177
+ self._stats.last_failure_time = time.monotonic()
178
+ self._stats.last_failure_error = str(error)
179
+
180
+ if self._state == CircuitState.HALF_OPEN:
181
+ # Any failure in half-open opens the circuit again
182
+ self._set_state(CircuitState.OPEN)
183
+ if self.exponential_backoff:
184
+ self._backoff_multiplier = min(
185
+ self._backoff_multiplier * 2,
186
+ self.max_backoff / self.reset_timeout,
187
+ )
188
+
189
+ elif self._state == CircuitState.CLOSED:
190
+ if self._stats.consecutive_failures >= self.failure_threshold:
191
+ self._set_state(CircuitState.OPEN)
192
+
193
+ def call(
194
+ self,
195
+ func: Callable[..., T],
196
+ *args: Any,
197
+ **kwargs: Any,
198
+ ) -> T:
199
+ """
200
+ Execute a function through the circuit breaker.
201
+
202
+ Args:
203
+ func: The function to call.
204
+ *args: Positional arguments for the function.
205
+ **kwargs: Keyword arguments for the function.
206
+
207
+ Returns:
208
+ The function's return value.
209
+
210
+ Raises:
211
+ CircuitOpenError: If the circuit is open.
212
+ Exception: Any exception raised by the function.
213
+
214
+ Example:
215
+ >>> result = breaker.call(api.get_data, user_id=123)
216
+ """
217
+ with self._lock:
218
+ self._maybe_transition_to_half_open()
219
+ state = self._state
220
+
221
+ if state == CircuitState.OPEN:
222
+ current_timeout = self.reset_timeout * self._backoff_multiplier
223
+ elapsed = time.monotonic() - self._stats.state_change_time
224
+ remaining = current_timeout - elapsed
225
+
226
+ raise CircuitOpenError(
227
+ circuit_name=getattr(func, "__name__", "unknown"),
228
+ failure_count=self._stats.consecutive_failures,
229
+ reset_timeout=remaining,
230
+ last_failure=self._stats.last_failure_error,
231
+ )
232
+
233
+ if state == CircuitState.HALF_OPEN:
234
+ if self._half_open_count >= self.half_open_max:
235
+ raise CircuitOpenError(
236
+ circuit_name=getattr(func, "__name__", "unknown"),
237
+ failure_count=self._stats.consecutive_failures,
238
+ reset_timeout=0.0,
239
+ last_failure="Half-open limit reached",
240
+ )
241
+ self._half_open_count += 1
242
+
243
+ # Execute outside lock to avoid blocking
244
+ try:
245
+ result = func(*args, **kwargs)
246
+ with self._lock:
247
+ self._record_success()
248
+ return result
249
+
250
+ except self.excluded_exceptions:
251
+ # Don't count as failure, but re-raise
252
+ raise
253
+
254
+ except Exception as e:
255
+ with self._lock:
256
+ self._record_failure(e)
257
+ raise
258
+
259
+ async def call_async(
260
+ self,
261
+ func: Callable[..., Any],
262
+ *args: Any,
263
+ **kwargs: Any,
264
+ ) -> Any:
265
+ """
266
+ Execute an async function through the circuit breaker.
267
+
268
+ Args:
269
+ func: The async function to call.
270
+ *args: Positional arguments.
271
+ **kwargs: Keyword arguments.
272
+
273
+ Returns:
274
+ The function's return value.
275
+
276
+ Raises:
277
+ CircuitOpenError: If the circuit is open.
278
+ """
279
+ with self._lock:
280
+ self._maybe_transition_to_half_open()
281
+ state = self._state
282
+
283
+ if state == CircuitState.OPEN:
284
+ current_timeout = self.reset_timeout * self._backoff_multiplier
285
+ elapsed = time.monotonic() - self._stats.state_change_time
286
+ remaining = current_timeout - elapsed
287
+
288
+ raise CircuitOpenError(
289
+ circuit_name=getattr(func, "__name__", "unknown"),
290
+ failure_count=self._stats.consecutive_failures,
291
+ reset_timeout=remaining,
292
+ last_failure=self._stats.last_failure_error,
293
+ )
294
+
295
+ if state == CircuitState.HALF_OPEN:
296
+ if self._half_open_count >= self.half_open_max:
297
+ raise CircuitOpenError(
298
+ circuit_name=getattr(func, "__name__", "unknown"),
299
+ failure_count=self._stats.consecutive_failures,
300
+ reset_timeout=0.0,
301
+ last_failure="Half-open limit reached",
302
+ )
303
+ self._half_open_count += 1
304
+
305
+ try:
306
+ result = await func(*args, **kwargs)
307
+ with self._lock:
308
+ self._record_success()
309
+ return result
310
+
311
+ except self.excluded_exceptions:
312
+ raise
313
+
314
+ except Exception as e:
315
+ with self._lock:
316
+ self._record_failure(e)
317
+ raise
318
+
319
+ def reset(self) -> None:
320
+ """Reset the circuit breaker to closed state."""
321
+ with self._lock:
322
+ self._set_state(CircuitState.CLOSED)
323
+ self._stats = CircuitStats()
324
+ self._half_open_count = 0
325
+ self._backoff_multiplier = 1.0
326
+
327
+ def force_open(self) -> None:
328
+ """Force the circuit to open state (for testing/maintenance)."""
329
+ with self._lock:
330
+ self._set_state(CircuitState.OPEN)
331
+
332
+ def is_available(self) -> bool:
333
+ """Check if the circuit will accept requests."""
334
+ return self.state != CircuitState.OPEN
335
+
336
+
337
+ class CircuitBreakerRegistry:
338
+ """
339
+ Registry for managing multiple circuit breakers.
340
+
341
+ Provides a central place to manage circuit breakers for
342
+ different tools or services.
343
+
344
+ Example:
345
+ >>> registry = CircuitBreakerRegistry()
346
+ >>> registry.register("external_api", CircuitBreaker(failure_threshold=3))
347
+ >>>
348
+ >>> breaker = registry.get("external_api")
349
+ >>> result = breaker.call(api_call)
350
+ """
351
+
352
+ def __init__(
353
+ self,
354
+ default_config: dict[str, Any] | None = None,
355
+ ) -> None:
356
+ """
357
+ Initialize the registry.
358
+
359
+ Args:
360
+ default_config: Default configuration for auto-created breakers.
361
+ """
362
+ self._breakers: dict[str, CircuitBreaker] = {}
363
+ self._lock = threading.RLock()
364
+ self.default_config = default_config or {
365
+ "failure_threshold": 5,
366
+ "reset_timeout": 30.0,
367
+ }
368
+
369
+ def register(
370
+ self,
371
+ name: str,
372
+ breaker: CircuitBreaker | None = None,
373
+ ) -> CircuitBreaker:
374
+ """
375
+ Register a circuit breaker.
376
+
377
+ Args:
378
+ name: Name for the circuit breaker.
379
+ breaker: CircuitBreaker instance, or None to create with defaults.
380
+
381
+ Returns:
382
+ The registered circuit breaker.
383
+ """
384
+ with self._lock:
385
+ if breaker is None:
386
+ breaker = CircuitBreaker(**self.default_config)
387
+ self._breakers[name] = breaker
388
+ return breaker
389
+
390
+ def get(self, name: str, auto_create: bool = True) -> CircuitBreaker:
391
+ """
392
+ Get a circuit breaker by name.
393
+
394
+ Args:
395
+ name: The circuit breaker name.
396
+ auto_create: If True, create a new breaker if not found.
397
+
398
+ Returns:
399
+ The circuit breaker.
400
+
401
+ Raises:
402
+ KeyError: If not found and auto_create is False.
403
+ """
404
+ with self._lock:
405
+ if name not in self._breakers:
406
+ if auto_create:
407
+ return self.register(name)
408
+ raise KeyError(f"Circuit breaker '{name}' not found")
409
+ return self._breakers[name]
410
+
411
+ def get_all_stats(self) -> dict[str, dict[str, Any]]:
412
+ """Get statistics for all circuit breakers."""
413
+ with self._lock:
414
+ return {
415
+ name: {
416
+ "state": breaker.state.value,
417
+ "failures": breaker.stats.failures,
418
+ "successes": breaker.stats.successes,
419
+ "consecutive_failures": breaker.stats.consecutive_failures,
420
+ }
421
+ for name, breaker in self._breakers.items()
422
+ }
423
+
424
+ def reset_all(self) -> None:
425
+ """Reset all circuit breakers."""
426
+ with self._lock:
427
+ for breaker in self._breakers.values():
428
+ breaker.reset()