agentle 0.9.4__py3-none-any.whl → 0.9.28__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 (85) hide show
  1. agentle/agents/agent.py +175 -10
  2. agentle/agents/agent_run_output.py +8 -1
  3. agentle/agents/apis/__init__.py +79 -6
  4. agentle/agents/apis/api.py +342 -73
  5. agentle/agents/apis/api_key_authentication.py +43 -0
  6. agentle/agents/apis/api_key_location.py +11 -0
  7. agentle/agents/apis/api_metrics.py +16 -0
  8. agentle/agents/apis/auth_type.py +17 -0
  9. agentle/agents/apis/authentication.py +32 -0
  10. agentle/agents/apis/authentication_base.py +42 -0
  11. agentle/agents/apis/authentication_config.py +117 -0
  12. agentle/agents/apis/basic_authentication.py +34 -0
  13. agentle/agents/apis/bearer_authentication.py +52 -0
  14. agentle/agents/apis/cache_strategy.py +12 -0
  15. agentle/agents/apis/circuit_breaker.py +69 -0
  16. agentle/agents/apis/circuit_breaker_error.py +7 -0
  17. agentle/agents/apis/circuit_breaker_state.py +11 -0
  18. agentle/agents/apis/endpoint.py +413 -254
  19. agentle/agents/apis/file_upload.py +23 -0
  20. agentle/agents/apis/hmac_authentication.py +56 -0
  21. agentle/agents/apis/no_authentication.py +27 -0
  22. agentle/agents/apis/oauth2_authentication.py +111 -0
  23. agentle/agents/apis/oauth2_grant_type.py +12 -0
  24. agentle/agents/apis/object_schema.py +86 -1
  25. agentle/agents/apis/params/__init__.py +10 -1
  26. agentle/agents/apis/params/boolean_param.py +44 -0
  27. agentle/agents/apis/params/number_param.py +56 -0
  28. agentle/agents/apis/rate_limit_error.py +7 -0
  29. agentle/agents/apis/rate_limiter.py +57 -0
  30. agentle/agents/apis/request_config.py +126 -4
  31. agentle/agents/apis/request_hook.py +16 -0
  32. agentle/agents/apis/response_cache.py +49 -0
  33. agentle/agents/apis/retry_strategy.py +12 -0
  34. agentle/agents/whatsapp/human_delay_calculator.py +462 -0
  35. agentle/agents/whatsapp/models/audio_message.py +6 -4
  36. agentle/agents/whatsapp/models/key.py +2 -2
  37. agentle/agents/whatsapp/models/whatsapp_bot_config.py +375 -21
  38. agentle/agents/whatsapp/models/whatsapp_response_base.py +31 -0
  39. agentle/agents/whatsapp/models/whatsapp_webhook_payload.py +5 -1
  40. agentle/agents/whatsapp/providers/base/whatsapp_provider.py +51 -0
  41. agentle/agents/whatsapp/providers/evolution/evolution_api_provider.py +237 -10
  42. agentle/agents/whatsapp/providers/meta/meta_whatsapp_provider.py +126 -0
  43. agentle/agents/whatsapp/v2/batch_processor_manager.py +4 -0
  44. agentle/agents/whatsapp/v2/bot_config.py +188 -0
  45. agentle/agents/whatsapp/v2/message_limit.py +9 -0
  46. agentle/agents/whatsapp/v2/payload.py +0 -0
  47. agentle/agents/whatsapp/v2/whatsapp_bot.py +13 -0
  48. agentle/agents/whatsapp/v2/whatsapp_cloud_api_provider.py +0 -0
  49. agentle/agents/whatsapp/v2/whatsapp_provider.py +0 -0
  50. agentle/agents/whatsapp/whatsapp_bot.py +827 -45
  51. agentle/generations/providers/google/adapters/generate_generate_content_response_to_generation_adapter.py +13 -10
  52. agentle/generations/providers/google/google_generation_provider.py +35 -5
  53. agentle/generations/providers/openrouter/_adapters/openrouter_message_to_generated_assistant_message_adapter.py +35 -1
  54. agentle/mcp/servers/stdio_mcp_server.py +23 -4
  55. agentle/parsing/parsers/docx.py +8 -0
  56. agentle/parsing/parsers/file_parser.py +4 -0
  57. agentle/parsing/parsers/pdf.py +7 -1
  58. agentle/storage/__init__.py +11 -0
  59. agentle/storage/file_storage_manager.py +44 -0
  60. agentle/storage/local_file_storage_manager.py +122 -0
  61. agentle/storage/s3_file_storage_manager.py +124 -0
  62. agentle/tts/audio_format.py +6 -0
  63. agentle/tts/elevenlabs_tts_provider.py +108 -0
  64. agentle/tts/output_format_type.py +26 -0
  65. agentle/tts/speech_config.py +14 -0
  66. agentle/tts/speech_result.py +15 -0
  67. agentle/tts/tts_provider.py +16 -0
  68. agentle/tts/voice_settings.py +30 -0
  69. agentle/utils/parse_streaming_json.py +39 -13
  70. agentle/voice_cloning/__init__.py +0 -0
  71. agentle/voice_cloning/voice_cloner.py +0 -0
  72. agentle/web/extractor.py +282 -148
  73. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/METADATA +1 -1
  74. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/RECORD +78 -39
  75. agentle/tts/real_time/definitions/audio_data.py +0 -20
  76. agentle/tts/real_time/definitions/speech_config.py +0 -27
  77. agentle/tts/real_time/definitions/speech_result.py +0 -14
  78. agentle/tts/real_time/definitions/tts_stream_chunk.py +0 -15
  79. agentle/tts/real_time/definitions/voice_gender.py +0 -9
  80. agentle/tts/real_time/definitions/voice_info.py +0 -18
  81. agentle/tts/real_time/real_time_speech_to_text_provider.py +0 -66
  82. /agentle/{tts/real_time → agents/whatsapp/v2}/__init__.py +0 -0
  83. /agentle/{tts/real_time/definitions/__init__.py → agents/whatsapp/v2/in_memory_batch_processor_manager.py} +0 -0
  84. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/WHEEL +0 -0
  85. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,23 @@
