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.
Files changed (82) hide show
  1. amazon_ads_mcp/__init__.py +11 -0
  2. amazon_ads_mcp/auth/__init__.py +33 -0
  3. amazon_ads_mcp/auth/base.py +211 -0
  4. amazon_ads_mcp/auth/hooks.py +172 -0
  5. amazon_ads_mcp/auth/manager.py +791 -0
  6. amazon_ads_mcp/auth/oauth_state_store.py +277 -0
  7. amazon_ads_mcp/auth/providers/__init__.py +14 -0
  8. amazon_ads_mcp/auth/providers/direct.py +393 -0
  9. amazon_ads_mcp/auth/providers/example_auth0.py.example +216 -0
  10. amazon_ads_mcp/auth/providers/openbridge.py +512 -0
  11. amazon_ads_mcp/auth/registry.py +146 -0
  12. amazon_ads_mcp/auth/secure_token_store.py +297 -0
  13. amazon_ads_mcp/auth/token_store.py +723 -0
  14. amazon_ads_mcp/config/__init__.py +5 -0
  15. amazon_ads_mcp/config/sampling.py +111 -0
  16. amazon_ads_mcp/config/settings.py +366 -0
  17. amazon_ads_mcp/exceptions.py +314 -0
  18. amazon_ads_mcp/middleware/__init__.py +11 -0
  19. amazon_ads_mcp/middleware/authentication.py +1474 -0
  20. amazon_ads_mcp/middleware/caching.py +177 -0
  21. amazon_ads_mcp/middleware/oauth.py +175 -0
  22. amazon_ads_mcp/middleware/sampling.py +112 -0
  23. amazon_ads_mcp/models/__init__.py +320 -0
  24. amazon_ads_mcp/models/amc_models.py +837 -0
  25. amazon_ads_mcp/models/api_responses.py +847 -0
  26. amazon_ads_mcp/models/base_models.py +215 -0
  27. amazon_ads_mcp/models/builtin_responses.py +496 -0
  28. amazon_ads_mcp/models/dsp_models.py +556 -0
  29. amazon_ads_mcp/models/stores_brands.py +610 -0
  30. amazon_ads_mcp/server/__init__.py +6 -0
  31. amazon_ads_mcp/server/__main__.py +6 -0
  32. amazon_ads_mcp/server/builtin_prompts.py +269 -0
  33. amazon_ads_mcp/server/builtin_tools.py +962 -0
  34. amazon_ads_mcp/server/file_routes.py +547 -0
  35. amazon_ads_mcp/server/html_templates.py +149 -0
  36. amazon_ads_mcp/server/mcp_server.py +327 -0
  37. amazon_ads_mcp/server/openapi_utils.py +158 -0
  38. amazon_ads_mcp/server/sampling_handler.py +251 -0
  39. amazon_ads_mcp/server/server_builder.py +751 -0
  40. amazon_ads_mcp/server/sidecar_loader.py +178 -0
  41. amazon_ads_mcp/server/transform_executor.py +827 -0
  42. amazon_ads_mcp/tools/__init__.py +22 -0
  43. amazon_ads_mcp/tools/cache_management.py +105 -0
  44. amazon_ads_mcp/tools/download_tools.py +267 -0
  45. amazon_ads_mcp/tools/identity.py +236 -0
  46. amazon_ads_mcp/tools/oauth.py +598 -0
  47. amazon_ads_mcp/tools/profile.py +150 -0
  48. amazon_ads_mcp/tools/profile_listing.py +285 -0
  49. amazon_ads_mcp/tools/region.py +320 -0
  50. amazon_ads_mcp/tools/region_identity.py +175 -0
  51. amazon_ads_mcp/utils/__init__.py +6 -0
  52. amazon_ads_mcp/utils/async_compat.py +215 -0
  53. amazon_ads_mcp/utils/errors.py +452 -0
  54. amazon_ads_mcp/utils/export_content_type_resolver.py +249 -0
  55. amazon_ads_mcp/utils/export_download_handler.py +579 -0
  56. amazon_ads_mcp/utils/header_resolver.py +81 -0
  57. amazon_ads_mcp/utils/http/__init__.py +56 -0
  58. amazon_ads_mcp/utils/http/circuit_breaker.py +127 -0
  59. amazon_ads_mcp/utils/http/client_manager.py +329 -0
  60. amazon_ads_mcp/utils/http/request.py +207 -0
  61. amazon_ads_mcp/utils/http/resilience.py +512 -0
  62. amazon_ads_mcp/utils/http/resilient_client.py +195 -0
  63. amazon_ads_mcp/utils/http/retry.py +76 -0
  64. amazon_ads_mcp/utils/http_client.py +873 -0
  65. amazon_ads_mcp/utils/media/__init__.py +21 -0
  66. amazon_ads_mcp/utils/media/negotiator.py +243 -0
  67. amazon_ads_mcp/utils/media/types.py +199 -0
  68. amazon_ads_mcp/utils/openapi/__init__.py +16 -0
  69. amazon_ads_mcp/utils/openapi/json.py +55 -0
  70. amazon_ads_mcp/utils/openapi/loader.py +263 -0
  71. amazon_ads_mcp/utils/openapi/refs.py +46 -0
  72. amazon_ads_mcp/utils/region_config.py +200 -0
  73. amazon_ads_mcp/utils/response_wrapper.py +171 -0
  74. amazon_ads_mcp/utils/sampling_helpers.py +156 -0
  75. amazon_ads_mcp/utils/sampling_wrapper.py +173 -0
  76. amazon_ads_mcp/utils/security.py +630 -0
  77. amazon_ads_mcp/utils/tool_naming.py +137 -0
  78. amazon_ads_mcp-0.2.7.dist-info/METADATA +664 -0
  79. amazon_ads_mcp-0.2.7.dist-info/RECORD +82 -0
  80. amazon_ads_mcp-0.2.7.dist-info/WHEEL +4 -0
  81. amazon_ads_mcp-0.2.7.dist-info/entry_points.txt +3 -0
  82. 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"]