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,320 @@
|
|
|
1
|
+
"""Manage Amazon Ads API region for the MCP server.
|
|
2
|
+
|
|
3
|
+
Provide utilities to set, inspect, and list region information including
|
|
4
|
+
endpoints and sandbox state.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
.. code-block:: python
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from amazon_ads_mcp.tools.region import set_active_region, get_active_region
|
|
12
|
+
|
|
13
|
+
async def main():
|
|
14
|
+
await set_active_region("na")
|
|
15
|
+
info = await get_active_region()
|
|
16
|
+
print(info["region"]) # "na"
|
|
17
|
+
|
|
18
|
+
asyncio.run(main())
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
from ..auth.manager import get_auth_manager
|
|
25
|
+
from ..config.settings import Settings
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def set_active_region(region: Literal["na", "eu", "fe"]) -> dict:
|
|
31
|
+
"""Set the active Amazon Ads API region.
|
|
32
|
+
|
|
33
|
+
Update API and OAuth endpoints for subsequent calls. When the provider
|
|
34
|
+
binds region to identity, advise switching identities instead.
|
|
35
|
+
|
|
36
|
+
:param region: Target region ("na", "eu", or "fe").
|
|
37
|
+
:return: Result payload with region info and endpoints.
|
|
38
|
+
:raises ValueError: If the region value is invalid.
|
|
39
|
+
:raises Exception: If updating the region fails.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
# Validate region
|
|
43
|
+
if region not in ["na", "eu", "fe"]:
|
|
44
|
+
raise ValueError(f"Invalid region: {region}. Must be 'na', 'eu', or 'fe'")
|
|
45
|
+
|
|
46
|
+
# Get auth manager to check provider capabilities
|
|
47
|
+
auth_manager = get_auth_manager()
|
|
48
|
+
|
|
49
|
+
# Check if provider controls region through identity
|
|
50
|
+
if (
|
|
51
|
+
hasattr(auth_manager.provider, "region_controlled_by_identity")
|
|
52
|
+
and auth_manager.provider.region_controlled_by_identity()
|
|
53
|
+
):
|
|
54
|
+
# For providers where region is identity-controlled, we can't directly set the region
|
|
55
|
+
# The region is determined by the active identity
|
|
56
|
+
current_identity = auth_manager.get_active_identity()
|
|
57
|
+
if current_identity:
|
|
58
|
+
identity_region = current_identity.attributes.get("region", "na")
|
|
59
|
+
if identity_region != region:
|
|
60
|
+
return {
|
|
61
|
+
"success": False,
|
|
62
|
+
"error": "REGION_MISMATCH",
|
|
63
|
+
"message": f"Cannot set region to '{region}' when using {auth_manager.provider.provider_type} authentication. "
|
|
64
|
+
f"Current identity is in '{identity_region}' region. "
|
|
65
|
+
f"Please select an identity in the '{region}' region instead.",
|
|
66
|
+
"current_identity": current_identity.attributes.get(
|
|
67
|
+
"name", current_identity.id
|
|
68
|
+
),
|
|
69
|
+
"identity_region": identity_region,
|
|
70
|
+
"requested_region": region,
|
|
71
|
+
}
|
|
72
|
+
else:
|
|
73
|
+
# Region already matches the identity's region
|
|
74
|
+
return {
|
|
75
|
+
"success": True,
|
|
76
|
+
"message": f"Region is already set to '{region}' via OpenBridge identity",
|
|
77
|
+
"region": region,
|
|
78
|
+
"identity": current_identity.attributes.get(
|
|
79
|
+
"name", current_identity.id
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
else:
|
|
83
|
+
return {
|
|
84
|
+
"success": False,
|
|
85
|
+
"error": "NO_IDENTITY",
|
|
86
|
+
"message": "No active identity set. Please select an identity first using 'set_active_identity'",
|
|
87
|
+
"requested_region": region,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# For non-OpenBridge providers (like DirectAmazonAdsProvider)
|
|
91
|
+
# Get the current region first
|
|
92
|
+
old_region = (
|
|
93
|
+
auth_manager.provider.region
|
|
94
|
+
if hasattr(auth_manager.provider, "region")
|
|
95
|
+
else "na"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# If using direct auth, update the provider's region
|
|
99
|
+
if hasattr(auth_manager.provider, "_region"):
|
|
100
|
+
# For DirectAmazonAdsProvider - it has a settable _region attribute
|
|
101
|
+
auth_manager.provider._region = region
|
|
102
|
+
# Note: OAuth endpoint is determined dynamically via get_oauth_endpoint()
|
|
103
|
+
# so no need to update it explicitly
|
|
104
|
+
|
|
105
|
+
# Clear cached tokens as they might be region-specific
|
|
106
|
+
if hasattr(auth_manager.provider, "_access_token"):
|
|
107
|
+
auth_manager.provider._access_token = None
|
|
108
|
+
logger.info("Cleared cached access token due to region change")
|
|
109
|
+
|
|
110
|
+
# Map region to name for clarity
|
|
111
|
+
region_names = {
|
|
112
|
+
"na": "North America",
|
|
113
|
+
"eu": "Europe",
|
|
114
|
+
"fe": "Far East",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Get the new endpoint URLs from provider if available
|
|
118
|
+
if hasattr(auth_manager.provider, "get_region_endpoint"):
|
|
119
|
+
region_endpoint = auth_manager.provider.get_region_endpoint(region)
|
|
120
|
+
else:
|
|
121
|
+
# Fallback to settings for display
|
|
122
|
+
settings = Settings()
|
|
123
|
+
region_endpoint = settings.region_endpoint
|
|
124
|
+
|
|
125
|
+
# Get OAuth endpoint if available
|
|
126
|
+
oauth_endpoint = None
|
|
127
|
+
if hasattr(auth_manager.provider, "get_oauth_endpoint"):
|
|
128
|
+
oauth_endpoint = auth_manager.provider.get_oauth_endpoint(region)
|
|
129
|
+
|
|
130
|
+
# Build response
|
|
131
|
+
response = {
|
|
132
|
+
"success": True,
|
|
133
|
+
"previous_region": old_region,
|
|
134
|
+
"new_region": region,
|
|
135
|
+
"region_name": region_names[region],
|
|
136
|
+
"api_endpoint": region_endpoint,
|
|
137
|
+
"message": f"Region changed from {old_region} to {region}",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Add OAuth endpoint if available
|
|
141
|
+
if oauth_endpoint:
|
|
142
|
+
response["oauth_endpoint"] = oauth_endpoint
|
|
143
|
+
|
|
144
|
+
logger.info(
|
|
145
|
+
f"Region changed from {old_region} to {region} ({region_names[region]})"
|
|
146
|
+
)
|
|
147
|
+
return response
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to set active region: {e}")
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def get_active_region() -> dict:
|
|
155
|
+
"""Return information about the active region.
|
|
156
|
+
|
|
157
|
+
Include endpoints, sandbox mode, active auth method, and whether the
|
|
158
|
+
region source is identity or configuration.
|
|
159
|
+
|
|
160
|
+
:return: Region information with endpoints and metadata.
|
|
161
|
+
:raises Exception: If retrieval fails.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Get auth manager to access current region
|
|
165
|
+
auth_manager = get_auth_manager()
|
|
166
|
+
|
|
167
|
+
# Get region from provider if available, otherwise use default
|
|
168
|
+
if hasattr(auth_manager.provider, "region"):
|
|
169
|
+
region = auth_manager.provider.region
|
|
170
|
+
else:
|
|
171
|
+
# Fallback to environment/default if provider doesn't have region
|
|
172
|
+
settings = Settings()
|
|
173
|
+
region = settings.amazon_ads_region
|
|
174
|
+
|
|
175
|
+
# Map region to name
|
|
176
|
+
region_names = {
|
|
177
|
+
"na": "North America",
|
|
178
|
+
"eu": "Europe",
|
|
179
|
+
"fe": "Far East",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Get endpoint URLs from provider if available, otherwise from settings
|
|
183
|
+
if hasattr(auth_manager.provider, "get_region_endpoint"):
|
|
184
|
+
region_endpoint = auth_manager.provider.get_region_endpoint()
|
|
185
|
+
else:
|
|
186
|
+
settings = Settings()
|
|
187
|
+
region_endpoint = settings.region_endpoint
|
|
188
|
+
|
|
189
|
+
# Get sandbox mode from settings
|
|
190
|
+
settings = Settings()
|
|
191
|
+
sandbox_mode = settings.amazon_ads_sandbox_mode
|
|
192
|
+
|
|
193
|
+
response = {
|
|
194
|
+
"success": True,
|
|
195
|
+
"region": region,
|
|
196
|
+
"region_name": region_names.get(region, "Unknown"),
|
|
197
|
+
"api_endpoint": region_endpoint,
|
|
198
|
+
"sandbox_mode": sandbox_mode,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Add OAuth endpoint if available
|
|
202
|
+
if hasattr(auth_manager.provider, "get_oauth_endpoint"):
|
|
203
|
+
response["oauth_endpoint"] = auth_manager.provider.get_oauth_endpoint()
|
|
204
|
+
response["auth_method"] = "direct"
|
|
205
|
+
else:
|
|
206
|
+
response["auth_method"] = "openbridge"
|
|
207
|
+
|
|
208
|
+
# Check if region is from identity (OpenBridge) or config
|
|
209
|
+
if auth_manager.get_active_identity():
|
|
210
|
+
identity_region = auth_manager.get_active_region()
|
|
211
|
+
if identity_region:
|
|
212
|
+
response["identity_region"] = identity_region
|
|
213
|
+
response["source"] = (
|
|
214
|
+
"identity" if identity_region == region else "config"
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
response["source"] = "config"
|
|
218
|
+
else:
|
|
219
|
+
response["source"] = "config"
|
|
220
|
+
|
|
221
|
+
return response
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Failed to get active region: {e}")
|
|
225
|
+
raise
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def list_available_regions() -> dict:
|
|
229
|
+
"""List available regions and their endpoints.
|
|
230
|
+
|
|
231
|
+
Include API and OAuth endpoints, marketplaces, sandbox mode, and the
|
|
232
|
+
currently selected region.
|
|
233
|
+
|
|
234
|
+
:return: Mapping containing region details.
|
|
235
|
+
:raises Exception: If listing fails.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
# Get auth manager to access current region
|
|
239
|
+
auth_manager = get_auth_manager()
|
|
240
|
+
|
|
241
|
+
# Get current region from provider or settings
|
|
242
|
+
if hasattr(auth_manager.provider, "region"):
|
|
243
|
+
current_region = auth_manager.provider.region
|
|
244
|
+
else:
|
|
245
|
+
settings = Settings()
|
|
246
|
+
current_region = settings.amazon_ads_region
|
|
247
|
+
|
|
248
|
+
# Get sandbox mode from settings
|
|
249
|
+
settings = Settings()
|
|
250
|
+
sandbox_mode = settings.amazon_ads_sandbox_mode
|
|
251
|
+
|
|
252
|
+
regions = {
|
|
253
|
+
"na": {
|
|
254
|
+
"name": "North America",
|
|
255
|
+
"api_endpoint": "https://advertising-api.amazon.com",
|
|
256
|
+
"oauth_endpoint": "https://api.amazon.com/auth/o2/token",
|
|
257
|
+
"marketplaces": ["US", "CA", "MX", "BR"],
|
|
258
|
+
},
|
|
259
|
+
"eu": {
|
|
260
|
+
"name": "Europe",
|
|
261
|
+
"api_endpoint": "https://advertising-api-eu.amazon.com",
|
|
262
|
+
"oauth_endpoint": "https://api.amazon.co.uk/auth/o2/token",
|
|
263
|
+
"marketplaces": [
|
|
264
|
+
"UK",
|
|
265
|
+
"DE",
|
|
266
|
+
"FR",
|
|
267
|
+
"IT",
|
|
268
|
+
"ES",
|
|
269
|
+
"NL",
|
|
270
|
+
"AE",
|
|
271
|
+
"SE",
|
|
272
|
+
"PL",
|
|
273
|
+
"TR",
|
|
274
|
+
"SG",
|
|
275
|
+
"AU",
|
|
276
|
+
"IN",
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
"fe": {
|
|
280
|
+
"name": "Far East",
|
|
281
|
+
"api_endpoint": "https://advertising-api-fe.amazon.com",
|
|
282
|
+
"oauth_endpoint": "https://api.amazon.co.jp/auth/o2/token",
|
|
283
|
+
"marketplaces": ["JP"],
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Adjust for sandbox mode
|
|
288
|
+
if sandbox_mode:
|
|
289
|
+
for region_data in regions.values():
|
|
290
|
+
region_data["api_endpoint"] = region_data["api_endpoint"].replace(
|
|
291
|
+
"advertising-api", "advertising-api-test"
|
|
292
|
+
)
|
|
293
|
+
region_data["sandbox"] = True
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
"success": True,
|
|
297
|
+
"current_region": current_region,
|
|
298
|
+
"sandbox_mode": sandbox_mode,
|
|
299
|
+
"regions": regions,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.error(f"Failed to list regions: {e}")
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# Alias functions for backward compatibility with builtin_tools
|
|
308
|
+
async def set_region(region: Literal["na", "eu", "fe"]) -> dict:
|
|
309
|
+
"""Alias for set_active_region for backward compatibility."""
|
|
310
|
+
return await set_active_region(region)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def get_region() -> dict:
|
|
314
|
+
"""Alias for get_active_region for backward compatibility."""
|
|
315
|
+
return await get_active_region()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def list_regions() -> dict:
|
|
319
|
+
"""Alias for list_available_regions for backward compatibility."""
|
|
320
|
+
return await list_available_regions()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Region-aware identity selection tools.
|
|
2
|
+
|
|
3
|
+
This module provides tools to help find and switch to identities
|
|
4
|
+
in specific regions when using OpenBridge authentication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..auth.manager import get_auth_manager
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def list_identities_by_region(region: Optional[str] = None) -> Dict:
|
|
16
|
+
"""List identities grouped by region.
|
|
17
|
+
|
|
18
|
+
This is particularly useful for OpenBridge authentication where
|
|
19
|
+
regions are tied to identities rather than being freely switchable.
|
|
20
|
+
|
|
21
|
+
:param region: Optional region filter ('na', 'eu', 'fe')
|
|
22
|
+
:type region: Optional[str]
|
|
23
|
+
:return: Dictionary with identities grouped by region
|
|
24
|
+
:rtype: Dict
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
auth_manager = get_auth_manager()
|
|
28
|
+
|
|
29
|
+
# Get all identities
|
|
30
|
+
if hasattr(auth_manager.provider, "list_identities"):
|
|
31
|
+
all_identities = await auth_manager.provider.list_identities()
|
|
32
|
+
else:
|
|
33
|
+
all_identities = await auth_manager.list_identities()
|
|
34
|
+
|
|
35
|
+
# Group by region
|
|
36
|
+
by_region = {"na": [], "eu": [], "fe": [], "unknown": []}
|
|
37
|
+
|
|
38
|
+
for identity in all_identities:
|
|
39
|
+
identity_region = identity.attributes.get("region", "unknown").lower()
|
|
40
|
+
if identity_region in by_region:
|
|
41
|
+
by_region[identity_region].append(
|
|
42
|
+
{
|
|
43
|
+
"id": identity.id,
|
|
44
|
+
"name": identity.attributes.get("name", identity.id),
|
|
45
|
+
"email": identity.attributes.get("email", ""),
|
|
46
|
+
"region": identity_region,
|
|
47
|
+
"account_id": identity.attributes.get("account_id", ""),
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
by_region["unknown"].append(
|
|
52
|
+
{
|
|
53
|
+
"id": identity.id,
|
|
54
|
+
"name": identity.attributes.get("name", identity.id),
|
|
55
|
+
"region": identity_region,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Get current active identity
|
|
60
|
+
current_identity = auth_manager.get_active_identity()
|
|
61
|
+
current_id = current_identity.id if current_identity else None
|
|
62
|
+
|
|
63
|
+
# Filter by region if specified
|
|
64
|
+
if region and region in ["na", "eu", "fe"]:
|
|
65
|
+
filtered = {
|
|
66
|
+
region: by_region[region],
|
|
67
|
+
"total": len(by_region[region]),
|
|
68
|
+
"current_identity": current_id,
|
|
69
|
+
"message": f"Found {len(by_region[region])} identities in {region.upper()} region",
|
|
70
|
+
}
|
|
71
|
+
return filtered
|
|
72
|
+
|
|
73
|
+
# Return all grouped by region
|
|
74
|
+
return {
|
|
75
|
+
"regions": by_region,
|
|
76
|
+
"totals": {k: len(v) for k, v in by_region.items()},
|
|
77
|
+
"current_identity": current_id,
|
|
78
|
+
"message": "Identities grouped by region",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Failed to list identities by region: {e}")
|
|
83
|
+
return {
|
|
84
|
+
"success": False,
|
|
85
|
+
"error": str(e),
|
|
86
|
+
"message": "Failed to list identities by region",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def switch_to_region_identity(
|
|
91
|
+
target_region: str, identity_id: Optional[str] = None
|
|
92
|
+
) -> Dict:
|
|
93
|
+
"""Switch to an identity in the specified region.
|
|
94
|
+
|
|
95
|
+
If identity_id is provided, switches to that specific identity if it's in the
|
|
96
|
+
target region. Otherwise, switches to the first available identity in the region.
|
|
97
|
+
|
|
98
|
+
:param target_region: Target region ('na', 'eu', 'fe')
|
|
99
|
+
:type target_region: str
|
|
100
|
+
:param identity_id: Optional specific identity ID to switch to
|
|
101
|
+
:type identity_id: Optional[str]
|
|
102
|
+
:return: Result of the switch operation
|
|
103
|
+
:rtype: Dict
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
if target_region not in ["na", "eu", "fe"]:
|
|
107
|
+
return {
|
|
108
|
+
"success": False,
|
|
109
|
+
"error": "INVALID_REGION",
|
|
110
|
+
"message": f"Invalid region: {target_region}. Must be 'na', 'eu', or 'fe'",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Get identities in the target region
|
|
114
|
+
region_data = await list_identities_by_region(target_region)
|
|
115
|
+
|
|
116
|
+
if "error" in region_data:
|
|
117
|
+
return region_data
|
|
118
|
+
|
|
119
|
+
target_identities = region_data.get(target_region, [])
|
|
120
|
+
|
|
121
|
+
if not target_identities:
|
|
122
|
+
return {
|
|
123
|
+
"success": False,
|
|
124
|
+
"error": "NO_IDENTITIES",
|
|
125
|
+
"message": f"No identities found in {target_region.upper()} region",
|
|
126
|
+
"region": target_region,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# If specific identity requested, validate it's in the region
|
|
130
|
+
if identity_id:
|
|
131
|
+
matching = [i for i in target_identities if i["id"] == identity_id]
|
|
132
|
+
if not matching:
|
|
133
|
+
return {
|
|
134
|
+
"success": False,
|
|
135
|
+
"error": "IDENTITY_NOT_IN_REGION",
|
|
136
|
+
"message": f"Identity {identity_id} is not in {target_region.upper()} region",
|
|
137
|
+
"available_identities": target_identities,
|
|
138
|
+
}
|
|
139
|
+
selected = matching[0]
|
|
140
|
+
else:
|
|
141
|
+
# Select first available identity in region
|
|
142
|
+
selected = target_identities[0]
|
|
143
|
+
|
|
144
|
+
# Switch to the selected identity
|
|
145
|
+
from ..models import SetActiveIdentityRequest
|
|
146
|
+
|
|
147
|
+
request = SetActiveIdentityRequest(identity_id=selected["id"], persist=True)
|
|
148
|
+
|
|
149
|
+
from . import identity
|
|
150
|
+
|
|
151
|
+
result = await identity.set_active_identity(request)
|
|
152
|
+
|
|
153
|
+
if result.success:
|
|
154
|
+
return {
|
|
155
|
+
"success": True,
|
|
156
|
+
"message": f"Switched to {selected['name']} in {target_region.upper()} region",
|
|
157
|
+
"identity": selected,
|
|
158
|
+
"region": target_region,
|
|
159
|
+
"credentials_loaded": result.credentials_loaded,
|
|
160
|
+
}
|
|
161
|
+
else:
|
|
162
|
+
return {
|
|
163
|
+
"success": False,
|
|
164
|
+
"error": "SWITCH_FAILED",
|
|
165
|
+
"message": f"Failed to switch to identity: {result.message}",
|
|
166
|
+
"identity": selected,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to switch to region identity: {e}")
|
|
171
|
+
return {
|
|
172
|
+
"success": False,
|
|
173
|
+
"error": str(e),
|
|
174
|
+
"message": "Failed to switch to region identity",
|
|
175
|
+
}
|