1
+ """File upload support for endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import mimetypes
6
+ from rsb.models.base_model import BaseModel
7
+
8
+
9
+ class FileUpload(BaseModel):
10
+ """Represents a file to be uploaded."""
11
+
12
+ filename: str
13
+ content: bytes
14
+ mime_type: str | None = None
15
+
16
+ def to_form_part(self) -> tuple[str, bytes, str]:
17
+ """Convert to multipart form part."""
18
+ mime = (
19
+ self.mime_type
20
+ or mimetypes.guess_type(self.filename)[0]
21
+ or "application/octet-stream"
22
+ )
23
+ return (self.filename, self.content, mime)
@@ -0,0 +1,56 @@
1
+ """HMAC signature authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import time
8
+ from collections.abc import MutableMapping
9
+ from typing import Any
10
+
11
+ import aiohttp
12
+ from agentle.agents.apis.authentication_base import AuthenticationBase
13
+
14
+
15
+ class HMACAuthentication(AuthenticationBase):
16
+ """HMAC signature authentication."""
17
+
18
+ def __init__(
19
+ self,
20
+ secret_key: str,
21
+ algorithm: str = "sha256",
22
+ header_name: str = "X-Signature",
23
+ include_timestamp: bool = True,
24
+ ):
25
+ self.secret_key = secret_key
26
+ self.algorithm = algorithm
27
+ self.header_name = header_name
28
+ self.include_timestamp = include_timestamp
29
+
30
+ async def apply_auth(
31
+ self,
32
+ session: aiohttp.ClientSession,
33
+ url: str,
34
+ headers: MutableMapping[str, str],
35
+ params: MutableMapping[str, Any],
36
+ ) -> None:
37
+ """Add HMAC signature to headers."""
38
+ # Build signature string
39
+ timestamp = str(int(time.time()))
40
+ signature_string = url
41
+
42
+ if self.include_timestamp:
43
+ signature_string = f"{timestamp}:{signature_string}"
44
+ headers["X-Timestamp"] = timestamp
45
+
46
+ # Calculate HMAC
47
+ hash_func = getattr(hashlib, self.algorithm)
48
+ signature = hmac.new(
49
+ self.secret_key.encode(), signature_string.encode(), hash_func
50
+ ).hexdigest()
51
+
52
+ headers[self.header_name] = signature
53
+
54
+ async def refresh_if_needed(self) -> bool:
55
+ """No refresh needed for HMAC."""
56
+ return False
@@ -0,0 +1,27 @@
1
+ """No authentication handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import MutableMapping
6
+ from typing import Any
7
+
8
+ import aiohttp
9
+ from agentle.agents.apis.authentication_base import AuthenticationBase
10
+
11
+
12
+ class NoAuthentication(AuthenticationBase):
13
+ """No authentication."""
14
+
15
+ async def apply_auth(
16
+ self,
17
+ session: aiohttp.ClientSession,
18
+ url: str,
19
+ headers: MutableMapping[str, str],
20
+ params: MutableMapping[str, Any],
21
+ ) -> None:
22
+ """No authentication to apply."""
23
+ pass
24
+
25
+ async def refresh_if_needed(self) -> bool:
26
+ """No refresh needed."""
27
+ return False
@@ -0,0 +1,111 @@
1
+ """OAuth2 authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import MutableMapping
7
+ from datetime import datetime, timedelta
8
+ from typing import Any
9
+
10
+ import aiohttp
11
+ from agentle.agents.apis.authentication_base import AuthenticationBase
12
+ from agentle.agents.apis.oauth2_grant_type import OAuth2GrantType
13
+
14
+
15
+ class OAuth2Authentication(AuthenticationBase):
16
+ """OAuth2 authentication with token refresh."""
17
+
18
+ def __init__(
19
+ self,
20
+ token_url: str,
21
+ client_id: str,
22
+ client_secret: str,
23
+ grant_type: OAuth2GrantType = OAuth2GrantType.CLIENT_CREDENTIALS,
24
+ scope: str | None = None,
25
+ refresh_token: str | None = None,
26
+ scopes: list[str] | None = None,
27
+ ):
28
+ self.token_url = token_url
29
+ self.client_id = client_id
30
+ self.client_secret = client_secret
31
+ self.grant_type = grant_type
32
+ # Support both single scope and multiple scopes
33
+ # If scopes list is provided, use it; otherwise fall back to single scope
34
+ self.scopes = scopes
35
+ self.scope = scope
36
+ self.refresh_token_value = refresh_token
37
+
38
+ self.access_token: str | None = None
39
+ self.token_expiry: datetime | None = None
40
+ self._refresh_lock = asyncio.Lock()
41
+
42
+ async def apply_auth(
43
+ self,
44
+ session: aiohttp.ClientSession,
45
+ url: str,
46
+ headers: MutableMapping[str, str],
47
+ params: MutableMapping[str, Any],
48
+ ) -> None:
49
+ """Add OAuth2 token to Authorization header."""
50
+ # Ensure we have a valid token
51
+ await self.refresh_if_needed()
52
+
53
+ if self.access_token:
54
+ headers["Authorization"] = f"Bearer {self.access_token}"
55
+
56
+ async def refresh_if_needed(self) -> bool:
57
+ """Refresh token if expired or missing."""
58
+ # Check if token needs refresh
59
+ if self.access_token and self.token_expiry:
60
+ # Add 60 second buffer before expiry
61
+ if datetime.now() < self.token_expiry - timedelta(seconds=60):
62
+ return False
63
+
64
+ # Use lock to prevent concurrent refreshes
65
+ async with self._refresh_lock:
66
+ # Double-check after acquiring lock
67
+ if self.access_token and self.token_expiry:
68
+ if datetime.now() < self.token_expiry - timedelta(seconds=60):
69
+ return False
70
+
71
+ # Refresh the token
72
+ await self._fetch_token()
73
+ return True
74
+
75
+ async def _fetch_token(self) -> None:
76
+ """Fetch a new access token."""
77
+ data: dict[str, str] = {
78
+ "client_id": self.client_id,
79
+ "client_secret": self.client_secret,
80
+ "grant_type": self.grant_type.value,
81
+ }
82
+
83
+ # Handle scopes - prefer scopes list over single scope
84
+ if self.scopes:
85
+ data["scope"] = " ".join(self.scopes)
86
+ elif self.scope:
87
+ data["scope"] = self.scope
88
+
89
+ if (
90
+ self.grant_type == OAuth2GrantType.REFRESH_TOKEN
91
+ and self.refresh_token_value
92
+ ):
93
+ data["refresh_token"] = self.refresh_token_value
94
+
95
+ async with aiohttp.ClientSession() as session:
96
+ async with session.post(self.token_url, data=data) as response:
97
+ if response.status == 200:
98
+ token_data = await response.json()
99
+ self.access_token = token_data["access_token"]
100
+
101
+ # Calculate expiry
102
+ expires_in = token_data.get("expires_in", 3600)
103
+ self.token_expiry = datetime.now() + timedelta(seconds=expires_in)
104
+
105
+ # Update refresh token if provided
106
+ if "refresh_token" in token_data:
107
+ self.refresh_token_value = token_data["refresh_token"]
108
+ else:
109
+ raise ValueError(
110
+ f"Failed to fetch OAuth2 token: HTTP {response.status}"
111
+ )
@@ -0,0 +1,12 @@
1
+ """OAuth2 grant types."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class OAuth2GrantType(StrEnum):
7
+ """OAuth2 grant types."""
8
+
9
+ CLIENT_CREDENTIALS = "client_credentials"
10
+ AUTHORIZATION_CODE = "authorization_code"
11
+ REFRESH_TOKEN = "refresh_token"
12
+ PASSWORD = "password"
@@ -8,7 +8,7 @@ Simply replace the existing EndpointParameter and related classes with these imp
8
8
  from __future__ import annotations
9
9
 
10
10
  from collections.abc import Mapping, Sequence
11
- from typing import TYPE_CHECKING, Any, Literal
11
+ from typing import TYPE_CHECKING, Any, Literal, cast
12
12
 
13
13
  from rsb.models.base_model import BaseModel
14
14
  from rsb.models.field import Field
@@ -39,3 +39,88 @@ class ObjectSchema(BaseModel):
39
39
  example: Mapping[str, Any] | None = Field(
40
40
  default=None, description="Example value for the object"
41
41
  )
42
+
43
+ @classmethod
44
+ def from_json_schema(
45
+ cls, schema: Mapping[str, Any]
46
+ ) -> ObjectSchema | ArraySchema | PrimitiveSchema:
47
+ """
48
+ Recursively convert a JSON Schema definition to Agentle schema types.
49
+
50
+ This method handles deeply nested objects, arrays, and primitives,
51
+ making it easy to convert complex JSON Schema definitions.
52
+
53
+ Args:
54
+ schema: JSON Schema definition (dict with 'type', 'properties', etc.)
55
+
56
+ Returns:
57
+ Appropriate schema type (ObjectSchema, ArraySchema, or PrimitiveSchema)
58
+
59
+ Example:
60
+ ```python
61
+ from agentle.agents.apis.object_schema import ObjectSchema
62
+
63
+ json_schema = {
64
+ "type": "object",
65
+ "properties": {
66
+ "user": {
67
+ "type": "object",
68
+ "properties": {
69
+ "name": {"type": "string"},
70
+ "age": {"type": "integer"},
71
+ "settings": {
72
+ "type": "object",
73
+ "properties": {
74
+ "theme": {"type": "string"},
75
+ "notifications": {"type": "boolean"}
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ schema = ObjectSchema.from_json_schema(json_schema)
84
+ ```
85
+ """
86
+ from agentle.agents.apis.array_schema import ArraySchema
87
+ from agentle.agents.apis.primitive_schema import PrimitiveSchema
88
+
89
+ schema_type = schema.get("type", "string")
90
+
91
+ if schema_type == "object":
92
+ properties: dict[str, ObjectSchema | ArraySchema | PrimitiveSchema] = {}
93
+ for prop_name, prop_schema in schema.get("properties", {}).items():
94
+ properties[prop_name] = cls.from_json_schema(prop_schema)
95
+
96
+ return cls(
97
+ properties=properties,
98
+ required=list(schema.get("required", [])),
99
+ additional_properties=schema.get("additionalProperties", True),
100
+ example=schema.get("example"),
101
+ )
102
+
103
+ elif schema_type == "array":
104
+ items_schema = schema.get("items", {"type": "string"})
105
+ return ArraySchema(
106
+ items=cls.from_json_schema(items_schema),
107
+ min_items=schema.get("minItems"),
108
+ max_items=schema.get("maxItems"),
109
+ example=schema.get("example"),
110
+ )
111
+
112
+ else:
113
+ # Primitive type
114
+ return PrimitiveSchema(
115
+ type=cast(
116
+ Literal["string", "integer", "boolean", "number"], schema_type
117
+ )
118
+ if schema_type in ["string", "integer", "number", "boolean"]
119
+ else "string",
120
+ format=schema.get("format"),
121
+ enum=schema.get("enum"),
122
+ minimum=schema.get("minimum"),
123
+ maximum=schema.get("maximum"),
124
+ pattern=schema.get("pattern"),
125
+ example=schema.get("example"),
126
+ )
@@ -1,6 +1,15 @@
1
1
  from .array_param import array_param
2
+ from .boolean_param import boolean_param
2
3
  from .integer_param import integer_param
4
+ from .number_param import number_param
3
5
  from .object_param import object_param
4
6
  from .string_param import string_param
5
7
 
6
- __all__: list[str] = ["array_param", "integer_param", "object_param", "string_param"]
8
+ __all__: list[str] = [
9
+ "array_param",
10
+ "boolean_param",
11
+ "integer_param",
12
+ "number_param",
13
+ "object_param",
14
+ "string_param",
15
+ ]
@@ -0,0 +1,44 @@
1
+ from agentle.agents.apis.endpoint_parameter import EndpointParameter
2
+ from agentle.agents.apis.parameter_location import ParameterLocation
3
+ from agentle.agents.apis.primitive_schema import PrimitiveSchema
4
+
5
+
6
+ def boolean_param(
7
+ name: str,
8
+ description: str,
9
+ required: bool = False,
10
+ default: bool | None = None,
11
+ location: ParameterLocation = ParameterLocation.QUERY,
12
+ ) -> EndpointParameter:
13
+ """Create a boolean parameter.
14
+
15
+ Args:
16
+ name: Parameter name
17
+ description: Parameter description
18
+ required: Whether the parameter is required
19
+ default: Default value for the parameter
20
+ location: Where the parameter should be placed in the request
21
+
22
+ Returns:
23
+ EndpointParameter configured for boolean values
24
+
25
+ Example:
26
+ ```python
27
+ from agentle.agents.apis.params.boolean_param import boolean_param
28
+
29
+ boolean_param(
30
+ name="enabled",
31
+ description="Enable feature",
32
+ required=False,
33
+ default=True
34
+ )
35
+ ```
36
+ """
37
+ return EndpointParameter(
38
+ name=name,
39
+ description=description,
40
+ parameter_schema=PrimitiveSchema(type="boolean"),
41
+ location=location,
42
+ required=required,
43
+ default=default,
44
+ )
@@ -0,0 +1,56 @@
1
+ from agentle.agents.apis.endpoint_parameter import EndpointParameter
2
+ from agentle.agents.apis.parameter_location import ParameterLocation
3
+ from agentle.agents.apis.primitive_schema import PrimitiveSchema
4
+
5
+
6
+ def number_param(
7
+ name: str,
8
+ description: str,
9
+ required: bool = False,
10
+ minimum: float | None = None,
11
+ maximum: float | None = None,
12
+ default: float | None = None,
13
+ location: ParameterLocation = ParameterLocation.QUERY,
14
+ format: str | None = None,
15
+ ) -> EndpointParameter:
16
+ """Create a number (float/decimal) parameter.
17
+
18
+ Args:
19
+ name: Parameter name
20
+ description: Parameter description
21
+ required: Whether the parameter is required
22
+ minimum: Minimum allowed value
23
+ maximum: Maximum allowed value
24
+ default: Default value for the parameter
25
+ location: Where the parameter should be placed in the request
26
+ format: Format hint (e.g., 'float', 'double', 'decimal')
27
+
28
+ Returns:
29
+ EndpointParameter configured for number values
30
+
31
+ Example:
32
+ ```python
33
+ from agentle.agents.apis.params.number_param import number_param
34
+
35
+ number_param(
36
+ name="price",
37
+ description="Product price",
38
+ required=True,
39
+ minimum=0.0,
40
+ default=99.99
41
+ )
42
+ ```
43
+ """
44
+ return EndpointParameter(
45
+ name=name,
46
+ description=description,
47
+ parameter_schema=PrimitiveSchema(
48
+ type="number",
49
+ minimum=minimum,
50
+ maximum=maximum,
51
+ format=format,
52
+ ),
53
+ location=location,
54
+ required=required,
55
+ default=default,
56
+ )
@@ -0,0 +1,7 @@
1
+ """Rate limit errors."""
2
+
3
+
4
+ class RateLimitError(Exception):
5
+ """Raised when rate limit is exceeded."""
6
+
7
+ pass
@@ -0,0 +1,57 @@
1
+ """Rate limiter for API calls.
2
+
3
+ This module adapts the resilience module's rate limiter implementations
4
+ for use in the APIs module, maintaining backward compatibility.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+
11
+ from agentle.agents.apis.request_config import RequestConfig
12
+ from agentle.resilience.rate_limiting.rate_limit_config import RateLimitConfig
13
+ from agentle.resilience.rate_limiting.in_memory_rate_limiter import (
14
+ InMemoryRateLimiter as ResilienceRateLimiter,
15
+ )
16
+
17
+
18
+ class RateLimiter:
19
+ """
20
+ Rate limiter for API calls.
21
+
22
+ This wraps the resilience module's InMemoryRateLimiter to provide
23
+ a simpler acquire-based API for endpoint usage.
24
+ """
25
+
26
+ def __init__(self, config: RequestConfig):
27
+ self.config = config
28
+ self._identifier = "default" # Single rate limit per endpoint
29
+
30
+ # Convert rate limit config to resilience module format
31
+ rate_limit_config: RateLimitConfig = {
32
+ "max_requests_per_minute": int(
33
+ config.rate_limit_calls * (60 / config.rate_limit_period)
34
+ )
35
+ if config.rate_limit_period <= 60
36
+ else config.rate_limit_calls,
37
+ }
38
+
39
+ # Initialize the underlying rate limiter from resilience module
40
+ self._impl = ResilienceRateLimiter(
41
+ default_config=rate_limit_config,
42
+ enable_metrics=config.enable_metrics,
43
+ )
44
+
45
+ async def acquire(self) -> None:
46
+ """
47
+ Acquire rate limit slot, waiting if necessary.
48
+
49
+ This will block until a slot is available.
50
+ """
51
+ # Wait until we can proceed
52
+ while not await self._impl.can_proceed(self._identifier):
53
+ # Wait a short time before checking again
54
+ await asyncio.sleep(0.1)
55
+
56
+ # Record the request
57
+ await self._impl.record_request(self._identifier)
@@ -1,20 +1,142 @@
1
+ """
2
+ Enhanced request configuration with advanced features.
3
+
4
+ Includes timeouts, retries, circuit breakers, rate limiting, caching, and more.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+
1
11
  from rsb.models.base_model import BaseModel
2
12
  from rsb.models.field import Field
3
13
 
14
+ from agentle.agents.apis.cache_strategy import CacheStrategy
15
+ from agentle.agents.apis.retry_strategy import RetryStrategy
16
+
4
17
 
5
18
  class RequestConfig(BaseModel):
6
- """Configuration for HTTP requests."""
19
+ """
20
+ Enhanced configuration for HTTP requests.
7
21
 
