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,249 @@
1
+ """
2
+ Download/Export content-type resolver for Amazon Ads API.
3
+
4
+ This module provides helpers to determine preferred Accept/Content-Type
5
+ for known download-like flows across Amazon Ads APIs, beyond classic
6
+ Exports (e.g., DSP Measurement results, Sponsored Ads report/snapshot
7
+ redirectors, Brand Metrics report retrieval).
8
+ """
9
+
10
+ import base64
11
+ import logging
12
+ from typing import List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ def _decode_export_id(export_id: str) -> Optional[str]:
17
+ """Decode an export ID payload to text, if it looks base64-encoded."""
18
+ if not export_id:
19
+ return None
20
+
21
+ pad_len = (-len(export_id)) % 4
22
+ padded = export_id + ("=" * pad_len)
23
+
24
+ for decoder in (base64.urlsafe_b64decode, base64.b64decode):
25
+ try:
26
+ decoded_bytes = decoder(padded)
27
+ return decoded_bytes.decode("utf-8")
28
+ except Exception:
29
+ continue
30
+ return None
31
+
32
+
33
+ def resolve_export_content_type(export_id: str) -> Optional[str]:
34
+ """
35
+ Resolve the correct content-type for an export based on its ID.
36
+
37
+ Amazon export IDs appear to be base64-encoded with a suffix indicating type:
38
+ - ,C = Campaign export
39
+ - ,A = Ad Group export
40
+ - ,AD = Ads export
41
+ - ,T = Targets export
42
+
43
+ :param export_id: The export ID from Amazon
44
+ :type export_id: str
45
+ :return: The appropriate content-type or None if unable to determine
46
+ :rtype: Optional[str]
47
+ """
48
+ if not export_id:
49
+ return None
50
+
51
+ try:
52
+ # Export IDs appear to be base64 encoded with a type suffix
53
+ # Example: "OTc2MDhjNmEtNDg3Zi00YzMyLTllOWEtMDMwNjNhYTk1MGM0LEM"
54
+ # Decodes to: "97608c6a-487f-4c32-9e9a-03063aa950c4,C"
55
+
56
+ # Try to decode to check for pattern
57
+ try:
58
+ decoded = _decode_export_id(export_id)
59
+ if not decoded:
60
+ raise ValueError("Export ID did not decode")
61
+ if "," in decoded:
62
+ _, suffix = decoded.rsplit(",", 1)
63
+ suffix = suffix.upper()
64
+
65
+ # Map suffix to content-type
66
+ suffix_map = {
67
+ "C": "application/vnd.campaignsexport.v1+json",
68
+ "A": "application/vnd.adgroupsexport.v1+json",
69
+ "AD": "application/vnd.adsexport.v1+json",
70
+ # Some export IDs use ',R' for ads exports.
71
+ "R": "application/vnd.adsexport.v1+json",
72
+ "T": "application/vnd.targetsexport.v1+json",
73
+ }
74
+
75
+ content_type = suffix_map.get(suffix)
76
+ if content_type:
77
+ logger.debug(
78
+ f"Resolved export type from ID suffix '{suffix}': {content_type}"
79
+ )
80
+ return content_type
81
+ except Exception:
82
+ # Not base64 or doesn't match expected pattern
83
+ pass
84
+
85
+ # Fallback: check if the ID itself contains hints
86
+ export_id_lower = export_id.lower()
87
+
88
+ if "campaign" in export_id_lower:
89
+ return "application/vnd.campaignsexport.v1+json"
90
+ elif "adgroup" in export_id_lower:
91
+ return "application/vnd.adgroupsexport.v1+json"
92
+ elif "ad" in export_id_lower and "adgroup" not in export_id_lower:
93
+ return "application/vnd.adsexport.v1+json"
94
+ elif "target" in export_id_lower:
95
+ return "application/vnd.targetsexport.v1+json"
96
+
97
+ except Exception as e:
98
+ logger.warning(f"Error resolving export content-type: {e}")
99
+
100
+ return None
101
+
102
+
103
+ def get_export_accept_headers(export_id: str) -> List[str]:
104
+ """
105
+ Get a prioritized list of Accept headers for an export.
106
+
107
+ If we can determine the type, return that first.
108
+ Otherwise return all possibilities in a sensible order.
109
+
110
+ :param export_id: The export ID
111
+ :type export_id: str
112
+ :return: List of content-types to try
113
+ :rtype: List[str]
114
+ """
115
+ # Try to resolve the specific type
116
+ specific_type = resolve_export_content_type(export_id)
117
+
118
+ # All possible types
119
+ all_types = [
120
+ "application/vnd.campaignsexport.v1+json",
121
+ "application/vnd.adgroupsexport.v1+json",
122
+ "application/vnd.adsexport.v1+json",
123
+ "application/vnd.targetsexport.v1+json",
124
+ ]
125
+
126
+ if specific_type and specific_type in all_types:
127
+ # Put the specific type first, then other vendor types, then application/json
128
+ # FastMCP's experimental parser needs application/json in the Accept list
129
+ all_types.remove(specific_type)
130
+ return [specific_type] + all_types + ["application/json"]
131
+
132
+ # Include application/json as fallback for FastMCP compatibility
133
+ return all_types + ["application/json"]
134
+
135
+
136
+ def get_measurement_accept_headers(prefer_csv: bool = False) -> List[str]:
137
+ """
138
+ Preferred Accept headers for DSP Measurement result downloads.
139
+
140
+ Measurement endpoints support both JSON and CSV vendor types, with a 307
141
+ redirect to an S3 location for actual file download. If CSV is preferred,
142
+ return CSV first; otherwise JSON first.
143
+ """
144
+ json_type = "application/vnd.measurementresult.v1.2+json"
145
+ csv_type = "text/vnd.measurementresult.v1.2+csv"
146
+ # Include application/json for FastMCP compatibility
147
+ types = [csv_type, json_type] if prefer_csv else [json_type, csv_type]
148
+ return types + ["application/json"]
149
+
150
+
151
+ def get_brandmetrics_accept_headers() -> List[str]:
152
+ """
153
+ Preferred Accept headers for Brand Metrics report endpoints.
154
+
155
+ Brand Metrics typically returns vendor JSON, with multiple versions
156
+ (v1.1 and v1). Prefer the latest first.
157
+ """
158
+ return [
159
+ "application/vnd.insightsbrandmetrics.v1.1+json",
160
+ "application/vnd.insightsbrandmetrics.v1+json",
161
+ "application/json", # Include for FastMCP compatibility
162
+ ]
163
+
164
+
165
+ def get_reports_download_accept_headers() -> List[str]:
166
+ """
167
+ Sponsored Ads reports/snapshots download endpoints return a 307 redirect
168
+ with location header. Accept is usually application/json for the status
169
+ envelope prior to redirect.
170
+ """
171
+ return ["application/json"]
172
+
173
+
174
+ def resolve_download_accept_headers(
175
+ method: str, url: str, *, prefer_csv: bool = False
176
+ ) -> List[str]:
177
+ """
178
+ Resolve Accept headers for known download endpoints using URL heuristics.
179
+
180
+ - Exports: use export-specific vendor types inferred from export_id where possible.
181
+ - DSP Measurement: support JSON/CSV vendor types (prefer CSV if requested).
182
+ - Sponsored Ads Reports/Snapshots: default to application/json (redirect flow).
183
+ - Brand Metrics: prefer v1.1 then v1 vendor JSON.
184
+ - S3 URLs: No Accept override (let S3 handle it)
185
+ """
186
+ m = (method or "GET").upper()
187
+ u = (url or "").lower()
188
+
189
+ # Don't override Accept for S3 URLs - they serve pre-defined content
190
+ from urllib.parse import urlparse
191
+
192
+ parsed = urlparse(url)
193
+ # Validate hostname is legitimate AWS domain
194
+ hostname = (parsed.hostname or "").lower()
195
+ is_aws_domain = hostname.endswith(".amazonaws.com") or hostname == "amazonaws.com"
196
+ if hostname and is_aws_domain:
197
+ # S3 URLs don't need Accept headers - they serve what they have
198
+ return []
199
+
200
+ # Export creation endpoints (POST)
201
+ if m == "POST" and "/export" in u:
202
+ if "/campaigns/export" in u:
203
+ return [
204
+ "application/vnd.campaignsexport.v1+json",
205
+ "application/json",
206
+ ]
207
+ elif "/adgroups/export" in u:
208
+ return [
209
+ "application/vnd.adgroupsexport.v1+json",
210
+ "application/json",
211
+ ]
212
+ elif "/ads/export" in u:
213
+ return ["application/vnd.adsexport.v1+json", "application/json"]
214
+ elif "/targets/export" in u:
215
+ return [
216
+ "application/vnd.targetsexport.v1+json",
217
+ "application/json",
218
+ ]
219
+
220
+ # Export retrieval pattern: /exports/{exportId}
221
+ if "/exports/" in u:
222
+ # Try to extract exportId and leverage export content-type map
223
+ try:
224
+ import re
225
+
226
+ match = re.search(r"/exports/([^/?]+)", url)
227
+ if match:
228
+ export_id = match.group(1)
229
+ return get_export_accept_headers(export_id)
230
+ except Exception:
231
+ pass
232
+ return get_export_accept_headers("")
233
+
234
+ # DSP Measurement results (audienceResearch/brandLift/creativeTesting/etc.)
235
+ if "/dsp/measurement/" in u or "/measurement/" in u:
236
+ return get_measurement_accept_headers(prefer_csv=prefer_csv)
237
+
238
+ # Sponsored Display/Snapshots/Reports download endpoints
239
+ if "/snapshots/" in u and "/download" in u:
240
+ return get_reports_download_accept_headers()
241
+ if "/v2/reports/" in u and "/download" in u:
242
+ return get_reports_download_accept_headers()
243
+
244
+ # Brand Metrics
245
+ if "/insights/brandmetrics/" in u or "brandmetrics" in u:
246
+ return get_brandmetrics_accept_headers()
247
+
248
+ # Default fallback
249
+ return ["application/json"]