agent_os_kernel 3.1.0__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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
atr/policies.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Policy definitions for tool execution control.
|
|
5
|
+
|
|
6
|
+
Provides retry policies, rate limiting, and other execution policies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections import deque
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Callable, Optional, TypeVar, Union
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BackoffStrategy(str, Enum):
|
|
21
|
+
"""Backoff strategies for retry policies."""
|
|
22
|
+
|
|
23
|
+
CONSTANT = "constant"
|
|
24
|
+
LINEAR = "linear"
|
|
25
|
+
EXPONENTIAL = "exponential"
|
|
26
|
+
FIBONACCI = "fibonacci"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class RetryPolicy:
|
|
31
|
+
"""Configuration for retry behavior.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
max_attempts: Maximum number of retry attempts (including initial).
|
|
35
|
+
backoff: Backoff strategy between retries.
|
|
36
|
+
initial_delay: Initial delay in seconds before first retry.
|
|
37
|
+
max_delay: Maximum delay in seconds between retries.
|
|
38
|
+
jitter: Whether to add random jitter to delays.
|
|
39
|
+
retry_on: Exception types to retry on. None means retry on all exceptions.
|
|
40
|
+
on_retry: Optional callback called on each retry with (attempt, exception, delay).
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> policy = RetryPolicy(
|
|
44
|
+
... max_attempts=3,
|
|
45
|
+
... backoff=BackoffStrategy.EXPONENTIAL,
|
|
46
|
+
... initial_delay=1.0,
|
|
47
|
+
... max_delay=30.0
|
|
48
|
+
... )
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
max_attempts: int = 3
|
|
52
|
+
backoff: Union[BackoffStrategy, str] = BackoffStrategy.EXPONENTIAL
|
|
53
|
+
initial_delay: float = 1.0
|
|
54
|
+
max_delay: float = 60.0
|
|
55
|
+
jitter: bool = True
|
|
56
|
+
retry_on: Optional[tuple] = None
|
|
57
|
+
on_retry: Optional[Callable[[int, Exception, float], None]] = None
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
"""Convert string backoff to enum."""
|
|
61
|
+
if isinstance(self.backoff, str):
|
|
62
|
+
self.backoff = BackoffStrategy(self.backoff.lower())
|
|
63
|
+
|
|
64
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
65
|
+
"""Calculate delay before the given attempt.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
attempt: The attempt number (1-indexed).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Delay in seconds.
|
|
72
|
+
"""
|
|
73
|
+
if attempt <= 1:
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
retry_number = attempt - 1 # 0-indexed for calculation
|
|
77
|
+
|
|
78
|
+
if self.backoff == BackoffStrategy.CONSTANT:
|
|
79
|
+
delay = self.initial_delay
|
|
80
|
+
elif self.backoff == BackoffStrategy.LINEAR:
|
|
81
|
+
delay = self.initial_delay * retry_number
|
|
82
|
+
elif self.backoff == BackoffStrategy.EXPONENTIAL:
|
|
83
|
+
delay = self.initial_delay * (2 ** (retry_number - 1))
|
|
84
|
+
elif self.backoff == BackoffStrategy.FIBONACCI:
|
|
85
|
+
delay = self.initial_delay * self._fibonacci(retry_number)
|
|
86
|
+
else:
|
|
87
|
+
delay = self.initial_delay
|
|
88
|
+
|
|
89
|
+
# Apply max delay cap
|
|
90
|
+
delay = min(delay, self.max_delay)
|
|
91
|
+
|
|
92
|
+
# Add jitter if enabled (±25%)
|
|
93
|
+
if self.jitter:
|
|
94
|
+
import random
|
|
95
|
+
|
|
96
|
+
jitter_range = delay * 0.25
|
|
97
|
+
delay += random.uniform(-jitter_range, jitter_range)
|
|
98
|
+
delay = max(0, delay)
|
|
99
|
+
|
|
100
|
+
return delay
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _fibonacci(n: int) -> int:
|
|
104
|
+
"""Calculate nth Fibonacci number."""
|
|
105
|
+
if n <= 1:
|
|
106
|
+
return 1
|
|
107
|
+
a, b = 1, 1
|
|
108
|
+
for _ in range(n - 1):
|
|
109
|
+
a, b = b, a + b
|
|
110
|
+
return b
|
|
111
|
+
|
|
112
|
+
def should_retry(self, exception: Exception) -> bool:
|
|
113
|
+
"""Check if we should retry on this exception.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
exception: The exception that occurred.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if should retry, False otherwise.
|
|
120
|
+
"""
|
|
121
|
+
if self.retry_on is None:
|
|
122
|
+
return True
|
|
123
|
+
return isinstance(exception, self.retry_on)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class RateLimitPolicy:
|
|
128
|
+
"""Configuration for rate limiting.
|
|
129
|
+
|
|
130
|
+
Supports various rate limiting formats:
|
|
131
|
+
- "10/minute" - 10 calls per minute
|
|
132
|
+
- "100/hour" - 100 calls per hour
|
|
133
|
+
- "5/second" - 5 calls per second
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
limit: Maximum number of calls allowed.
|
|
137
|
+
period: Time period in seconds.
|
|
138
|
+
burst: Optional burst allowance above the limit.
|
|
139
|
+
on_limited: Optional callback when rate limited.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> policy = RateLimitPolicy.from_string("10/minute")
|
|
143
|
+
>>> # or
|
|
144
|
+
>>> policy = RateLimitPolicy(limit=10, period=60)
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
limit: int
|
|
148
|
+
period: float # in seconds
|
|
149
|
+
burst: Optional[int] = None
|
|
150
|
+
on_limited: Optional[Callable[[], None]] = None
|
|
151
|
+
|
|
152
|
+
_calls: deque = field(default_factory=deque, repr=False)
|
|
153
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_string(cls, rate_string: str) -> "RateLimitPolicy":
|
|
157
|
+
"""Parse a rate limit string like '10/minute'.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
rate_string: String in format 'N/period' where period is
|
|
161
|
+
'second', 'minute', 'hour', or 'day'.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
RateLimitPolicy instance.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If format is invalid.
|
|
168
|
+
"""
|
|
169
|
+
if "/" not in rate_string:
|
|
170
|
+
raise ValueError(f"Invalid rate limit format: {rate_string}. Expected 'N/period'")
|
|
171
|
+
|
|
172
|
+
parts = rate_string.split("/")
|
|
173
|
+
if len(parts) != 2:
|
|
174
|
+
raise ValueError(f"Invalid rate limit format: {rate_string}")
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
limit = int(parts[0])
|
|
178
|
+
except ValueError as err:
|
|
179
|
+
raise ValueError(f"Invalid limit value: {parts[0]}") from err
|
|
180
|
+
|
|
181
|
+
period_map = {
|
|
182
|
+
"second": 1,
|
|
183
|
+
"sec": 1,
|
|
184
|
+
"s": 1,
|
|
185
|
+
"minute": 60,
|
|
186
|
+
"min": 60,
|
|
187
|
+
"m": 60,
|
|
188
|
+
"hour": 3600,
|
|
189
|
+
"hr": 3600,
|
|
190
|
+
"h": 3600,
|
|
191
|
+
"day": 86400,
|
|
192
|
+
"d": 86400,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
period_str = parts[1].lower().strip()
|
|
196
|
+
if period_str not in period_map:
|
|
197
|
+
raise ValueError(f"Unknown period: {period_str}. Valid: {list(period_map.keys())}")
|
|
198
|
+
|
|
199
|
+
return cls(limit=limit, period=period_map[period_str])
|
|
200
|
+
|
|
201
|
+
def acquire(self, blocking: bool = True, timeout: Optional[float] = None) -> bool:
|
|
202
|
+
"""Attempt to acquire a rate limit slot.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
blocking: If True, wait until a slot is available.
|
|
206
|
+
timeout: Maximum time to wait if blocking (None = forever).
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if acquired, False if rate limited (non-blocking mode).
|
|
210
|
+
"""
|
|
211
|
+
start_time = time.monotonic()
|
|
212
|
+
|
|
213
|
+
while True:
|
|
214
|
+
with self._lock:
|
|
215
|
+
now = time.monotonic()
|
|
216
|
+
|
|
217
|
+
# Remove expired calls
|
|
218
|
+
cutoff = now - self.period
|
|
219
|
+
while self._calls and self._calls[0] < cutoff:
|
|
220
|
+
self._calls.popleft()
|
|
221
|
+
|
|
222
|
+
# Check if we can make a call
|
|
223
|
+
effective_limit = self.limit + (self.burst or 0)
|
|
224
|
+
if len(self._calls) < effective_limit:
|
|
225
|
+
self._calls.append(now)
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
if not blocking:
|
|
229
|
+
if self.on_limited:
|
|
230
|
+
self.on_limited()
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
# Calculate wait time
|
|
234
|
+
wait_time = self._calls[0] + self.period - now if self._calls else 0.1
|
|
235
|
+
|
|
236
|
+
# Check timeout
|
|
237
|
+
if timeout is not None:
|
|
238
|
+
elapsed = time.monotonic() - start_time
|
|
239
|
+
if elapsed >= timeout:
|
|
240
|
+
if self.on_limited:
|
|
241
|
+
self.on_limited()
|
|
242
|
+
return False
|
|
243
|
+
wait_time = min(wait_time, timeout - elapsed)
|
|
244
|
+
|
|
245
|
+
time.sleep(min(wait_time, 0.1)) # Sleep in small increments
|
|
246
|
+
|
|
247
|
+
async def acquire_async(self, blocking: bool = True, timeout: Optional[float] = None) -> bool:
|
|
248
|
+
"""Async version of acquire.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
blocking: If True, wait until a slot is available.
|
|
252
|
+
timeout: Maximum time to wait if blocking (None = forever).
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if acquired, False if rate limited.
|
|
256
|
+
"""
|
|
257
|
+
start_time = asyncio.get_event_loop().time()
|
|
258
|
+
|
|
259
|
+
while True:
|
|
260
|
+
with self._lock:
|
|
261
|
+
now = time.monotonic()
|
|
262
|
+
|
|
263
|
+
# Remove expired calls
|
|
264
|
+
cutoff = now - self.period
|
|
265
|
+
while self._calls and self._calls[0] < cutoff:
|
|
266
|
+
self._calls.popleft()
|
|
267
|
+
|
|
268
|
+
# Check if we can make a call
|
|
269
|
+
effective_limit = self.limit + (self.burst or 0)
|
|
270
|
+
if len(self._calls) < effective_limit:
|
|
271
|
+
self._calls.append(now)
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
if not blocking:
|
|
275
|
+
if self.on_limited:
|
|
276
|
+
self.on_limited()
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
# Calculate wait time
|
|
280
|
+
wait_time = self._calls[0] + self.period - now if self._calls else 0.1
|
|
281
|
+
|
|
282
|
+
# Check timeout
|
|
283
|
+
if timeout is not None:
|
|
284
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
285
|
+
if elapsed >= timeout:
|
|
286
|
+
if self.on_limited:
|
|
287
|
+
self.on_limited()
|
|
288
|
+
return False
|
|
289
|
+
wait_time = min(wait_time, timeout - elapsed)
|
|
290
|
+
|
|
291
|
+
await asyncio.sleep(min(wait_time, 0.1))
|
|
292
|
+
|
|
293
|
+
def reset(self) -> None:
|
|
294
|
+
"""Reset the rate limiter state."""
|
|
295
|
+
with self._lock:
|
|
296
|
+
self._calls.clear()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class RateLimitExceeded(Exception):
|
|
300
|
+
"""Raised when rate limit is exceeded in non-blocking mode."""
|
|
301
|
+
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class RetryExhausted(Exception):
|
|
306
|
+
"""Raised when all retry attempts have been exhausted."""
|
|
307
|
+
|
|
308
|
+
def __init__(self, message: str, last_exception: Exception, attempts: int):
|
|
309
|
+
super().__init__(message)
|
|
310
|
+
self.last_exception = last_exception
|
|
311
|
+
self.attempts = attempts
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
T = TypeVar("T")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def with_retry(policy: RetryPolicy, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
|
318
|
+
"""Execute a function with retry policy.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
policy: The retry policy to use.
|
|
322
|
+
func: The function to execute.
|
|
323
|
+
*args: Positional arguments to pass to func.
|
|
324
|
+
**kwargs: Keyword arguments to pass to func.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The result of the function.
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
RetryExhausted: If all attempts fail.
|
|
331
|
+
"""
|
|
332
|
+
last_exception = None
|
|
333
|
+
|
|
334
|
+
for attempt in range(1, policy.max_attempts + 1):
|
|
335
|
+
try:
|
|
336
|
+
# Calculate and apply delay
|
|
337
|
+
delay = policy.calculate_delay(attempt)
|
|
338
|
+
if delay > 0:
|
|
339
|
+
time.sleep(delay)
|
|
340
|
+
|
|
341
|
+
return func(*args, **kwargs)
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
last_exception = e
|
|
345
|
+
|
|
346
|
+
if not policy.should_retry(e):
|
|
347
|
+
raise
|
|
348
|
+
|
|
349
|
+
if attempt < policy.max_attempts:
|
|
350
|
+
next_delay = policy.calculate_delay(attempt + 1)
|
|
351
|
+
if policy.on_retry:
|
|
352
|
+
policy.on_retry(attempt, e, next_delay)
|
|
353
|
+
|
|
354
|
+
raise RetryExhausted(
|
|
355
|
+
f"All {policy.max_attempts} attempts failed", last_exception, policy.max_attempts
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def with_retry_async(
|
|
360
|
+
policy: RetryPolicy, func: Callable[..., T], *args: Any, **kwargs: Any
|
|
361
|
+
) -> T:
|
|
362
|
+
"""Execute an async function with retry policy.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
policy: The retry policy to use.
|
|
366
|
+
func: The async function to execute.
|
|
367
|
+
*args: Positional arguments to pass to func.
|
|
368
|
+
**kwargs: Keyword arguments to pass to func.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
The result of the function.
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
RetryExhausted: If all attempts fail.
|
|
375
|
+
"""
|
|
376
|
+
last_exception = None
|
|
377
|
+
|
|
378
|
+
for attempt in range(1, policy.max_attempts + 1):
|
|
379
|
+
try:
|
|
380
|
+
# Calculate and apply delay
|
|
381
|
+
delay = policy.calculate_delay(attempt)
|
|
382
|
+
if delay > 0:
|
|
383
|
+
await asyncio.sleep(delay)
|
|
384
|
+
|
|
385
|
+
result = func(*args, **kwargs)
|
|
386
|
+
if asyncio.iscoroutine(result):
|
|
387
|
+
result = await result
|
|
388
|
+
return result
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
last_exception = e
|
|
392
|
+
|
|
393
|
+
if not policy.should_retry(e):
|
|
394
|
+
raise
|
|
395
|
+
|
|
396
|
+
if attempt < policy.max_attempts:
|
|
397
|
+
next_delay = policy.calculate_delay(attempt + 1)
|
|
398
|
+
if policy.on_retry:
|
|
399
|
+
policy.on_retry(attempt, e, next_delay)
|
|
400
|
+
|
|
401
|
+
raise RetryExhausted(
|
|
402
|
+
f"All {policy.max_attempts} attempts failed", last_exception, policy.max_attempts
|
|
403
|
+
)
|
atr/py.typed
ADDED