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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Timeout and deadline management for AI agent operations.
|
|
3
|
+
|
|
4
|
+
Provides configurable timeout handling for tool calls, LLM operations,
|
|
5
|
+
and overall request budgets. Essential for WebSocket/real-time applications
|
|
6
|
+
where responsiveness is critical.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from proxilion.timeouts import (
|
|
10
|
+
... TimeoutManager, TimeoutConfig, DeadlineContext,
|
|
11
|
+
... with_timeout, with_deadline,
|
|
12
|
+
... )
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Configure timeouts
|
|
15
|
+
>>> config = TimeoutConfig(
|
|
16
|
+
... default_timeout=30.0,
|
|
17
|
+
... tool_timeouts={"web_search": 60.0, "database_query": 10.0},
|
|
18
|
+
... llm_timeout=120.0,
|
|
19
|
+
... )
|
|
20
|
+
>>> manager = TimeoutManager(config)
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Use deadline context for request budget
|
|
23
|
+
>>> async with DeadlineContext(timeout=60.0) as deadline:
|
|
24
|
+
... result1 = await tool1(timeout=deadline.remaining())
|
|
25
|
+
... result2 = await tool2(timeout=deadline.remaining())
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Use decorator for individual functions
|
|
28
|
+
>>> @with_timeout(10.0)
|
|
29
|
+
... async def quick_operation():
|
|
30
|
+
... return await some_api_call()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from proxilion.timeouts.decorators import (
|
|
34
|
+
with_deadline,
|
|
35
|
+
with_timeout,
|
|
36
|
+
)
|
|
37
|
+
from proxilion.timeouts.manager import (
|
|
38
|
+
DeadlineContext,
|
|
39
|
+
TimeoutConfig,
|
|
40
|
+
TimeoutManager,
|
|
41
|
+
)
|
|
42
|
+
from proxilion.timeouts.manager import (
|
|
43
|
+
TimeoutError as ProxilionTimeoutError,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Manager classes
|
|
48
|
+
"DeadlineContext",
|
|
49
|
+
"TimeoutConfig",
|
|
50
|
+
"TimeoutManager",
|
|
51
|
+
"ProxilionTimeoutError",
|
|
52
|
+
# Decorators
|
|
53
|
+
"with_deadline",
|
|
54
|
+
"with_timeout",
|
|
55
|
+
]
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Timeout decorators for AI agent operations.
|
|
3
|
+
|
|
4
|
+
Provides decorators for applying timeouts to sync and async functions,
|
|
5
|
+
with support for both explicit timeouts and deadline contexts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
import functools
|
|
13
|
+
import inspect
|
|
14
|
+
from collections.abc import Callable, Coroutine
|
|
15
|
+
from typing import Any, ParamSpec, TypeVar
|
|
16
|
+
|
|
17
|
+
from proxilion.timeouts.manager import (
|
|
18
|
+
DeadlineContext,
|
|
19
|
+
TimeoutError,
|
|
20
|
+
TimeoutManager,
|
|
21
|
+
get_current_deadline,
|
|
22
|
+
get_default_manager,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
P = ParamSpec("P")
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def with_timeout(
|
|
30
|
+
timeout: float | None = None,
|
|
31
|
+
timeout_manager: TimeoutManager | None = None,
|
|
32
|
+
operation_name: str | None = None,
|
|
33
|
+
use_deadline: bool = True,
|
|
34
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
35
|
+
"""
|
|
36
|
+
Decorator that applies a timeout to a sync or async function.
|
|
37
|
+
|
|
38
|
+
For async functions, uses asyncio.wait_for.
|
|
39
|
+
For sync functions, uses ThreadPoolExecutor.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
timeout: Explicit timeout in seconds. If None, uses manager's default.
|
|
43
|
+
timeout_manager: TimeoutManager to use for configuration.
|
|
44
|
+
operation_name: Name of operation (defaults to function name).
|
|
45
|
+
use_deadline: If True, respects active deadline context.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Decorator function.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> @with_timeout(10.0)
|
|
52
|
+
... async def slow_api_call():
|
|
53
|
+
... await asyncio.sleep(20) # Will raise TimeoutError
|
|
54
|
+
|
|
55
|
+
>>> @with_timeout(5.0)
|
|
56
|
+
... def sync_operation():
|
|
57
|
+
... time.sleep(10) # Will raise TimeoutError
|
|
58
|
+
"""
|
|
59
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
60
|
+
nonlocal operation_name
|
|
61
|
+
if operation_name is None:
|
|
62
|
+
operation_name = func.__name__
|
|
63
|
+
|
|
64
|
+
manager = timeout_manager or get_default_manager()
|
|
65
|
+
|
|
66
|
+
if inspect.iscoroutinefunction(func):
|
|
67
|
+
@functools.wraps(func)
|
|
68
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
69
|
+
effective_timeout = _get_effective_timeout(
|
|
70
|
+
timeout, manager, operation_name, use_deadline
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if effective_timeout <= 0:
|
|
74
|
+
raise TimeoutError(
|
|
75
|
+
message="Deadline already exceeded",
|
|
76
|
+
operation=operation_name,
|
|
77
|
+
timeout=effective_timeout,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
return await asyncio.wait_for(
|
|
82
|
+
func(*args, **kwargs),
|
|
83
|
+
timeout=effective_timeout,
|
|
84
|
+
)
|
|
85
|
+
except asyncio.TimeoutError as e:
|
|
86
|
+
raise TimeoutError(
|
|
87
|
+
message="Operation timed out",
|
|
88
|
+
operation=operation_name,
|
|
89
|
+
timeout=effective_timeout,
|
|
90
|
+
) from e
|
|
91
|
+
|
|
92
|
+
return async_wrapper # type: ignore
|
|
93
|
+
else:
|
|
94
|
+
@functools.wraps(func)
|
|
95
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
96
|
+
effective_timeout = _get_effective_timeout(
|
|
97
|
+
timeout, manager, operation_name, use_deadline
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if effective_timeout <= 0:
|
|
101
|
+
raise TimeoutError(
|
|
102
|
+
message="Deadline already exceeded",
|
|
103
|
+
operation=operation_name,
|
|
104
|
+
timeout=effective_timeout,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return _run_with_timeout_sync(
|
|
108
|
+
func, args, kwargs, effective_timeout, operation_name
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return sync_wrapper # type: ignore
|
|
112
|
+
|
|
113
|
+
return decorator
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def with_deadline(
|
|
117
|
+
timeout: float,
|
|
118
|
+
operation_name: str | None = None,
|
|
119
|
+
raise_on_expire: bool = True,
|
|
120
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
121
|
+
"""
|
|
122
|
+
Decorator that creates a deadline context for the function.
|
|
123
|
+
|
|
124
|
+
The entire function execution is wrapped in a DeadlineContext,
|
|
125
|
+
allowing nested calls to check remaining time.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
timeout: Total timeout budget for the function.
|
|
129
|
+
operation_name: Name of operation (defaults to function name).
|
|
130
|
+
raise_on_expire: Whether to raise TimeoutError on expiration.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Decorator function.
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> @with_deadline(30.0)
|
|
137
|
+
... async def complex_operation():
|
|
138
|
+
... # Nested calls can use get_current_deadline()
|
|
139
|
+
... deadline = get_current_deadline()
|
|
140
|
+
... await call_api(timeout=deadline.remaining())
|
|
141
|
+
"""
|
|
142
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
143
|
+
nonlocal operation_name
|
|
144
|
+
if operation_name is None:
|
|
145
|
+
operation_name = func.__name__
|
|
146
|
+
|
|
147
|
+
if inspect.iscoroutinefunction(func):
|
|
148
|
+
@functools.wraps(func)
|
|
149
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
150
|
+
async with DeadlineContext(
|
|
151
|
+
timeout=timeout,
|
|
152
|
+
operation=operation_name,
|
|
153
|
+
raise_on_expire=raise_on_expire,
|
|
154
|
+
) as deadline:
|
|
155
|
+
try:
|
|
156
|
+
return await asyncio.wait_for(
|
|
157
|
+
func(*args, **kwargs),
|
|
158
|
+
timeout=deadline.remaining(),
|
|
159
|
+
)
|
|
160
|
+
except asyncio.TimeoutError as e:
|
|
161
|
+
raise TimeoutError(
|
|
162
|
+
message="Deadline exceeded",
|
|
163
|
+
operation=operation_name,
|
|
164
|
+
timeout=timeout,
|
|
165
|
+
elapsed=deadline.elapsed(),
|
|
166
|
+
) from e
|
|
167
|
+
|
|
168
|
+
return async_wrapper # type: ignore
|
|
169
|
+
else:
|
|
170
|
+
@functools.wraps(func)
|
|
171
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
172
|
+
with DeadlineContext(
|
|
173
|
+
timeout=timeout,
|
|
174
|
+
operation=operation_name,
|
|
175
|
+
raise_on_expire=raise_on_expire,
|
|
176
|
+
) as deadline:
|
|
177
|
+
return _run_with_timeout_sync(
|
|
178
|
+
func, args, kwargs, deadline.remaining(), operation_name
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return sync_wrapper # type: ignore
|
|
182
|
+
|
|
183
|
+
return decorator
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_effective_timeout(
|
|
187
|
+
explicit_timeout: float | None,
|
|
188
|
+
manager: TimeoutManager,
|
|
189
|
+
operation_name: str,
|
|
190
|
+
use_deadline: bool,
|
|
191
|
+
) -> float:
|
|
192
|
+
"""
|
|
193
|
+
Get effective timeout considering explicit, configured, and deadline.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
explicit_timeout: Explicitly specified timeout.
|
|
197
|
+
manager: TimeoutManager for configuration.
|
|
198
|
+
operation_name: Name of the operation.
|
|
199
|
+
use_deadline: Whether to consider active deadline.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Effective timeout in seconds.
|
|
203
|
+
"""
|
|
204
|
+
# Start with explicit or configured timeout
|
|
205
|
+
if explicit_timeout is not None:
|
|
206
|
+
timeout = explicit_timeout
|
|
207
|
+
else:
|
|
208
|
+
timeout = manager.get_timeout(operation_name)
|
|
209
|
+
|
|
210
|
+
# Consider active deadline if enabled
|
|
211
|
+
if use_deadline:
|
|
212
|
+
current_deadline = get_current_deadline()
|
|
213
|
+
if current_deadline is not None:
|
|
214
|
+
try:
|
|
215
|
+
remaining = current_deadline.remaining()
|
|
216
|
+
timeout = min(timeout, remaining)
|
|
217
|
+
except TimeoutError:
|
|
218
|
+
return 0.0
|
|
219
|
+
|
|
220
|
+
return timeout
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_with_timeout_sync(
|
|
224
|
+
func: Callable[..., T],
|
|
225
|
+
args: tuple,
|
|
226
|
+
kwargs: dict,
|
|
227
|
+
timeout: float,
|
|
228
|
+
operation_name: str,
|
|
229
|
+
) -> T:
|
|
230
|
+
"""
|
|
231
|
+
Run a synchronous function with a timeout.
|
|
232
|
+
|
|
233
|
+
Uses ThreadPoolExecutor for cross-platform compatibility.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
func: Function to run.
|
|
237
|
+
args: Positional arguments.
|
|
238
|
+
kwargs: Keyword arguments.
|
|
239
|
+
timeout: Timeout in seconds.
|
|
240
|
+
operation_name: Name of operation for error messages.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Function result.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
TimeoutError: If function exceeds timeout.
|
|
247
|
+
"""
|
|
248
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
249
|
+
future = executor.submit(func, *args, **kwargs)
|
|
250
|
+
try:
|
|
251
|
+
return future.result(timeout=timeout)
|
|
252
|
+
except concurrent.futures.TimeoutError as e:
|
|
253
|
+
# Cancel the future (may not actually stop the thread)
|
|
254
|
+
future.cancel()
|
|
255
|
+
raise TimeoutError(
|
|
256
|
+
message="Operation timed out",
|
|
257
|
+
operation=operation_name,
|
|
258
|
+
timeout=timeout,
|
|
259
|
+
) from e
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def run_with_timeout(
|
|
263
|
+
coro: Coroutine[Any, Any, T],
|
|
264
|
+
timeout: float,
|
|
265
|
+
operation_name: str | None = None,
|
|
266
|
+
) -> T:
|
|
267
|
+
"""
|
|
268
|
+
Run a coroutine with a timeout.
|
|
269
|
+
|
|
270
|
+
Convenience function for applying timeout to a coroutine
|
|
271
|
+
without using a decorator.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
coro: Coroutine to run.
|
|
275
|
+
timeout: Timeout in seconds.
|
|
276
|
+
operation_name: Name of operation for error messages.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Coroutine result.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
TimeoutError: If coroutine exceeds timeout.
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
>>> result = await run_with_timeout(
|
|
286
|
+
... fetch_data(),
|
|
287
|
+
... timeout=10.0,
|
|
288
|
+
... operation_name="fetch_data"
|
|
289
|
+
... )
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
return await asyncio.wait_for(coro, timeout=timeout)
|
|
293
|
+
except asyncio.TimeoutError as e:
|
|
294
|
+
raise TimeoutError(
|
|
295
|
+
message="Operation timed out",
|
|
296
|
+
operation=operation_name,
|
|
297
|
+
timeout=timeout,
|
|
298
|
+
) from e
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def run_with_deadline(
|
|
302
|
+
coro: Coroutine[Any, Any, T],
|
|
303
|
+
deadline: DeadlineContext,
|
|
304
|
+
operation_name: str | None = None,
|
|
305
|
+
) -> T:
|
|
306
|
+
"""
|
|
307
|
+
Run a coroutine respecting an existing deadline context.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
coro: Coroutine to run.
|
|
311
|
+
deadline: Active deadline context.
|
|
312
|
+
operation_name: Name of operation for error messages.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Coroutine result.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
TimeoutError: If deadline is exceeded.
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
>>> async with DeadlineContext(30.0) as deadline:
|
|
322
|
+
... result1 = await run_with_deadline(call1(), deadline)
|
|
323
|
+
... result2 = await run_with_deadline(call2(), deadline)
|
|
324
|
+
"""
|
|
325
|
+
remaining = deadline.remaining() # May raise TimeoutError
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
return await asyncio.wait_for(coro, timeout=remaining)
|
|
329
|
+
except asyncio.TimeoutError as e:
|
|
330
|
+
raise TimeoutError(
|
|
331
|
+
message="Deadline exceeded during operation",
|
|
332
|
+
operation=operation_name,
|
|
333
|
+
timeout=deadline.timeout,
|
|
334
|
+
elapsed=deadline.elapsed(),
|
|
335
|
+
) from e
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class TimeoutScope:
|
|
339
|
+
"""
|
|
340
|
+
Structured timeout scope for multiple operations.
|
|
341
|
+
|
|
342
|
+
Similar to DeadlineContext but designed for grouping
|
|
343
|
+
multiple operations with named checkpoints.
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
>>> async with TimeoutScope(30.0) as scope:
|
|
347
|
+
... result1 = await scope.run("fetch", fetch_data())
|
|
348
|
+
... result2 = await scope.run("process", process_data(result1))
|
|
349
|
+
... result3 = await scope.run("save", save_result(result2))
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
timeout: float,
|
|
355
|
+
operation_name: str = "scope",
|
|
356
|
+
) -> None:
|
|
357
|
+
"""
|
|
358
|
+
Initialize timeout scope.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
timeout: Total timeout budget.
|
|
362
|
+
operation_name: Name of the scope.
|
|
363
|
+
"""
|
|
364
|
+
self.timeout = timeout
|
|
365
|
+
self.operation_name = operation_name
|
|
366
|
+
self._deadline: DeadlineContext | None = None
|
|
367
|
+
self._checkpoints: list[tuple[str, float]] = []
|
|
368
|
+
|
|
369
|
+
async def __aenter__(self) -> TimeoutScope:
|
|
370
|
+
"""Enter the scope."""
|
|
371
|
+
self._deadline = DeadlineContext(
|
|
372
|
+
timeout=self.timeout,
|
|
373
|
+
operation=self.operation_name,
|
|
374
|
+
)
|
|
375
|
+
await self._deadline.__aenter__()
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
379
|
+
"""Exit the scope."""
|
|
380
|
+
if self._deadline is not None:
|
|
381
|
+
await self._deadline.__aexit__(exc_type, exc_val, exc_tb)
|
|
382
|
+
|
|
383
|
+
def __enter__(self) -> TimeoutScope:
|
|
384
|
+
"""Enter the scope (sync)."""
|
|
385
|
+
self._deadline = DeadlineContext(
|
|
386
|
+
timeout=self.timeout,
|
|
387
|
+
operation=self.operation_name,
|
|
388
|
+
)
|
|
389
|
+
self._deadline.__enter__()
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
393
|
+
"""Exit the scope (sync)."""
|
|
394
|
+
if self._deadline is not None:
|
|
395
|
+
self._deadline.__exit__(exc_type, exc_val, exc_tb)
|
|
396
|
+
|
|
397
|
+
def remaining(self) -> float:
|
|
398
|
+
"""Get remaining time."""
|
|
399
|
+
if self._deadline is None:
|
|
400
|
+
raise RuntimeError("TimeoutScope not started")
|
|
401
|
+
return self._deadline.remaining()
|
|
402
|
+
|
|
403
|
+
def elapsed(self) -> float:
|
|
404
|
+
"""Get elapsed time."""
|
|
405
|
+
if self._deadline is None:
|
|
406
|
+
return 0.0
|
|
407
|
+
return self._deadline.elapsed()
|
|
408
|
+
|
|
409
|
+
def checkpoint(self, name: str) -> None:
|
|
410
|
+
"""
|
|
411
|
+
Record a checkpoint.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
name: Name of the checkpoint.
|
|
415
|
+
"""
|
|
416
|
+
self._checkpoints.append((name, self.elapsed()))
|
|
417
|
+
|
|
418
|
+
def get_checkpoints(self) -> list[tuple[str, float]]:
|
|
419
|
+
"""Get all recorded checkpoints."""
|
|
420
|
+
return list(self._checkpoints)
|
|
421
|
+
|
|
422
|
+
async def run(
|
|
423
|
+
self,
|
|
424
|
+
name: str,
|
|
425
|
+
coro: Coroutine[Any, Any, T],
|
|
426
|
+
) -> T:
|
|
427
|
+
"""
|
|
428
|
+
Run a coroutine with remaining timeout.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
name: Name of the operation (for checkpointing).
|
|
432
|
+
coro: Coroutine to run.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Coroutine result.
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
TimeoutError: If deadline is exceeded.
|
|
439
|
+
"""
|
|
440
|
+
if self._deadline is None:
|
|
441
|
+
raise RuntimeError("TimeoutScope not started")
|
|
442
|
+
|
|
443
|
+
self.checkpoint(f"{name}_start")
|
|
444
|
+
try:
|
|
445
|
+
result = await run_with_deadline(coro, self._deadline, name)
|
|
446
|
+
self.checkpoint(f"{name}_end")
|
|
447
|
+
return result
|
|
448
|
+
except TimeoutError:
|
|
449
|
+
self.checkpoint(f"{name}_timeout")
|
|
450
|
+
raise
|
|
451
|
+
|
|
452
|
+
def run_sync(self, name: str, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
|
453
|
+
"""
|
|
454
|
+
Run a sync function with remaining timeout.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
name: Name of the operation.
|
|
458
|
+
func: Function to run.
|
|
459
|
+
*args: Positional arguments.
|
|
460
|
+
**kwargs: Keyword arguments.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Function result.
|
|
464
|
+
"""
|
|
465
|
+
if self._deadline is None:
|
|
466
|
+
raise RuntimeError("TimeoutScope not started")
|
|
467
|
+
|
|
468
|
+
self.checkpoint(f"{name}_start")
|
|
469
|
+
try:
|
|
470
|
+
result = _run_with_timeout_sync(
|
|
471
|
+
func, args, kwargs, self._deadline.remaining(), name
|
|
472
|
+
)
|
|
473
|
+
self.checkpoint(f"{name}_end")
|
|
474
|
+
return result
|
|
475
|
+
except TimeoutError:
|
|
476
|
+
self.checkpoint(f"{name}_timeout")
|
|
477
|
+
raise
|