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,22 @@
1
+ """Tools module for Amazon Ads MCP.
2
+
3
+ This module provides MCP tools for Amazon Ads API integration,
4
+ including identity management and API operation tools.
5
+
6
+ :var __all__: List of public exports from this module
7
+ :type __all__: List[str]
8
+ """
9
+
10
+ from .identity import (
11
+ get_active_identity,
12
+ get_identity_info,
13
+ list_remote_identities,
14
+ set_active_identity,
15
+ )
16
+
17
+ __all__ = [
18
+ "list_remote_identities",
19
+ "get_active_identity",
20
+ "set_active_identity",
21
+ "get_identity_info",
22
+ ]
@@ -0,0 +1,105 @@
1
+ """Cache management tools for MCP server.
2
+
3
+ This module provides tools for managing and clearing caches
4
+ to ensure fresh data is fetched from the API.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict
9
+
10
+ from ..auth.manager import get_auth_manager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def clear_identity_cache() -> Dict:
16
+ """Clear the OpenBridge identity cache to force fresh fetch.
17
+
18
+ This function clears the cached identities in the OpenBridge provider
19
+ to ensure the next call to list_identities fetches fresh data from
20
+ the API instead of returning cached results.
21
+
22
+ :return: Status of the cache clearing operation
23
+ :rtype: Dict
24
+ """
25
+ try:
26
+ auth_manager = get_auth_manager()
27
+
28
+ # Check if provider has cache
29
+ if hasattr(auth_manager.provider, "_identities_cache"):
30
+ cache_size = len(auth_manager.provider._identities_cache)
31
+ auth_manager.provider._identities_cache.clear()
32
+ logger.info(f"Cleared identity cache ({cache_size} entries)")
33
+
34
+ return {
35
+ "success": True,
36
+ "message": f"Identity cache cleared ({cache_size} entries removed)",
37
+ "cache_size_before": cache_size,
38
+ "cache_size_after": 0,
39
+ }
40
+ else:
41
+ return {
42
+ "success": True,
43
+ "message": "No identity cache found (provider may not use caching)",
44
+ "cache_size_before": 0,
45
+ "cache_size_after": 0,
46
+ }
47
+
48
+ except Exception as e:
49
+ logger.error(f"Failed to clear identity cache: {e}")
50
+ return {
51
+ "success": False,
52
+ "error": str(e),
53
+ "message": "Failed to clear identity cache",
54
+ }
55
+
56
+
57
+ async def get_cache_status() -> Dict:
58
+ """Get the current status of the identity cache.
59
+
60
+ Returns information about the current cache state including
61
+ the number of cached entries and their keys.
62
+
63
+ :return: Cache status information
64
+ :rtype: Dict
65
+ """
66
+ try:
67
+ auth_manager = get_auth_manager()
68
+
69
+ if hasattr(auth_manager.provider, "_identities_cache"):
70
+ cache = auth_manager.provider._identities_cache
71
+ cache_keys = list(cache.keys())
72
+ cache_size = len(cache)
73
+
74
+ # Count identities in cache
75
+ total_identities = 0
76
+ cache_details = []
77
+ for key in cache_keys:
78
+ identities = cache[key]
79
+ total_identities += len(identities)
80
+ cache_details.append(
81
+ {"key": str(key), "identity_count": len(identities)}
82
+ )
83
+
84
+ return {
85
+ "success": True,
86
+ "cache_enabled": True,
87
+ "cache_size": cache_size,
88
+ "total_identities_cached": total_identities,
89
+ "cache_entries": cache_details,
90
+ "message": f"Cache contains {cache_size} entries with {total_identities} total identities",
91
+ }
92
+ else:
93
+ return {
94
+ "success": True,
95
+ "cache_enabled": False,
96
+ "message": "No identity cache found",
97
+ }
98
+
99
+ except Exception as e:
100
+ logger.error(f"Failed to get cache status: {e}")
101
+ return {
102
+ "success": False,
103
+ "error": str(e),
104
+ "message": "Failed to get cache status",
105
+ }
@@ -0,0 +1,267 @@
1
+ """Download management tools for Amazon Ads MCP server.
2
+
3
+ This module provides tools for managing downloads of exports and reports
4
+ from the Amazon Ads API. It handles export status checking, file
5
+ downloading, local file management, and cleanup operations.
6
+
7
+ The tools integrate with the export download handler to provide a
8
+ unified interface for managing downloaded data files.
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+
16
+ from ..utils.export_download_handler import get_download_handler
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ async def check_and_download_export(
22
+ export_id: str,
23
+ export_response: Dict[str, Any],
24
+ export_type: Optional[str] = None,
25
+ profile_id: Optional[str] = None,
26
+ ) -> Dict[str, Any]:
27
+ """Check export status and download if ready.
28
+
29
+ This function checks the status of an export and attempts to download
30
+ it if it's ready. It can infer the export type from the export ID
31
+ if not provided, and handles various export statuses appropriately.
32
+
33
+ :param export_id: The export ID to check and download
34
+ :type export_id: str
35
+ :param export_response: Response from the GetExport API call
36
+ :type export_response: Dict[str, Any]
37
+ :param export_type: Optional export type (campaign, adgroup, etc.)
38
+ :type export_type: Optional[str]
39
+ :param profile_id: Optional profile ID for scoped storage
40
+ :type profile_id: Optional[str]
41
+ :return: Dictionary containing status and download information
42
+ :rtype: Dict[str, Any]
43
+ """
44
+ handler = get_download_handler()
45
+
46
+ # Infer export type from response if not provided
47
+ if not export_type and export_id:
48
+ # Try to decode from export ID
49
+ import base64
50
+
51
+ try:
52
+ padded = export_id + "=" * (4 - len(export_id) % 4)
53
+ decoded = base64.b64decode(padded).decode("utf-8")
54
+ if "," in decoded:
55
+ _, suffix = decoded.rsplit(",", 1)
56
+ type_map = {
57
+ "C": "campaigns",
58
+ "A": "adgroups",
59
+ "AD": "ads",
60
+ "T": "targets",
61
+ }
62
+ export_type = type_map.get(suffix.upper(), "general")
63
+ except (AttributeError, TypeError, ValueError, KeyError):
64
+ export_type = "general"
65
+
66
+ # Handle the export response with profile scoping
67
+ file_path = await handler.handle_export_response(
68
+ export_response, export_type, profile_id=profile_id
69
+ )
70
+
71
+ if file_path:
72
+ return {
73
+ "success": True,
74
+ "status": "downloaded",
75
+ "file_path": str(file_path),
76
+ "export_id": export_id,
77
+ "message": f"Export downloaded successfully to {file_path}",
78
+ }
79
+ else:
80
+ status = export_response.get("status", "UNKNOWN")
81
+ if status == "PROCESSING":
82
+ return {
83
+ "success": False,
84
+ "status": "processing",
85
+ "export_id": export_id,
86
+ "message": "Export is still being processed. Check again later.",
87
+ }
88
+ elif status == "FAILED":
89
+ error = export_response.get("error", {})
90
+ return {
91
+ "success": False,
92
+ "status": "failed",
93
+ "export_id": export_id,
94
+ "error": error,
95
+ "message": f"Export failed: {error.get('message', 'Unknown error')}",
96
+ }
97
+ else:
98
+ return {
99
+ "success": False,
100
+ "status": status.lower(),
101
+ "export_id": export_id,
102
+ "message": f"Export has status {status} - no download available",
103
+ }
104
+
105
+
106
+ async def list_downloaded_files(
107
+ resource_type: Optional[str] = None,
108
+ profile_id: Optional[str] = None,
109
+ ) -> Dict[str, Any]:
110
+ """List all downloaded files in the data directory.
111
+
112
+ Scans the data directory for downloaded files and provides a summary
113
+ of all available downloads. Can filter by resource type to show only
114
+ specific types of downloads.
115
+
116
+ When profile_id is provided:
117
+ - Lists files only from data/profiles/{profile_id}/
118
+
119
+ When profile_id is None:
120
+ - Lists files only from legacy (non-profile) directories
121
+
122
+ :param resource_type: Optional filter to show only specific resource types
123
+ :type resource_type: Optional[str]
124
+ :param profile_id: Optional profile ID for scoped listing
125
+ :type profile_id: Optional[str]
126
+ :return: Dictionary containing download summary and file listings
127
+ :rtype: Dict[str, Any]
128
+ """
129
+ handler = get_download_handler()
130
+ # handler.list_downloads returns a flat list of file dicts
131
+ files = handler.list_downloads(resource_type, profile_id=profile_id)
132
+
133
+ # Calculate totals from the flat list
134
+ total_files = len(files)
135
+ total_size = sum(f.get("size", 0) for f in files)
136
+
137
+ return {
138
+ "base_directory": str(handler.base_dir),
139
+ "total_files": total_files,
140
+ "total_size_bytes": total_size,
141
+ "files": files, # Flat list format
142
+ }
143
+
144
+
145
+ async def get_download_metadata(file_path: str) -> Dict[str, Any]:
146
+ """Get metadata for a downloaded file.
147
+
148
+ Retrieves metadata associated with a downloaded file, including
149
+ any custom metadata stored alongside the file. Falls back to
150
+ basic file information if no metadata is available.
151
+
152
+ :param file_path: Path to the downloaded file
153
+ :type file_path: str
154
+ :return: Dictionary containing file metadata and status
155
+ :rtype: Dict[str, Any]
156
+ """
157
+ path = Path(file_path)
158
+
159
+ if not path.exists():
160
+ return {
161
+ "success": False,
162
+ "error": "File not found",
163
+ "file_path": file_path,
164
+ }
165
+
166
+ meta_path = path.with_suffix(".meta.json")
167
+
168
+ if meta_path.exists():
169
+ with open(meta_path) as f:
170
+ metadata = json.load(f)
171
+ return {"success": True, "file_path": file_path, "metadata": metadata}
172
+ else:
173
+ # Return basic file info
174
+ stat = path.stat()
175
+ return {
176
+ "success": True,
177
+ "file_path": file_path,
178
+ "metadata": {
179
+ "file_name": path.name,
180
+ "file_size": stat.st_size,
181
+ "modified": stat.st_mtime,
182
+ "note": "No detailed metadata available",
183
+ },
184
+ }
185
+
186
+
187
+ async def clean_old_downloads(
188
+ profile_id: str,
189
+ resource_type: Optional[str] = None,
190
+ days_old: int = 7,
191
+ ) -> Dict[str, Any]:
192
+ """Clean up old downloaded files for a specific profile.
193
+
194
+ Removes downloaded files that are older than the specified number
195
+ of days within the profile's storage directory. This helps manage
196
+ disk space by cleaning up outdated export data. Can filter by
197
+ resource type to clean only specific types of downloads.
198
+
199
+ IMPORTANT: This function is profile-scoped to maintain multi-tenant
200
+ isolation. It only deletes files within data/profiles/{profile_id}/.
201
+
202
+ :param profile_id: Profile ID for scoped cleanup (required)
203
+ :type profile_id: str
204
+ :param resource_type: Optional filter by resource type
205
+ :type resource_type: Optional[str]
206
+ :param days_old: Delete files older than this many days
207
+ :type days_old: int
208
+ :return: Dictionary containing cleanup summary and deleted file list
209
+ :rtype: Dict[str, Any]
210
+ """
211
+ from datetime import datetime, timedelta
212
+
213
+ if not profile_id:
214
+ return {
215
+ "success": False,
216
+ "error": "profile_id is required for cleanup operations",
217
+ "deleted_files": 0,
218
+ "deleted_size_bytes": 0,
219
+ "files": [],
220
+ }
221
+
222
+ handler = get_download_handler()
223
+ cutoff_date = datetime.now() - timedelta(days=days_old)
224
+
225
+ # Use profile-scoped base directory for multi-tenant isolation
226
+ profile_base = handler.get_profile_base_dir(profile_id)
227
+
228
+ deleted_files = []
229
+ deleted_size = 0
230
+
231
+ if resource_type:
232
+ search_paths = [profile_base / resource_type]
233
+ else:
234
+ # Only search directories within the profile's storage
235
+ search_paths = [
236
+ p for p in profile_base.iterdir() if p.is_dir()
237
+ ] if profile_base.exists() else []
238
+
239
+ for resource_dir in search_paths:
240
+ if not resource_dir.exists():
241
+ continue
242
+
243
+ for sub_dir in resource_dir.iterdir():
244
+ if sub_dir.is_dir():
245
+ for file_path in sub_dir.iterdir():
246
+ if file_path.is_file():
247
+ stat = file_path.stat()
248
+ modified = datetime.fromtimestamp(stat.st_mtime)
249
+
250
+ if modified < cutoff_date:
251
+ deleted_size += stat.st_size
252
+ deleted_files.append(str(file_path))
253
+
254
+ # Delete the file and its metadata
255
+ file_path.unlink()
256
+ meta_path = file_path.with_suffix(".meta.json")
257
+ if meta_path.exists():
258
+ meta_path.unlink()
259
+
260
+ return {
261
+ "success": True,
262
+ "profile_id": profile_id,
263
+ "deleted_files": len(deleted_files),
264
+ "deleted_size_bytes": deleted_size,
265
+ "files": deleted_files,
266
+ "message": f"Deleted {len(deleted_files)} files older than {days_old} days for profile {profile_id}",
267
+ }
@@ -0,0 +1,236 @@
1
+ """MCP tools for identity management.
2
+
3
+ This module provides MCP tools for managing remote identities
4
+ for Amazon Ads API access, including listing, selecting, and
5
+ querying identity information.
6
+
7
+ The tools provide:
8
+
9
+ - List all available remote identities from OpenBridge
10
+ - Set active identity for Amazon Ads API operations
11
+ - Get current active identity information
12
+ - Get detailed information about specific identities
13
+ - Comprehensive error handling and logging
14
+
15
+ Examples:
16
+ >>> identities = await list_remote_identities()
17
+ >>> active = await get_active_identity()
18
+ >>> response = await set_active_identity(
19
+ ... SetActiveIdentityRequest(identity_id="123")
20
+ ... )
21
+ """
22
+
23
+ import logging
24
+ from typing import Optional
25
+
26
+ from ..auth.manager import get_auth_manager
27
+ from ..models import (
28
+ Identity,
29
+ IdentityListResponse,
30
+ SetActiveIdentityRequest,
31
+ SetActiveIdentityResponse,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ async def list_remote_identities(
38
+ identity_type: Optional[str] = "14",
39
+ ) -> IdentityListResponse:
40
+ """List all available Amazon Ads remote identities.
41
+
42
+ Returns a list of remote identities that can be used to access
43
+ different Amazon Ads accounts through OpenBridge. By default, filters
44
+ for Amazon Ads identities (type 14). The function handles both
45
+ OpenBridge and direct authentication providers.
46
+
47
+ The function automatically determines the provider type and calls the
48
+ appropriate method for listing identities. For OpenBridge providers,
49
+ it passes the identity_type filter to get only relevant identities.
50
+
51
+ :param identity_type: Filter by remote identity type (default "14" for Amazon Ads)
52
+ :type identity_type: Optional[str]
53
+ :return: Response containing the list of available identities
54
+ :rtype: IdentityListResponse
55
+ :raises Exception: If listing identities fails
56
+
57
+ .. note::
58
+ Identity type "14" corresponds to Amazon Ads identities in OpenBridge.
59
+
60
+ .. example::
61
+ >>> response = await list_remote_identities()
62
+ >>> print(f"Found {response.total} identities")
63
+ """
64
+ logger.info(f"Listing remote identities (type={identity_type})")
65
+
66
+ try:
67
+ auth_manager = get_auth_manager()
68
+ # Pass the identity_type filter to the provider
69
+ if hasattr(auth_manager.provider, "list_identities"):
70
+ identities = await auth_manager.provider.list_identities(
71
+ identity_type=identity_type
72
+ )
73
+ else:
74
+ identities = await auth_manager.list_identities()
75
+
76
+ return IdentityListResponse(
77
+ identities=identities, total=len(identities), has_more=False
78
+ )
79
+ except Exception as e:
80
+ logger.error(f"Failed to list identities: {e}")
81
+ raise
82
+
83
+
84
+ async def get_active_identity() -> Optional[Identity]:
85
+ """Get the currently active remote identity.
86
+
87
+ Returns the identity that is currently being used for Amazon Ads API calls.
88
+ This function provides access to the currently selected identity
89
+ and logs information about it for debugging purposes.
90
+
91
+ The function retrieves the active identity from the authentication manager
92
+ and logs relevant information for troubleshooting.
93
+
94
+ :return: The active Identity, or None if no identity is set
95
+ :rtype: Optional[Identity]
96
+
97
+ .. example::
98
+ >>> identity = await get_active_identity()
99
+ >>> if identity:
100
+ ... print(f"Active: {identity.attributes.get('name')}")
101
+ """
102
+ logger.info("Getting active identity")
103
+
104
+ auth_manager = get_auth_manager()
105
+ active_identity = auth_manager.get_active_identity()
106
+
107
+ if active_identity:
108
+ name = active_identity.attributes.get("name", active_identity.id)
109
+ logger.info(f"Active identity: {name} ({active_identity.id})")
110
+ else:
111
+ logger.info("No active identity set")
112
+
113
+ return active_identity
114
+
115
+
116
+ async def set_active_identity(
117
+ request: SetActiveIdentityRequest,
118
+ ) -> SetActiveIdentityResponse:
119
+ """Set the active remote identity for Amazon Ads API operations.
120
+
121
+ This identity will be used for all subsequent Amazon Ads API calls
122
+ until a different identity is selected. The function also attempts
123
+ to pre-load credentials for the selected identity.
124
+
125
+ The function performs identity validation and credential pre-loading
126
+ to ensure the selected identity is ready for use. If credential
127
+ loading fails, the identity is still set but a warning is included
128
+ in the response.
129
+
130
+ :param request: Request containing the identity_id to activate
131
+ :type request: SetActiveIdentityRequest
132
+ :return: Response indicating success and the activated identity
133
+ :rtype: SetActiveIdentityResponse
134
+ :raises ValueError: If the specified identity is invalid
135
+ :raises Exception: If setting the active identity fails
136
+
137
+ .. example::
138
+ >>> request = SetActiveIdentityRequest(identity_id="abc123")
139
+ >>> response = await set_active_identity(request)
140
+ >>> if response.success:
141
+ ... print("Identity activated successfully")
142
+ """
143
+ logger.info(f"Setting active identity: {request.identity_id}")
144
+
145
+ try:
146
+ auth_manager = get_auth_manager()
147
+
148
+ # Set the active identity
149
+ identity = await auth_manager.set_active_identity(request.identity_id)
150
+
151
+ # Try to pre-load credentials
152
+ credentials_loaded = False
153
+ message = None
154
+ try:
155
+ await auth_manager.get_active_credentials()
156
+ credentials_loaded = True
157
+ logger.info("Credentials pre-loaded successfully")
158
+ except Exception as e:
159
+ message = f"Identity set but credentials not loaded: {str(e)}"
160
+ logger.warning(message)
161
+
162
+ return SetActiveIdentityResponse(
163
+ success=True,
164
+ identity=identity,
165
+ credentials_loaded=credentials_loaded,
166
+ message=message,
167
+ )
168
+
169
+ except ValueError as e:
170
+ logger.error(f"Invalid identity: {e}")
171
+ raise
172
+ except Exception as e:
173
+ logger.error(f"Failed to set active identity: {e}")
174
+ raise
175
+
176
+
177
+ async def get_identity_info(identity_id: str) -> Optional[Identity]:
178
+ """Get detailed information about a specific remote identity.
179
+
180
+ Retrieves detailed information about a specific remote identity
181
+ by its unique identifier. This function is useful for getting
182
+ additional details about an identity before selecting it.
183
+
184
+ The function queries the authentication manager for the specified
185
+ identity and returns its details including attributes and metadata.
186
+
187
+ :param identity_id: The ID of the identity to retrieve
188
+ :type identity_id: str
189
+ :return: The Identity if found, None otherwise
190
+ :rtype: Optional[Identity]
191
+ :raises Exception: If retrieving identity info fails
192
+
193
+ .. example::
194
+ >>> identity = await get_identity_info("abc123")
195
+ >>> if identity:
196
+ ... name = identity.attributes.get("name", "Unknown")
197
+ ... print(f"Identity: {name}")
198
+ """
199
+ logger.info(f"Getting identity info for: {identity_id}")
200
+
201
+ try:
202
+ auth_manager = get_auth_manager()
203
+ identity = await auth_manager.get_identity(identity_id)
204
+
205
+ if identity:
206
+ name = identity.attributes.get("name", identity.id)
207
+ logger.info(f"Found identity: {name}")
208
+ else:
209
+ logger.info(f"Identity not found: {identity_id}")
210
+
211
+ return identity
212
+ except Exception as e:
213
+ logger.error(f"Failed to get identity info: {e}")
214
+ raise
215
+
216
+
217
+ async def list_identities() -> IdentityListResponse:
218
+ """List all available identities.
219
+
220
+ Retrieves a list of all available identities from the current
221
+ authentication provider. This is a simplified wrapper around
222
+ list_remote_identities for common use cases.
223
+
224
+ This function uses the default identity type filter and is equivalent
225
+ to calling list_remote_identities() with no parameters.
226
+
227
+ :return: List of available identities
228
+ :rtype: IdentityListResponse
229
+ :raises Exception: If listing identities fails
230
+
231
+ .. example::
232
+ >>> identities = await list_identities()
233
+ >>> for identity in identities.identities:
234
+ ... print(f"ID: {identity.id}")
235
+ """
236
+ return await list_remote_identities()