8
- timeout: float = Field(description="Request timeout in seconds", default=30.0)
22
+ This configuration can be set at both API-level and per-endpoint level.
23
+ Endpoint-level configs override API-level configs.
24
+
25
+ Example:
26
+ ```python
27
+ # API-level config (applies to all endpoints)
28
+ api_config = RequestConfig(
29
+ timeout=30.0,
30
+ max_retries=3,
31
+ enable_caching=True
32
+ )
33
+
34
+ # Per-endpoint override
35
+ endpoint_config = RequestConfig(
36
+ timeout=60.0, # Override for this specific endpoint
37
+ max_retries=5,
38
+ cache_ttl=600.0
39
+ )
40
+ ```
41
+ """
42
+
43
+ # Timeouts (in seconds)
44
+ timeout: float = Field(description="Total request timeout in seconds", default=30.0)
45
+ connect_timeout: float | None = Field(
46
+ description="Connection timeout in seconds", default=None
47
+ )
48
+ read_timeout: float | None = Field(
49
+ description="Read timeout in seconds", default=None
50
+ )
9
51
 
52
+ # Retry configuration
10
53
  max_retries: int = Field(
11
54
  description="Maximum number of retries for failed requests", default=3
12
55
  )
13
-
14
56
  retry_delay: float = Field(
15
- description="Delay between retries in seconds", default=1.0
57
+ description="Base delay between retries in seconds", default=1.0
58
+ )
59
+ retry_strategy: RetryStrategy = Field(
60
+ description="Strategy for calculating retry delays",
61
+ default=RetryStrategy.EXPONENTIAL,
62
+ )
63
+ retry_on_status_codes: Sequence[int] = Field(
64
+ description="HTTP status codes that should trigger retries",
65
+ default_factory=lambda: [408, 429, 500, 502, 503, 504],
66
+ )
67
+ retry_on_exceptions: bool = Field(
68
+ description="Whether to retry on network exceptions", default=True
69
+ )
70
+
71
+ # Circuit breaker configuration
72
+ enable_circuit_breaker: bool = Field(
73
+ description="Enable circuit breaker pattern", default=False
74
+ )
75
+ circuit_breaker_failure_threshold: int = Field(
76
+ description="Number of failures before opening circuit", default=5
77
+ )
78
+ circuit_breaker_recovery_timeout: float = Field(
79
+ description="Seconds to wait before attempting recovery", default=60.0
80
+ )
81
+ circuit_breaker_success_threshold: int = Field(
82
+ description="Number of successes in half-open state to close circuit", default=2
16
83
  )
17
84
 
85
+ # Rate limiting
86
+ enable_rate_limiting: bool = Field(
87
+ description="Enable rate limiting", default=False
88
+ )
89
+ rate_limit_calls: int = Field(
90
+ description="Maximum number of calls per period", default=100
91
+ )
92
+ rate_limit_period: float = Field(
93
+ description="Rate limit period in seconds", default=60.0
94
+ )
95
+ respect_retry_after: bool = Field(
96
+ description="Respect Retry-After header from server", default=True
97
+ )
98
+
99
+ # Caching
100
+ enable_caching: bool = Field(description="Enable response caching", default=False)
101
+ cache_strategy: CacheStrategy = Field(
102
+ description="Caching strategy to use", default=CacheStrategy.MEMORY
103
+ )
104
+ cache_ttl: float = Field(description="Cache TTL in seconds", default=300.0)
105
+ cache_only_get: bool = Field(description="Only cache GET requests", default=True)
106
+
107
+ # Request/Response hooks
108
+ enable_request_logging: bool = Field(
109
+ description="Enable request logging", default=False
110
+ )
111
+ enable_response_logging: bool = Field(
112
+ description="Enable response logging", default=False
113
+ )
114
+ enable_metrics: bool = Field(description="Enable metrics collection", default=False)
115
+
116
+ # Connection settings
18
117
  follow_redirects: bool = Field(
19
118
  description="Whether to follow HTTP redirects", default=True
20
119
  )
120
+ max_redirects: int = Field(
121
+ description="Maximum number of redirects to follow", default=10
122
+ )
123
+ verify_ssl: bool = Field(description="Verify SSL certificates", default=True)
124
+ ssl_cert_path: str | None = Field(
125
+ description="Path to custom SSL certificate", default=None
126
+ )
127
+
128
+ # Proxy configuration
129
+ proxy_url: str | None = Field(
130
+ description="Proxy URL (http://host:port)", default=None
131
+ )
132
+ proxy_auth: tuple[str, str] | None = Field(
133
+ description="Proxy authentication (username, password)", default=None
134
+ )
135
+
136
+ # Content handling
137
+ compress_request: bool = Field(
138
+ description="Enable request compression", default=False
139
+ )
140
+ decompress_response: bool = Field(
141
+ description="Enable response decompression", default=True
142
+ )
@@ -0,0 +1,16 @@
1
+ """Request hooks for endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from rsb.models.base_model import BaseModel
9
+ from rsb.models.field import Field
10
+
11
+
12
+ class RequestHook(BaseModel):
13
+ """Hook for request/response interception."""
14
+
15
+ name: str
16
+ callback: Callable[[dict[str, Any]], Any] | None = None