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,21 @@
|
|
|
1
|
+
"""Media utilities public API (re-exports)."""
|
|
2
|
+
|
|
3
|
+
from .negotiator import (
|
|
4
|
+
EnhancedMediaTypeRegistry,
|
|
5
|
+
ResourceTypeNegotiator,
|
|
6
|
+
create_enhanced_registry,
|
|
7
|
+
)
|
|
8
|
+
from .types import (
|
|
9
|
+
MediaTypeRegistry,
|
|
10
|
+
build_media_maps_from_spec,
|
|
11
|
+
split_method_path_key,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MediaTypeRegistry",
|
|
16
|
+
"split_method_path_key",
|
|
17
|
+
"build_media_maps_from_spec",
|
|
18
|
+
"ResourceTypeNegotiator",
|
|
19
|
+
"EnhancedMediaTypeRegistry",
|
|
20
|
+
"create_enhanced_registry",
|
|
21
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Media type negotiation helpers.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for negotiating media types based on
|
|
4
|
+
resource types and URL patterns. It includes a resource type negotiator
|
|
5
|
+
that can determine appropriate content types for specific resources,
|
|
6
|
+
particularly for export operations where the content type depends on
|
|
7
|
+
the export ID and resource type.
|
|
8
|
+
|
|
9
|
+
The module also provides an enhanced media type registry that combines
|
|
10
|
+
base registry functionality with negotiation capabilities.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from typing import Callable, Dict, List, Optional
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
def _decode_export_id(export_id: str) -> Optional[str]:
|
|
22
|
+
pad_len = (-len(export_id)) % 4
|
|
23
|
+
padded = export_id + ("=" * pad_len)
|
|
24
|
+
for decoder in (base64.urlsafe_b64decode, base64.b64decode):
|
|
25
|
+
try:
|
|
26
|
+
return decoder(padded).decode("utf-8")
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ResourceTypeNegotiator:
|
|
33
|
+
"""Negotiator for determining media types based on resource types.
|
|
34
|
+
|
|
35
|
+
This class provides a framework for negotiating appropriate media
|
|
36
|
+
types based on the resource type extracted from URLs. It supports
|
|
37
|
+
registering custom negotiators for different resource types and
|
|
38
|
+
includes built-in negotiation logic for export operations.
|
|
39
|
+
|
|
40
|
+
The negotiator extracts resource types from URL paths and applies
|
|
41
|
+
type-specific logic to determine the most appropriate content type
|
|
42
|
+
from available options.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
"""Initialize the resource type negotiator.
|
|
47
|
+
|
|
48
|
+
Sets up internal storage for negotiator functions and registers
|
|
49
|
+
the default negotiator for export operations.
|
|
50
|
+
"""
|
|
51
|
+
self._negotiators: Dict[
|
|
52
|
+
str, Callable[[str, str, List[str]], Optional[str]]
|
|
53
|
+
] = {}
|
|
54
|
+
self._register_default_negotiators()
|
|
55
|
+
|
|
56
|
+
def _register_default_negotiators(self):
|
|
57
|
+
"""Register the default negotiator for export operations.
|
|
58
|
+
|
|
59
|
+
Sets up the built-in negotiator for handling export resource
|
|
60
|
+
types, which determines content types based on export ID suffixes.
|
|
61
|
+
"""
|
|
62
|
+
self.register_negotiator("exports", self._negotiate_exports)
|
|
63
|
+
|
|
64
|
+
def register_negotiator(
|
|
65
|
+
self,
|
|
66
|
+
resource_type: str,
|
|
67
|
+
negotiator: Callable[[str, str, List[str]], Optional[str]],
|
|
68
|
+
):
|
|
69
|
+
"""Register a custom negotiator function for a resource type.
|
|
70
|
+
|
|
71
|
+
:param resource_type: The resource type to register the negotiator for
|
|
72
|
+
:type resource_type: str
|
|
73
|
+
:param negotiator: Function that implements negotiation logic for the
|
|
74
|
+
resource type
|
|
75
|
+
:type negotiator: Callable[[str, str, List[str]], Optional[str]]
|
|
76
|
+
"""
|
|
77
|
+
self._negotiators[resource_type.lower()] = negotiator
|
|
78
|
+
logger.debug("Registered negotiator for resource type: %s", resource_type)
|
|
79
|
+
|
|
80
|
+
def negotiate(
|
|
81
|
+
self, method: str, url: str, available_types: List[str]
|
|
82
|
+
) -> Optional[str]:
|
|
83
|
+
"""Negotiate the appropriate media type for a request.
|
|
84
|
+
|
|
85
|
+
Extracts the resource type from the URL and applies the appropriate
|
|
86
|
+
negotiator to determine the best content type from the available
|
|
87
|
+
options. Falls back gracefully if negotiation fails.
|
|
88
|
+
|
|
89
|
+
:param method: HTTP method of the request
|
|
90
|
+
:type method: str
|
|
91
|
+
:param url: URL to negotiate media type for
|
|
92
|
+
:type url: str
|
|
93
|
+
:param available_types: List of available content types to choose from
|
|
94
|
+
:type available_types: List[str]
|
|
95
|
+
:return: Negotiated content type or None if negotiation fails
|
|
96
|
+
:rtype: Optional[str]
|
|
97
|
+
"""
|
|
98
|
+
resource_type = self._extract_resource_type(url)
|
|
99
|
+
if resource_type and resource_type in self._negotiators:
|
|
100
|
+
try:
|
|
101
|
+
result = self._negotiators[resource_type](method, url, available_types)
|
|
102
|
+
if result:
|
|
103
|
+
logger.debug("Negotiated %s for %s resource", result, resource_type)
|
|
104
|
+
return result
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning("Negotiator failed for %s: %s", resource_type, e)
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def _extract_resource_type(self, url: str) -> Optional[str]:
|
|
110
|
+
"""Extract the resource type from a URL path.
|
|
111
|
+
|
|
112
|
+
Parses the URL path to identify the resource type, handling
|
|
113
|
+
version prefixes and normalizing the result.
|
|
114
|
+
|
|
115
|
+
:param url: URL to extract resource type from
|
|
116
|
+
:type url: str
|
|
117
|
+
:return: Resource type string or None if extraction fails
|
|
118
|
+
:rtype: Optional[str]
|
|
119
|
+
"""
|
|
120
|
+
path = urlparse(url).path
|
|
121
|
+
path = re.sub(r"^/v\d+/", "/", path)
|
|
122
|
+
match = re.match(r"^/([^/]+)", path)
|
|
123
|
+
if match:
|
|
124
|
+
return match.group(1).lower()
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def _negotiate_exports(
|
|
128
|
+
self, method: str, url: str, available_types: List[str]
|
|
129
|
+
) -> Optional[str]:
|
|
130
|
+
"""Negotiate media type for export operations.
|
|
131
|
+
|
|
132
|
+
This negotiator handles export-specific media type determination
|
|
133
|
+
by decoding the export ID from the URL and mapping suffix codes
|
|
134
|
+
to appropriate content types. It only processes GET requests
|
|
135
|
+
and expects URLs in the format /exports/{export_id}.
|
|
136
|
+
|
|
137
|
+
:param method: HTTP method of the request
|
|
138
|
+
:type method: str
|
|
139
|
+
:param url: URL containing the export ID
|
|
140
|
+
:type url: str
|
|
141
|
+
:param available_types: List of available content types
|
|
142
|
+
:type available_types: List[str]
|
|
143
|
+
:return: Appropriate content type for the export or None if
|
|
144
|
+
negotiation fails
|
|
145
|
+
:rtype: Optional[str]
|
|
146
|
+
"""
|
|
147
|
+
if method.upper() != "GET":
|
|
148
|
+
return None
|
|
149
|
+
m = re.search(r"/exports/([^/?]+)", url)
|
|
150
|
+
if not m:
|
|
151
|
+
return None
|
|
152
|
+
export_id = m.group(1)
|
|
153
|
+
try:
|
|
154
|
+
decoded = _decode_export_id(export_id)
|
|
155
|
+
if not decoded:
|
|
156
|
+
return None
|
|
157
|
+
if "," in decoded:
|
|
158
|
+
_, suffix = decoded.rsplit(",", 1)
|
|
159
|
+
suffix_map = {
|
|
160
|
+
"C": "application/vnd.campaignsexport.v1+json",
|
|
161
|
+
"A": "application/vnd.adgroupsexport.v1+json",
|
|
162
|
+
"AD": "application/vnd.adsexport.v1+json",
|
|
163
|
+
# Some export IDs use ',R' for ads exports.
|
|
164
|
+
"R": "application/vnd.adsexport.v1+json",
|
|
165
|
+
"T": "application/vnd.targetsexport.v1+json",
|
|
166
|
+
}
|
|
167
|
+
ct = suffix_map.get(suffix.upper())
|
|
168
|
+
if ct and ct in available_types:
|
|
169
|
+
return ct
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class EnhancedMediaTypeRegistry:
|
|
176
|
+
"""Enhanced media type registry with negotiation capabilities.
|
|
177
|
+
|
|
178
|
+
This class extends a base media type registry with negotiation
|
|
179
|
+
functionality. It can resolve media types using the base registry
|
|
180
|
+
and then apply negotiation logic when multiple content types are
|
|
181
|
+
available to select the most appropriate one.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, base_registry):
|
|
185
|
+
"""Initialize the enhanced registry with a base registry.
|
|
186
|
+
|
|
187
|
+
:param base_registry: Base media type registry to extend
|
|
188
|
+
:type base_registry: Any
|
|
189
|
+
"""
|
|
190
|
+
self.base_registry = base_registry
|
|
191
|
+
self.negotiator = ResourceTypeNegotiator()
|
|
192
|
+
|
|
193
|
+
def resolve(self, method: str, url: str):
|
|
194
|
+
"""Resolve media types with optional negotiation.
|
|
195
|
+
|
|
196
|
+
First attempts to resolve media types using the base registry.
|
|
197
|
+
If multiple response content types are available, applies
|
|
198
|
+
negotiation to select the most appropriate one.
|
|
199
|
+
|
|
200
|
+
:param method: HTTP method of the request
|
|
201
|
+
:type method: str
|
|
202
|
+
:param url: URL to resolve media types for
|
|
203
|
+
:type url: str
|
|
204
|
+
:return: Tuple of (request_content_type, response_content_types)
|
|
205
|
+
:rtype: tuple
|
|
206
|
+
"""
|
|
207
|
+
content_type, accepts = self.base_registry.resolve(method, url)
|
|
208
|
+
if accepts and len(accepts) > 1:
|
|
209
|
+
negotiated = self.negotiator.negotiate(method, url, accepts)
|
|
210
|
+
if negotiated:
|
|
211
|
+
return content_type, [negotiated]
|
|
212
|
+
return content_type, accepts
|
|
213
|
+
|
|
214
|
+
def add_negotiator(self, resource_type: str, negotiator: Callable):
|
|
215
|
+
"""Add a custom negotiator for a specific resource type.
|
|
216
|
+
|
|
217
|
+
:param resource_type: Resource type to add negotiator for
|
|
218
|
+
:type resource_type: str
|
|
219
|
+
:param negotiator: Negotiation function to register
|
|
220
|
+
:type negotiator: Callable
|
|
221
|
+
"""
|
|
222
|
+
self.negotiator.register_negotiator(resource_type, negotiator)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def create_enhanced_registry(base_registry) -> EnhancedMediaTypeRegistry:
|
|
226
|
+
"""Create an enhanced media type registry.
|
|
227
|
+
|
|
228
|
+
Factory function that creates an EnhancedMediaTypeRegistry instance
|
|
229
|
+
with the provided base registry.
|
|
230
|
+
|
|
231
|
+
:param base_registry: Base media type registry to enhance
|
|
232
|
+
:type base_registry: Any
|
|
233
|
+
:return: Enhanced media type registry instance
|
|
234
|
+
:rtype: EnhancedMediaTypeRegistry
|
|
235
|
+
"""
|
|
236
|
+
return EnhancedMediaTypeRegistry(base_registry)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = [
|
|
240
|
+
"ResourceTypeNegotiator",
|
|
241
|
+
"EnhancedMediaTypeRegistry",
|
|
242
|
+
"create_enhanced_registry",
|
|
243
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Media type registry and resolution for OpenAPI specifications.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for managing and resolving media types
|
|
4
|
+
from OpenAPI specifications and sidecar files. It includes a registry
|
|
5
|
+
class that can build media type mappings from OpenAPI specs and resolve
|
|
6
|
+
content types for specific HTTP methods and URL paths.
|
|
7
|
+
|
|
8
|
+
The module handles both request and response media types, supports
|
|
9
|
+
templated paths, and provides caching for performance optimization.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
from amazon_ads_mcp.utils.openapi import deref, oai_template_to_regex
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MediaTypeRegistry:
|
|
20
|
+
"""Registry for managing media types from OpenAPI specs and sidecars.
|
|
21
|
+
|
|
22
|
+
This class maintains a registry of media types for both requests and
|
|
23
|
+
responses, extracted from OpenAPI specifications and sidecar files.
|
|
24
|
+
It provides methods to add media type mappings and resolve the
|
|
25
|
+
appropriate content types for specific HTTP methods and URL paths.
|
|
26
|
+
|
|
27
|
+
The registry supports templated paths and includes caching for
|
|
28
|
+
improved performance when resolving media types.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
"""Initialize the media type registry.
|
|
33
|
+
|
|
34
|
+
Sets up internal storage for request entries, response entries,
|
|
35
|
+
and a cache for resolved media type lookups.
|
|
36
|
+
"""
|
|
37
|
+
self._req_entries: List[Dict[Tuple[str, str], str]] = []
|
|
38
|
+
self._resp_entries: List[Dict[Tuple[str, str], List[str]]] = []
|
|
39
|
+
self._cache: Dict[
|
|
40
|
+
Tuple[str, str], Tuple[Optional[str], Optional[List[str]]]
|
|
41
|
+
] = {}
|
|
42
|
+
|
|
43
|
+
def add_from_spec(self, spec: dict) -> None:
|
|
44
|
+
"""Add media type mappings from an OpenAPI specification.
|
|
45
|
+
|
|
46
|
+
Extracts request and response media types from the provided
|
|
47
|
+
OpenAPI specification and adds them to the registry. Clears
|
|
48
|
+
the internal cache to ensure fresh lookups.
|
|
49
|
+
|
|
50
|
+
:param spec: OpenAPI specification dictionary
|
|
51
|
+
:type spec: dict
|
|
52
|
+
"""
|
|
53
|
+
req_map, resp_map = build_media_maps_from_spec(spec)
|
|
54
|
+
self._req_entries.append(req_map)
|
|
55
|
+
self._resp_entries.append(resp_map)
|
|
56
|
+
self._cache.clear()
|
|
57
|
+
|
|
58
|
+
def add_from_sidecar(self, sidecar: dict) -> None:
|
|
59
|
+
"""Add media type mappings from a sidecar configuration file.
|
|
60
|
+
|
|
61
|
+
Processes sidecar configuration to extract request and response
|
|
62
|
+
media type mappings. The sidecar should contain 'requests' and
|
|
63
|
+
'responses' sections with method+path keys and media type values.
|
|
64
|
+
|
|
65
|
+
:param sidecar: Sidecar configuration dictionary
|
|
66
|
+
:type sidecar: dict
|
|
67
|
+
"""
|
|
68
|
+
req_map: Dict[Tuple[str, str], str] = {}
|
|
69
|
+
resp_map: Dict[Tuple[str, str], List[str]] = {}
|
|
70
|
+
for k, v in (sidecar.get("requests") or {}).items():
|
|
71
|
+
m, p = split_method_path_key(k)
|
|
72
|
+
if m and p and isinstance(v, str):
|
|
73
|
+
req_map[(m, p)] = v
|
|
74
|
+
for k, v in (sidecar.get("responses") or {}).items():
|
|
75
|
+
m, p = split_method_path_key(k)
|
|
76
|
+
if m and p and isinstance(v, list):
|
|
77
|
+
resp_map[(m, p)] = list(v)
|
|
78
|
+
if req_map or resp_map:
|
|
79
|
+
self._req_entries.append(req_map)
|
|
80
|
+
self._resp_entries.append(resp_map)
|
|
81
|
+
self._cache.clear()
|
|
82
|
+
|
|
83
|
+
def resolve(
|
|
84
|
+
self, method: str, url: str
|
|
85
|
+
) -> Tuple[Optional[str], Optional[List[str]]]:
|
|
86
|
+
"""Resolve media types for a specific HTTP method and URL.
|
|
87
|
+
|
|
88
|
+
Attempts to find the appropriate request and response media
|
|
89
|
+
types for the given method and URL. First checks for exact
|
|
90
|
+
matches, then falls back to templated path matching. Results
|
|
91
|
+
are cached for subsequent lookups.
|
|
92
|
+
|
|
93
|
+
:param method: HTTP method (e.g., 'GET', 'POST')
|
|
94
|
+
:type method: str
|
|
95
|
+
:param url: URL to resolve media types for
|
|
96
|
+
:type url: str
|
|
97
|
+
:return: Tuple of (request_media_type, response_media_types)
|
|
98
|
+
:rtype: Tuple[Optional[str], Optional[List[str]]]
|
|
99
|
+
"""
|
|
100
|
+
m = (method or "get").lower()
|
|
101
|
+
path = (urlparse(url).path or "/").rstrip("/") or "/"
|
|
102
|
+
cache_key = (m, path)
|
|
103
|
+
if cache_key in self._cache:
|
|
104
|
+
return self._cache[cache_key]
|
|
105
|
+
for req_map, resp_map in zip(self._req_entries, self._resp_entries):
|
|
106
|
+
if (m, path) in req_map or (m, path) in resp_map:
|
|
107
|
+
result = (req_map.get((m, path)), resp_map.get((m, path)))
|
|
108
|
+
self._cache[cache_key] = result
|
|
109
|
+
return result
|
|
110
|
+
for req_map, resp_map in zip(self._req_entries, self._resp_entries):
|
|
111
|
+
keys: Set[Tuple[str, str]] = set(req_map.keys()) | set(resp_map.keys())
|
|
112
|
+
for mm, templated in keys:
|
|
113
|
+
if mm != m:
|
|
114
|
+
continue
|
|
115
|
+
if re.match(oai_template_to_regex(templated), path):
|
|
116
|
+
result = (
|
|
117
|
+
req_map.get((mm, templated)),
|
|
118
|
+
resp_map.get((mm, templated)),
|
|
119
|
+
)
|
|
120
|
+
self._cache[cache_key] = result
|
|
121
|
+
return result
|
|
122
|
+
result = (None, None)
|
|
123
|
+
self._cache[cache_key] = result
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def split_method_path_key(key: str) -> Tuple[Optional[str], Optional[str]]:
|
|
128
|
+
"""Split a method+path key into separate method and path components.
|
|
129
|
+
|
|
130
|
+
Parses keys in the format "METHOD /path" (e.g., "GET /users/{id}")
|
|
131
|
+
and returns the method and path as separate components. Handles
|
|
132
|
+
path normalization including leading slash addition and trailing
|
|
133
|
+
slash removal.
|
|
134
|
+
|
|
135
|
+
:param key: Method+path key string (e.g., "POST /api/users")
|
|
136
|
+
:type key: str
|
|
137
|
+
:return: Tuple of (method, path) or (None, None) if parsing fails
|
|
138
|
+
:rtype: Tuple[Optional[str], Optional[str]]
|
|
139
|
+
"""
|
|
140
|
+
parts = (key or "").strip().split(" ", 1)
|
|
141
|
+
if len(parts) != 2:
|
|
142
|
+
return None, None
|
|
143
|
+
method = parts[0].lower()
|
|
144
|
+
path = parts[1].strip()
|
|
145
|
+
if not path.startswith("/"):
|
|
146
|
+
path = "/" + path
|
|
147
|
+
path = path.rstrip("/") or "/"
|
|
148
|
+
return method, path
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_media_maps_from_spec(
|
|
152
|
+
openapi_spec: dict,
|
|
153
|
+
) -> Tuple[Dict[Tuple[str, str], str], Dict[Tuple[str, str], List[str]]]:
|
|
154
|
+
"""Build media type mappings from an OpenAPI specification.
|
|
155
|
+
|
|
156
|
+
Extracts request and response media types from the OpenAPI spec's
|
|
157
|
+
paths section. For each operation, it identifies the content types
|
|
158
|
+
for request bodies and response content, building comprehensive
|
|
159
|
+
mappings keyed by method and path.
|
|
160
|
+
|
|
161
|
+
:param openapi_spec: OpenAPI specification dictionary
|
|
162
|
+
:type openapi_spec: dict
|
|
163
|
+
:return: Tuple of (request_media_map, response_media_map)
|
|
164
|
+
:rtype: Tuple[Dict[Tuple[str, str], str], Dict[Tuple[str, str], List[str]]]
|
|
165
|
+
"""
|
|
166
|
+
req_media: Dict[Tuple[str, str], str] = {}
|
|
167
|
+
resp_media: Dict[Tuple[str, str], List[str]] = {}
|
|
168
|
+
paths = openapi_spec.get("paths", {}) or {}
|
|
169
|
+
for raw_path, ops in paths.items():
|
|
170
|
+
if not isinstance(ops, dict):
|
|
171
|
+
continue
|
|
172
|
+
norm_path = (raw_path or "/").rstrip("/") or "/"
|
|
173
|
+
for method, op in ops.items():
|
|
174
|
+
if not isinstance(op, dict):
|
|
175
|
+
continue
|
|
176
|
+
m = method.lower()
|
|
177
|
+
rb = deref(openapi_spec, op.get("requestBody"))
|
|
178
|
+
rb_content = (rb or {}).get("content", {}) if isinstance(rb, dict) else {}
|
|
179
|
+
if isinstance(rb_content, dict) and rb_content:
|
|
180
|
+
ct = sorted(rb_content.keys())[0]
|
|
181
|
+
req_media[(m, norm_path)] = ct
|
|
182
|
+
responses = (op.get("responses") or {}) if isinstance(op, dict) else {}
|
|
183
|
+
accepts: Set[str] = set()
|
|
184
|
+
if isinstance(responses, dict):
|
|
185
|
+
for _, r in responses.items():
|
|
186
|
+
r = deref(openapi_spec, r)
|
|
187
|
+
rc = (r or {}).get("content", {}) if isinstance(r, dict) else {}
|
|
188
|
+
if isinstance(rc, dict):
|
|
189
|
+
accepts.update(rc.keys())
|
|
190
|
+
if accepts:
|
|
191
|
+
resp_media[(m, norm_path)] = sorted(accepts)
|
|
192
|
+
return req_media, resp_media
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = [
|
|
196
|
+
"MediaTypeRegistry",
|
|
197
|
+
"split_method_path_key",
|
|
198
|
+
"build_media_maps_from_spec",
|
|
199
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""OpenAPI utilities public API (re-exports).
|
|
2
|
+
|
|
3
|
+
Recommended imports:
|
|
4
|
+
from amazon_ads_mcp.utils.openapi import json_load, deref, oai_template_to_regex, OpenAPISpecLoader
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .json import json_load, oai_template_to_regex
|
|
8
|
+
from .loader import OpenAPISpecLoader
|
|
9
|
+
from .refs import deref
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"json_load",
|
|
13
|
+
"deref",
|
|
14
|
+
"oai_template_to_regex",
|
|
15
|
+
"OpenAPISpecLoader",
|
|
16
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""JSON and path helpers for OpenAPI specs.
|
|
2
|
+
|
|
3
|
+
This module provides utility functions for working with JSON files and
|
|
4
|
+
OpenAPI path templates. It includes functionality for loading JSON files
|
|
5
|
+
with proper encoding and converting OpenAPI path templates to regular
|
|
6
|
+
expressions for pattern matching.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def json_load(path: Path) -> dict:
|
|
15
|
+
"""Load JSON from a file path with UTF-8 encoding.
|
|
16
|
+
|
|
17
|
+
This function opens a JSON file at the specified path and loads
|
|
18
|
+
its contents as a Python dictionary. It ensures proper UTF-8
|
|
19
|
+
encoding handling and automatically closes the file after reading.
|
|
20
|
+
|
|
21
|
+
:param path: Path to the JSON file to load
|
|
22
|
+
:type path: Path
|
|
23
|
+
:return: Parsed JSON content as a dictionary
|
|
24
|
+
:rtype: dict
|
|
25
|
+
:raises FileNotFoundError: If the specified file path does not exist
|
|
26
|
+
:raises json.JSONDecodeError: If the file contains invalid JSON
|
|
27
|
+
"""
|
|
28
|
+
with path.open("r", encoding="utf-8") as f:
|
|
29
|
+
return json.load(f)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def oai_template_to_regex(path_template: str) -> str:
|
|
33
|
+
"""Convert OpenAPI path template to regex pattern.
|
|
34
|
+
|
|
35
|
+
This function converts OpenAPI path templates (e.g., "/users/{id}/posts")
|
|
36
|
+
to regular expression patterns that can be used for matching actual
|
|
37
|
+
URL paths. It handles parameter placeholders by converting them to
|
|
38
|
+
regex patterns that match any non-slash characters.
|
|
39
|
+
|
|
40
|
+
The function ensures the resulting regex is anchored to the start and
|
|
41
|
+
end of the string, and handles trailing slashes appropriately.
|
|
42
|
+
|
|
43
|
+
:param path_template: OpenAPI path template string (e.g., "/users/{id}")
|
|
44
|
+
:type path_template: str
|
|
45
|
+
:return: Regular expression pattern string
|
|
46
|
+
:rtype: str
|
|
47
|
+
"""
|
|
48
|
+
return (
|
|
49
|
+
"^"
|
|
50
|
+
+ re.sub(r"\{[^/]+\}", r"[^/]+", path_template.rstrip("/") or "/")
|
|
51
|
+
+ "$"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["json_load", "oai_template_to_regex"]
|