amazon-ads-mcp 0.2.7__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.
- amazon_ads_mcp/__init__.py +11 -0
- amazon_ads_mcp/auth/__init__.py +33 -0
- amazon_ads_mcp/auth/base.py +211 -0
- amazon_ads_mcp/auth/hooks.py +172 -0
- amazon_ads_mcp/auth/manager.py +791 -0
- amazon_ads_mcp/auth/oauth_state_store.py +277 -0
- amazon_ads_mcp/auth/providers/__init__.py +14 -0
- amazon_ads_mcp/auth/providers/direct.py +393 -0
- amazon_ads_mcp/auth/providers/example_auth0.py.example +216 -0
- amazon_ads_mcp/auth/providers/openbridge.py +512 -0
- amazon_ads_mcp/auth/registry.py +146 -0
- amazon_ads_mcp/auth/secure_token_store.py +297 -0
- amazon_ads_mcp/auth/token_store.py +723 -0
- amazon_ads_mcp/config/__init__.py +5 -0
- amazon_ads_mcp/config/sampling.py +111 -0
- amazon_ads_mcp/config/settings.py +366 -0
- amazon_ads_mcp/exceptions.py +314 -0
- amazon_ads_mcp/middleware/__init__.py +11 -0
- amazon_ads_mcp/middleware/authentication.py +1474 -0
- amazon_ads_mcp/middleware/caching.py +177 -0
- amazon_ads_mcp/middleware/oauth.py +175 -0
- amazon_ads_mcp/middleware/sampling.py +112 -0
- amazon_ads_mcp/models/__init__.py +320 -0
- amazon_ads_mcp/models/amc_models.py +837 -0
- amazon_ads_mcp/models/api_responses.py +847 -0
- amazon_ads_mcp/models/base_models.py +215 -0
- amazon_ads_mcp/models/builtin_responses.py +496 -0
- amazon_ads_mcp/models/dsp_models.py +556 -0
- amazon_ads_mcp/models/stores_brands.py +610 -0
- amazon_ads_mcp/server/__init__.py +6 -0
- amazon_ads_mcp/server/__main__.py +6 -0
- amazon_ads_mcp/server/builtin_prompts.py +269 -0
- amazon_ads_mcp/server/builtin_tools.py +962 -0
- amazon_ads_mcp/server/file_routes.py +547 -0
- amazon_ads_mcp/server/html_templates.py +149 -0
- amazon_ads_mcp/server/mcp_server.py +327 -0
- amazon_ads_mcp/server/openapi_utils.py +158 -0
- amazon_ads_mcp/server/sampling_handler.py +251 -0
- amazon_ads_mcp/server/server_builder.py +751 -0
- amazon_ads_mcp/server/sidecar_loader.py +178 -0
- amazon_ads_mcp/server/transform_executor.py +827 -0
- amazon_ads_mcp/tools/__init__.py +22 -0
- amazon_ads_mcp/tools/cache_management.py +105 -0
- amazon_ads_mcp/tools/download_tools.py +267 -0
- amazon_ads_mcp/tools/identity.py +236 -0
- amazon_ads_mcp/tools/oauth.py +598 -0
- amazon_ads_mcp/tools/profile.py +150 -0
- amazon_ads_mcp/tools/profile_listing.py +285 -0
- amazon_ads_mcp/tools/region.py +320 -0
- amazon_ads_mcp/tools/region_identity.py +175 -0
- amazon_ads_mcp/utils/__init__.py +6 -0
- amazon_ads_mcp/utils/async_compat.py +215 -0
- amazon_ads_mcp/utils/errors.py +452 -0
- amazon_ads_mcp/utils/export_content_type_resolver.py +249 -0
- amazon_ads_mcp/utils/export_download_handler.py +579 -0
- amazon_ads_mcp/utils/header_resolver.py +81 -0
- amazon_ads_mcp/utils/http/__init__.py +56 -0
- amazon_ads_mcp/utils/http/circuit_breaker.py +127 -0
- amazon_ads_mcp/utils/http/client_manager.py +329 -0
- amazon_ads_mcp/utils/http/request.py +207 -0
- amazon_ads_mcp/utils/http/resilience.py +512 -0
- amazon_ads_mcp/utils/http/resilient_client.py +195 -0
- amazon_ads_mcp/utils/http/retry.py +76 -0
- amazon_ads_mcp/utils/http_client.py +873 -0
- amazon_ads_mcp/utils/media/__init__.py +21 -0
- amazon_ads_mcp/utils/media/negotiator.py +243 -0
- amazon_ads_mcp/utils/media/types.py +199 -0
- amazon_ads_mcp/utils/openapi/__init__.py +16 -0
- amazon_ads_mcp/utils/openapi/json.py +55 -0
- amazon_ads_mcp/utils/openapi/loader.py +263 -0
- amazon_ads_mcp/utils/openapi/refs.py +46 -0
- amazon_ads_mcp/utils/region_config.py +200 -0
- amazon_ads_mcp/utils/response_wrapper.py +171 -0
- amazon_ads_mcp/utils/sampling_helpers.py +156 -0
- amazon_ads_mcp/utils/sampling_wrapper.py +173 -0
- amazon_ads_mcp/utils/security.py +630 -0
- amazon_ads_mcp/utils/tool_naming.py +137 -0
- amazon_ads_mcp-0.2.7.dist-info/METADATA +664 -0
- amazon_ads_mcp-0.2.7.dist-info/RECORD +82 -0
- amazon_ads_mcp-0.2.7.dist-info/WHEEL +4 -0
- amazon_ads_mcp-0.2.7.dist-info/entry_points.txt +3 -0
- amazon_ads_mcp-0.2.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""HTTP utilities public API (barrel module).
|
|
2
|
+
|
|
3
|
+
This package provides:
|
|
4
|
+
- Shared HTTP client manager and helpers
|
|
5
|
+
- Authenticated client for Amazon Ads API
|
|
6
|
+
- Retry decorator with jittered backoff
|
|
7
|
+
- Circuit breaker
|
|
8
|
+
- Request helpers and convenience wrappers
|
|
9
|
+
|
|
10
|
+
All names are re-exported here to preserve import compatibility.
|
|
11
|
+
|
|
12
|
+
Recommended import pattern for consumers:
|
|
13
|
+
from amazon_ads_mcp.utils.http import get_http_client, async_retry, make_request
|
|
14
|
+
from amazon_ads_mcp.utils.http import AuthenticatedClient # For direct use
|
|
15
|
+
|
|
16
|
+
This keeps call sites stable even if internal modules are reorganized.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .circuit_breaker import CircuitBreaker, CircuitBreakerState
|
|
20
|
+
from .client_manager import (
|
|
21
|
+
HTTPClientManager,
|
|
22
|
+
create_limits,
|
|
23
|
+
create_timeout,
|
|
24
|
+
get_http_client,
|
|
25
|
+
health_check,
|
|
26
|
+
http_client_manager,
|
|
27
|
+
)
|
|
28
|
+
from .request import (
|
|
29
|
+
HTTPResponse,
|
|
30
|
+
delete,
|
|
31
|
+
get,
|
|
32
|
+
make_request,
|
|
33
|
+
patch,
|
|
34
|
+
post,
|
|
35
|
+
put,
|
|
36
|
+
)
|
|
37
|
+
from .retry import async_retry
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"HTTPClientManager",
|
|
41
|
+
"http_client_manager",
|
|
42
|
+
"get_http_client",
|
|
43
|
+
"create_timeout",
|
|
44
|
+
"create_limits",
|
|
45
|
+
"health_check",
|
|
46
|
+
"async_retry",
|
|
47
|
+
"CircuitBreaker",
|
|
48
|
+
"CircuitBreakerState",
|
|
49
|
+
"HTTPResponse",
|
|
50
|
+
"make_request",
|
|
51
|
+
"get",
|
|
52
|
+
"post",
|
|
53
|
+
"put",
|
|
54
|
+
"delete",
|
|
55
|
+
"patch",
|
|
56
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Circuit breaker implementation for HTTP requests.
|
|
2
|
+
|
|
3
|
+
This module provides a circuit breaker pattern implementation designed
|
|
4
|
+
for HTTP operations. The circuit breaker helps prevent cascading failures
|
|
5
|
+
by temporarily stopping requests when a service is experiencing issues,
|
|
6
|
+
allowing it to recover before resuming normal operation.
|
|
7
|
+
|
|
8
|
+
The implementation supports configurable failure thresholds, recovery
|
|
9
|
+
timeouts, and exception types, making it suitable for various HTTP
|
|
10
|
+
client scenarios.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import Any, Callable, Type
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CircuitBreakerState:
|
|
21
|
+
"""Constants representing the possible states of a circuit breaker.
|
|
22
|
+
|
|
23
|
+
The circuit breaker operates in three states:
|
|
24
|
+
- CLOSED: Normal operation, requests are allowed
|
|
25
|
+
- OPEN: Circuit is open, requests are blocked
|
|
26
|
+
- HALF_OPEN: Testing if service has recovered
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
CLOSED = "closed"
|
|
30
|
+
OPEN = "open"
|
|
31
|
+
HALF_OPEN = "half_open"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CircuitBreaker:
|
|
35
|
+
"""Circuit breaker decorator for async functions.
|
|
36
|
+
|
|
37
|
+
This class implements the circuit breaker pattern as a decorator
|
|
38
|
+
that can be applied to async functions. It monitors failures and
|
|
39
|
+
automatically opens the circuit when the failure threshold is
|
|
40
|
+
reached, preventing further requests until the recovery timeout
|
|
41
|
+
has elapsed.
|
|
42
|
+
|
|
43
|
+
The circuit breaker supports custom failure thresholds, recovery
|
|
44
|
+
timeouts, and exception types to handle different failure scenarios.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
failure_threshold: int = 5,
|
|
50
|
+
recovery_timeout: float = 60.0,
|
|
51
|
+
expected_exception: Type[Exception] = httpx.RequestError,
|
|
52
|
+
):
|
|
53
|
+
"""Initialize the circuit breaker with configuration.
|
|
54
|
+
|
|
55
|
+
:param failure_threshold: Number of consecutive failures before
|
|
56
|
+
opening the circuit
|
|
57
|
+
:type failure_threshold: int
|
|
58
|
+
:param recovery_timeout: Time in seconds to wait before attempting
|
|
59
|
+
to reset the circuit
|
|
60
|
+
:type recovery_timeout: float
|
|
61
|
+
:param expected_exception: Exception type to monitor for failures
|
|
62
|
+
:type expected_exception: Type[Exception]
|
|
63
|
+
"""
|
|
64
|
+
self.failure_threshold = failure_threshold
|
|
65
|
+
self.recovery_timeout = recovery_timeout
|
|
66
|
+
self.expected_exception = expected_exception
|
|
67
|
+
self.failure_count = 0
|
|
68
|
+
self.last_failure_time = None
|
|
69
|
+
self.state = CircuitBreakerState.CLOSED
|
|
70
|
+
|
|
71
|
+
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
72
|
+
"""Apply the circuit breaker pattern to the decorated function.
|
|
73
|
+
|
|
74
|
+
:param func: The async function to wrap with circuit breaker logic
|
|
75
|
+
:type func: Callable[..., Any]
|
|
76
|
+
:return: Wrapped function with circuit breaker behavior
|
|
77
|
+
:rtype: Callable[..., Any]
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@wraps(func)
|
|
81
|
+
async def wrapper(*args, **kwargs):
|
|
82
|
+
if self.state == CircuitBreakerState.OPEN:
|
|
83
|
+
if self._should_attempt_reset():
|
|
84
|
+
self.state = CircuitBreakerState.HALF_OPEN
|
|
85
|
+
else:
|
|
86
|
+
raise Exception("Circuit breaker is OPEN")
|
|
87
|
+
try:
|
|
88
|
+
result = await func(*args, **kwargs)
|
|
89
|
+
self._on_success()
|
|
90
|
+
return result
|
|
91
|
+
except self.expected_exception:
|
|
92
|
+
self._on_failure()
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
return wrapper
|
|
96
|
+
|
|
97
|
+
def _should_attempt_reset(self) -> bool:
|
|
98
|
+
"""Determine if enough time has passed to attempt resetting the circuit.
|
|
99
|
+
|
|
100
|
+
:return: True if recovery timeout has elapsed, False otherwise
|
|
101
|
+
:rtype: bool
|
|
102
|
+
"""
|
|
103
|
+
return (
|
|
104
|
+
self.last_failure_time is not None
|
|
105
|
+
and asyncio.get_event_loop().time() - self.last_failure_time
|
|
106
|
+
>= self.recovery_timeout
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _on_success(self):
|
|
110
|
+
"""Handle successful function execution.
|
|
111
|
+
|
|
112
|
+
Resets the failure count and closes the circuit, allowing
|
|
113
|
+
normal operation to resume.
|
|
114
|
+
"""
|
|
115
|
+
self.failure_count = 0
|
|
116
|
+
self.state = CircuitBreakerState.CLOSED
|
|
117
|
+
|
|
118
|
+
def _on_failure(self):
|
|
119
|
+
"""Handle function execution failure.
|
|
120
|
+
|
|
121
|
+
Increments the failure count and records the failure time.
|
|
122
|
+
Opens the circuit if the failure threshold is reached.
|
|
123
|
+
"""
|
|
124
|
+
self.failure_count += 1
|
|
125
|
+
self.last_failure_time = asyncio.get_event_loop().time()
|
|
126
|
+
if self.failure_count >= self.failure_threshold:
|
|
127
|
+
self.state = CircuitBreakerState.OPEN
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""HTTP client manager with connection pooling and lifecycle management.
|
|
2
|
+
|
|
3
|
+
This module provides a singleton HTTP client manager that handles
|
|
4
|
+
creation, caching, and lifecycle management of HTTP clients. It
|
|
5
|
+
implements connection pooling, configurable timeouts and limits,
|
|
6
|
+
and supports both HTTP/1.1 and HTTP/2 protocols.
|
|
7
|
+
|
|
8
|
+
The manager ensures efficient resource usage by reusing clients
|
|
9
|
+
with matching configurations and provides centralized cleanup
|
|
10
|
+
for all managed HTTP clients.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any, Dict, Optional, Type
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HTTPClientManager:
|
|
24
|
+
"""Manages shared HTTP clients with connection pooling.
|
|
25
|
+
|
|
26
|
+
This singleton class manages the lifecycle of HTTP clients,
|
|
27
|
+
providing connection pooling, configurable timeouts and limits,
|
|
28
|
+
and automatic cleanup. It caches clients based on configuration
|
|
29
|
+
parameters to avoid creating duplicate clients with the same settings.
|
|
30
|
+
|
|
31
|
+
The manager supports both managed clients (created internally)
|
|
32
|
+
and external clients (registered for cleanup tracking).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_instance: Optional["HTTPClientManager"] = None
|
|
36
|
+
_lock = asyncio.Lock()
|
|
37
|
+
_external_clients: set = set()
|
|
38
|
+
|
|
39
|
+
def __new__(cls):
|
|
40
|
+
"""Ensure singleton pattern - only one instance exists.
|
|
41
|
+
|
|
42
|
+
:return: The single instance of HTTPClientManager
|
|
43
|
+
:rtype: HTTPClientManager
|
|
44
|
+
"""
|
|
45
|
+
if cls._instance is None:
|
|
46
|
+
cls._instance = super().__new__(cls)
|
|
47
|
+
return cls._instance
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
"""Initialize the HTTP client manager.
|
|
51
|
+
|
|
52
|
+
Sets up default timeout and connection limit configurations,
|
|
53
|
+
initializes internal storage for clients, and sets up
|
|
54
|
+
state tracking for cleanup operations.
|
|
55
|
+
"""
|
|
56
|
+
if not hasattr(self, "_initialized"):
|
|
57
|
+
self._clients: Dict[str, httpx.AsyncClient] = {}
|
|
58
|
+
self._default_timeout = httpx.Timeout(
|
|
59
|
+
connect=5.0, read=30.0, write=10.0, pool=5.0
|
|
60
|
+
)
|
|
61
|
+
self._default_limits = httpx.Limits(
|
|
62
|
+
max_keepalive_connections=10,
|
|
63
|
+
max_connections=20,
|
|
64
|
+
keepalive_expiry=30.0,
|
|
65
|
+
)
|
|
66
|
+
self._initialized = True
|
|
67
|
+
self._is_closing = False
|
|
68
|
+
|
|
69
|
+
async def get_client(
|
|
70
|
+
self,
|
|
71
|
+
base_url: Optional[str] = None,
|
|
72
|
+
timeout: Optional[httpx.Timeout] = None,
|
|
73
|
+
limits: Optional[httpx.Limits] = None,
|
|
74
|
+
client_class: Optional[Type[httpx.AsyncClient]] = None,
|
|
75
|
+
**kwargs,
|
|
76
|
+
) -> httpx.AsyncClient:
|
|
77
|
+
"""Get or create an HTTP client for the given configuration.
|
|
78
|
+
|
|
79
|
+
Retrieves a cached client if one exists with matching
|
|
80
|
+
configuration, or creates a new client if needed. The client
|
|
81
|
+
is cached based on a combination of base_url, timeout, limits,
|
|
82
|
+
HTTP version, redirect following settings, and client class.
|
|
83
|
+
|
|
84
|
+
:param base_url: Optional base URL for the client
|
|
85
|
+
:type base_url: Optional[str]
|
|
86
|
+
:param timeout: Optional custom timeout configuration
|
|
87
|
+
:type timeout: Optional[httpx.Timeout]
|
|
88
|
+
:param limits: Optional custom connection limits
|
|
89
|
+
:type limits: Optional[httpx.Limits]
|
|
90
|
+
:param client_class: Optional custom client class (e.g., AuthenticatedClient)
|
|
91
|
+
:type client_class: Optional[Type[httpx.AsyncClient]]
|
|
92
|
+
:param **kwargs: Additional client configuration options
|
|
93
|
+
:return: Configured HTTP client instance
|
|
94
|
+
:rtype: httpx.AsyncClient
|
|
95
|
+
"""
|
|
96
|
+
http2_flag = kwargs.get("http2")
|
|
97
|
+
if http2_flag is None:
|
|
98
|
+
http2_flag = os.getenv("HTTP_ENABLE_HTTP2", "false").lower() == "true"
|
|
99
|
+
if http2_flag:
|
|
100
|
+
try:
|
|
101
|
+
import h2 # type: ignore # noqa: F401
|
|
102
|
+
except Exception:
|
|
103
|
+
logger.warning(
|
|
104
|
+
"HTTP/2 requested but 'h2' package not installed; falling back to HTTP/1.1"
|
|
105
|
+
)
|
|
106
|
+
http2_flag = False
|
|
107
|
+
follow = kwargs.get("follow_redirects", True)
|
|
108
|
+
|
|
109
|
+
def timeout_key(t: Optional[httpx.Timeout]):
|
|
110
|
+
if not t:
|
|
111
|
+
return None
|
|
112
|
+
return (t.connect, t.read, t.write, t.pool)
|
|
113
|
+
|
|
114
|
+
def limits_key(limits_obj: Optional[httpx.Limits]):
|
|
115
|
+
if not limits_obj:
|
|
116
|
+
return None
|
|
117
|
+
return (
|
|
118
|
+
limits_obj.max_keepalive_connections,
|
|
119
|
+
limits_obj.max_connections,
|
|
120
|
+
limits_obj.keepalive_expiry,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
t_key = timeout_key(timeout)
|
|
124
|
+
l_key = limits_key(limits)
|
|
125
|
+
class_name = (client_class or httpx.AsyncClient).__name__
|
|
126
|
+
cache_key = str(
|
|
127
|
+
(
|
|
128
|
+
base_url or "default",
|
|
129
|
+
t_key,
|
|
130
|
+
l_key,
|
|
131
|
+
http2_flag,
|
|
132
|
+
follow,
|
|
133
|
+
class_name,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if cache_key not in self._clients:
|
|
138
|
+
async with self._lock:
|
|
139
|
+
if cache_key not in self._clients:
|
|
140
|
+
client_config: Dict[str, Any] = {
|
|
141
|
+
"timeout": timeout or self._default_timeout,
|
|
142
|
+
"limits": limits or self._default_limits,
|
|
143
|
+
"http2": http2_flag,
|
|
144
|
+
"follow_redirects": follow,
|
|
145
|
+
**kwargs,
|
|
146
|
+
}
|
|
147
|
+
if base_url:
|
|
148
|
+
client_config["base_url"] = base_url
|
|
149
|
+
|
|
150
|
+
# Use the specified client class or default to httpx.AsyncClient
|
|
151
|
+
actual_client_class = client_class or httpx.AsyncClient
|
|
152
|
+
self._clients[cache_key] = actual_client_class(**client_config)
|
|
153
|
+
logger.debug(
|
|
154
|
+
"Created new %s client for %s",
|
|
155
|
+
actual_client_class.__name__,
|
|
156
|
+
cache_key,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return self._clients[cache_key]
|
|
160
|
+
|
|
161
|
+
def register_external_client(self, client: httpx.AsyncClient) -> None:
|
|
162
|
+
"""Register an external HTTP client for cleanup tracking.
|
|
163
|
+
|
|
164
|
+
Adds an externally created HTTP client to the manager's
|
|
165
|
+
tracking system so it can be properly closed during cleanup.
|
|
166
|
+
|
|
167
|
+
:param client: External HTTP client to track
|
|
168
|
+
:type client: httpx.AsyncClient
|
|
169
|
+
"""
|
|
170
|
+
self._external_clients.add(client)
|
|
171
|
+
logger.debug("Registered external client for cleanup tracking")
|
|
172
|
+
|
|
173
|
+
async def close_all(self):
|
|
174
|
+
"""Close all managed and external HTTP clients.
|
|
175
|
+
|
|
176
|
+
Safely closes all HTTP clients managed by this instance,
|
|
177
|
+
including both internally created clients and externally
|
|
178
|
+
registered ones. Implements duplicate call protection and
|
|
179
|
+
comprehensive error handling during cleanup.
|
|
180
|
+
"""
|
|
181
|
+
if self._is_closing:
|
|
182
|
+
logger.debug("Already closing HTTP clients, skipping duplicate call")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
self._is_closing = True
|
|
186
|
+
try:
|
|
187
|
+
total_clients = len(self._clients) + len(self._external_clients)
|
|
188
|
+
if total_clients == 0:
|
|
189
|
+
logger.debug("No HTTP clients to close")
|
|
190
|
+
return
|
|
191
|
+
logger.info("Closing %d HTTP client(s)...", total_clients)
|
|
192
|
+
for cache_key, client in list(self._clients.items()):
|
|
193
|
+
try:
|
|
194
|
+
await client.aclose()
|
|
195
|
+
logger.debug("Closed managed HTTP client: %s", cache_key)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning(
|
|
198
|
+
"Error closing managed HTTP client %s: %s",
|
|
199
|
+
cache_key,
|
|
200
|
+
e,
|
|
201
|
+
)
|
|
202
|
+
for client in list(self._external_clients):
|
|
203
|
+
try:
|
|
204
|
+
await client.aclose()
|
|
205
|
+
logger.debug("Closed external HTTP client")
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning("Error closing external HTTP client: %s", e)
|
|
208
|
+
self._clients.clear()
|
|
209
|
+
self._external_clients.clear()
|
|
210
|
+
logger.info("All HTTP clients closed successfully")
|
|
211
|
+
finally:
|
|
212
|
+
self._is_closing = False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
http_client_manager = HTTPClientManager()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def get_http_client(
|
|
219
|
+
authenticated: bool = False,
|
|
220
|
+
auth_manager=None,
|
|
221
|
+
media_registry=None,
|
|
222
|
+
header_resolver=None,
|
|
223
|
+
**kwargs,
|
|
224
|
+
) -> httpx.AsyncClient:
|
|
225
|
+
"""Get an HTTP client with the specified configuration.
|
|
226
|
+
|
|
227
|
+
Convenience function that delegates to the global HTTP client
|
|
228
|
+
manager to retrieve or create an HTTP client. Optionally creates
|
|
229
|
+
an AuthenticatedClient for Amazon Ads API calls.
|
|
230
|
+
|
|
231
|
+
:param authenticated: Whether to use AuthenticatedClient
|
|
232
|
+
:type authenticated: bool
|
|
233
|
+
:param auth_manager: Authentication manager (required if authenticated=True)
|
|
234
|
+
:type auth_manager: Optional[AuthManager]
|
|
235
|
+
:param media_registry: Media type registry for content negotiation
|
|
236
|
+
:type media_registry: Optional[MediaTypeRegistry]
|
|
237
|
+
:param header_resolver: Header name resolver
|
|
238
|
+
:type header_resolver: Optional[HeaderNameResolver]
|
|
239
|
+
:param **kwargs: Client configuration parameters
|
|
240
|
+
:return: Configured HTTP client instance
|
|
241
|
+
:rtype: httpx.AsyncClient
|
|
242
|
+
"""
|
|
243
|
+
if authenticated:
|
|
244
|
+
# Import here to avoid circular dependency
|
|
245
|
+
from ..http_client import AuthenticatedClient
|
|
246
|
+
|
|
247
|
+
# Extract standard httpx client params
|
|
248
|
+
httpx_kwargs = {
|
|
249
|
+
k: v
|
|
250
|
+
for k, v in kwargs.items()
|
|
251
|
+
if k not in ["auth_manager", "media_registry", "header_resolver"]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Add auth-specific params to kwargs for the AuthenticatedClient constructor
|
|
255
|
+
httpx_kwargs["auth_manager"] = auth_manager
|
|
256
|
+
httpx_kwargs["media_registry"] = media_registry
|
|
257
|
+
httpx_kwargs["header_resolver"] = header_resolver
|
|
258
|
+
|
|
259
|
+
return await http_client_manager.get_client(
|
|
260
|
+
client_class=AuthenticatedClient, **httpx_kwargs
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return await http_client_manager.get_client(**kwargs)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def create_timeout(
|
|
267
|
+
connect: float = 5.0,
|
|
268
|
+
read: float = 30.0,
|
|
269
|
+
write: float = 10.0,
|
|
270
|
+
pool: float = 5.0,
|
|
271
|
+
) -> httpx.Timeout:
|
|
272
|
+
"""Create a timeout configuration object.
|
|
273
|
+
|
|
274
|
+
:param connect: Connection timeout in seconds
|
|
275
|
+
:type connect: float
|
|
276
|
+
:param read: Read timeout in seconds
|
|
277
|
+
:type read: float
|
|
278
|
+
:param write: Write timeout in seconds
|
|
279
|
+
:type write: float
|
|
280
|
+
:param pool: Pool timeout in seconds
|
|
281
|
+
:type pool: float
|
|
282
|
+
:return: Configured timeout object
|
|
283
|
+
:rtype: httpx.Timeout
|
|
284
|
+
"""
|
|
285
|
+
return httpx.Timeout(connect=connect, read=read, write=write, pool=pool)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def create_limits(
|
|
289
|
+
max_keepalive_connections: int = 10,
|
|
290
|
+
max_connections: int = 20,
|
|
291
|
+
keepalive_expiry: float = 30.0,
|
|
292
|
+
) -> httpx.Limits:
|
|
293
|
+
"""Create a connection limits configuration object.
|
|
294
|
+
|
|
295
|
+
:param max_keepalive_connections: Maximum number of keepalive connections
|
|
296
|
+
:type max_keepalive_connections: int
|
|
297
|
+
:param max_connections: Maximum total number of connections
|
|
298
|
+
:type max_connections: int
|
|
299
|
+
:param keepalive_expiry: Keepalive connection expiry time in seconds
|
|
300
|
+
:type keepalive_expiry: float
|
|
301
|
+
:return: Configured limits object
|
|
302
|
+
:rtype: httpx.Limits
|
|
303
|
+
"""
|
|
304
|
+
return httpx.Limits(
|
|
305
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
306
|
+
max_connections=max_connections,
|
|
307
|
+
keepalive_expiry=keepalive_expiry,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def health_check(url: str, timeout: float = 5.0) -> bool:
|
|
312
|
+
"""Perform a health check on a URL.
|
|
313
|
+
|
|
314
|
+
Makes a simple GET request to the specified URL to check if
|
|
315
|
+
the service is responding with a successful status code.
|
|
316
|
+
|
|
317
|
+
:param url: URL to perform health check on
|
|
318
|
+
:type url: str
|
|
319
|
+
:param timeout: Timeout for the health check request in seconds
|
|
320
|
+
:type timeout: float
|
|
321
|
+
:return: True if health check passes, False otherwise
|
|
322
|
+
:rtype: bool
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
326
|
+
r = await client.get(url)
|
|
327
|
+
return 200 <= r.status_code < 300
|
|
328
|
+
except Exception:
|
|
329
|
+
return False
|