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,150 @@
|
|
|
1
|
+
"""Profile management tools for Amazon Ads MCP.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing Amazon Ads profiles,
|
|
4
|
+
including setting and getting the active profile ID.
|
|
5
|
+
|
|
6
|
+
The tools handle profile selection for API operations and
|
|
7
|
+
provide fallback mechanisms to default profiles when needed.
|
|
8
|
+
|
|
9
|
+
Key Features:
|
|
10
|
+
|
|
11
|
+
- Set active profile ID for API scope headers
|
|
12
|
+
- Retrieve current active profile with source information
|
|
13
|
+
- Clear profile settings to fall back to defaults
|
|
14
|
+
- Comprehensive error handling and logging
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
>>> result = await set_active_profile("123456789")
|
|
18
|
+
>>> profile_info = await get_active_profile()
|
|
19
|
+
>>> clear_result = await clear_active_profile()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
from ..auth.manager import get_auth_manager
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def set_active_profile(profile_id: str) -> dict:
|
|
30
|
+
"""Set the active Amazon Ads profile ID.
|
|
31
|
+
|
|
32
|
+
Sets the profile ID to be used in the Amazon-Advertising-API-Scope
|
|
33
|
+
header for subsequent API calls. This profile ID is associated with
|
|
34
|
+
the current active identity and overrides any default profile.
|
|
35
|
+
|
|
36
|
+
The profile ID will be used for all API requests until cleared or
|
|
37
|
+
changed. This setting is per-identity and does not affect other
|
|
38
|
+
active identities.
|
|
39
|
+
|
|
40
|
+
:param profile_id: The Amazon Ads profile ID to use
|
|
41
|
+
:type profile_id: str
|
|
42
|
+
:return: Success response with profile ID confirmation
|
|
43
|
+
:rtype: dict
|
|
44
|
+
:raises Exception: If setting the active profile fails
|
|
45
|
+
|
|
46
|
+
.. example::
|
|
47
|
+
>>> result = await set_active_profile("123456789")
|
|
48
|
+
>>> print(f"Profile set: {result['profile_id']}")
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
auth_manager = get_auth_manager()
|
|
52
|
+
auth_manager.set_active_profile_id(profile_id)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"success": True,
|
|
56
|
+
"profile_id": profile_id,
|
|
57
|
+
"message": f"Active profile set to {profile_id}",
|
|
58
|
+
}
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Failed to set active profile: {e}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def get_active_profile() -> dict:
|
|
65
|
+
"""Get the currently active Amazon Ads profile ID.
|
|
66
|
+
|
|
67
|
+
Returns the profile ID that will be used for API requests,
|
|
68
|
+
which could be from:
|
|
69
|
+
|
|
70
|
+
1. Explicitly set profile for current identity
|
|
71
|
+
2. Default from AMAZON_ADS_PROFILE_ID environment variable
|
|
72
|
+
3. None if no profile is set
|
|
73
|
+
|
|
74
|
+
The response includes information about the source of the
|
|
75
|
+
profile ID for debugging purposes.
|
|
76
|
+
|
|
77
|
+
The function provides transparency about where the profile ID
|
|
78
|
+
originates from to help with troubleshooting API scope issues.
|
|
79
|
+
|
|
80
|
+
:return: Current profile information with source details
|
|
81
|
+
:rtype: dict
|
|
82
|
+
:raises Exception: If getting the active profile fails
|
|
83
|
+
|
|
84
|
+
.. example::
|
|
85
|
+
>>> profile_info = await get_active_profile()
|
|
86
|
+
>>> print(f"Profile: {profile_info.get('profile_id')}")
|
|
87
|
+
>>> print(f"Source: {profile_info.get('source')}")
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
auth_manager = get_auth_manager()
|
|
91
|
+
profile_id = auth_manager.get_active_profile_id()
|
|
92
|
+
|
|
93
|
+
if profile_id:
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"profile_id": profile_id,
|
|
97
|
+
"source": auth_manager.get_profile_source(),
|
|
98
|
+
}
|
|
99
|
+
else:
|
|
100
|
+
return {
|
|
101
|
+
"success": True,
|
|
102
|
+
"profile_id": None,
|
|
103
|
+
"message": "No active profile set",
|
|
104
|
+
}
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to get active profile: {e}")
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def clear_active_profile() -> dict:
|
|
111
|
+
"""Clear the active profile ID for the current identity.
|
|
112
|
+
|
|
113
|
+
Removes the explicitly set profile ID, falling back to the
|
|
114
|
+
default profile ID from environment if available. This is
|
|
115
|
+
useful for resetting profile selection to system defaults.
|
|
116
|
+
|
|
117
|
+
After clearing, the profile selection falls back to the
|
|
118
|
+
AMAZON_ADS_PROFILE_ID environment variable if set, or None
|
|
119
|
+
if no default is configured.
|
|
120
|
+
|
|
121
|
+
:return: Success response with fallback profile information
|
|
122
|
+
:rtype: dict
|
|
123
|
+
:raises Exception: If clearing the active profile fails
|
|
124
|
+
|
|
125
|
+
.. example::
|
|
126
|
+
>>> result = await clear_active_profile()
|
|
127
|
+
>>> if result.get('fallback_profile_id'):
|
|
128
|
+
... print(f"Fell back to: {result['fallback_profile_id']}")
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
auth_manager = get_auth_manager()
|
|
132
|
+
auth_manager.clear_active_profile_id()
|
|
133
|
+
|
|
134
|
+
# Check what we're falling back to
|
|
135
|
+
fallback_profile = auth_manager.get_active_profile_id()
|
|
136
|
+
|
|
137
|
+
if fallback_profile:
|
|
138
|
+
return {
|
|
139
|
+
"success": True,
|
|
140
|
+
"message": "Profile cleared",
|
|
141
|
+
"fallback_profile_id": fallback_profile,
|
|
142
|
+
}
|
|
143
|
+
else:
|
|
144
|
+
return {
|
|
145
|
+
"success": True,
|
|
146
|
+
"message": "Profile cleared, no fallback profile available",
|
|
147
|
+
}
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Failed to clear active profile: {e}")
|
|
150
|
+
raise
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Profile listing tools with server-side caching and bounded responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from ..auth.manager import get_auth_manager
|
|
12
|
+
from ..config.settings import Settings
|
|
13
|
+
from ..utils.http import get_http_client
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_CACHE_TTL_SECONDS = int(os.getenv("AMAZON_ADS_PROFILE_CACHE_TTL", "300"))
|
|
19
|
+
MAX_SEARCH_LIMIT = 50
|
|
20
|
+
DEFAULT_SEARCH_LIMIT = 50
|
|
21
|
+
MAX_PAGE_LIMIT = 100
|
|
22
|
+
DEFAULT_PAGE_LIMIT = 100
|
|
23
|
+
PROFILE_SELECTION_THRESHOLD = 50
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CacheEntry:
|
|
28
|
+
profiles: List[Dict[str, Any]]
|
|
29
|
+
timestamp: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProfileCache:
|
|
33
|
+
"""In-memory profile cache scoped by identity and region."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS) -> None:
|
|
36
|
+
self._cache: Dict[Tuple[str, str], CacheEntry] = {}
|
|
37
|
+
self._ttl_seconds = ttl_seconds
|
|
38
|
+
|
|
39
|
+
def _is_expired(self, entry: CacheEntry, now: float) -> bool:
|
|
40
|
+
return (now - entry.timestamp) > self._ttl_seconds
|
|
41
|
+
|
|
42
|
+
async def get_profiles(
|
|
43
|
+
self,
|
|
44
|
+
key: Tuple[str, str],
|
|
45
|
+
fetcher: Callable[[], Any],
|
|
46
|
+
force_refresh: bool = False,
|
|
47
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
48
|
+
now = time.time()
|
|
49
|
+
entry = self._cache.get(key)
|
|
50
|
+
|
|
51
|
+
if entry and not force_refresh and not self._is_expired(entry, now):
|
|
52
|
+
return entry.profiles, False
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
profiles = await fetcher()
|
|
56
|
+
self._cache[key] = CacheEntry(profiles=profiles, timestamp=now)
|
|
57
|
+
return profiles, False
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
if entry:
|
|
60
|
+
logger.warning("Using stale profile cache for %s: %s", key, exc)
|
|
61
|
+
return entry.profiles, True
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
def clear(self, key: Tuple[str, str]) -> None:
|
|
65
|
+
self._cache.pop(key, None)
|
|
66
|
+
|
|
67
|
+
def get_entry(self, key: Tuple[str, str]) -> Optional[CacheEntry]:
|
|
68
|
+
return self._cache.get(key)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_profile_cache = ProfileCache()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_cache_key() -> Tuple[str, str]:
|
|
75
|
+
auth_manager = get_auth_manager()
|
|
76
|
+
identity_id = auth_manager.get_active_identity_id() or "default"
|
|
77
|
+
region = auth_manager.get_active_region() or Settings().amazon_ads_region
|
|
78
|
+
return identity_id, region
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _apply_limit(limit: Optional[int], default: int, max_limit: int) -> int:
|
|
82
|
+
if limit is None:
|
|
83
|
+
return default
|
|
84
|
+
try:
|
|
85
|
+
value = int(limit)
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
return default
|
|
88
|
+
if value <= 0:
|
|
89
|
+
return default
|
|
90
|
+
return min(value, max_limit)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _normalize_profile(profile: Dict[str, Any]) -> Dict[str, str]:
|
|
94
|
+
account_info = profile.get("accountInfo") or {}
|
|
95
|
+
return {
|
|
96
|
+
"profile_id": str(profile.get("profileId", "")),
|
|
97
|
+
"name": str(account_info.get("name", "")),
|
|
98
|
+
"country_code": str(profile.get("countryCode", "")),
|
|
99
|
+
"type": str(account_info.get("type", "")),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _matches_filters(
|
|
104
|
+
profile: Dict[str, Any],
|
|
105
|
+
query: Optional[str],
|
|
106
|
+
country_code: Optional[str],
|
|
107
|
+
account_type: Optional[str],
|
|
108
|
+
) -> bool:
|
|
109
|
+
if country_code:
|
|
110
|
+
if str(profile.get("countryCode", "")).upper() != country_code.upper():
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
if account_type:
|
|
114
|
+
p_type = (profile.get("accountInfo") or {}).get("type", "")
|
|
115
|
+
if str(p_type).lower() != account_type.lower():
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
if query:
|
|
119
|
+
q = query.lower()
|
|
120
|
+
name = (profile.get("accountInfo") or {}).get("name", "")
|
|
121
|
+
profile_id = str(profile.get("profileId", "")).lower()
|
|
122
|
+
if q not in str(name).lower() and q not in profile_id:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _fetch_profiles() -> List[Dict[str, Any]]:
|
|
129
|
+
auth_manager = get_auth_manager()
|
|
130
|
+
credentials = await auth_manager.get_active_credentials()
|
|
131
|
+
base_url = credentials.base_url or Settings().region_endpoint
|
|
132
|
+
client = await get_http_client(
|
|
133
|
+
authenticated=True,
|
|
134
|
+
auth_manager=auth_manager,
|
|
135
|
+
base_url=base_url,
|
|
136
|
+
)
|
|
137
|
+
response = await client.get("/v2/profiles")
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
data = response.json()
|
|
140
|
+
return data if isinstance(data, list) else []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def _get_profiles_cached(force_refresh: bool = False) -> Tuple[List[Dict[str, Any]], bool]:
|
|
144
|
+
key = _get_cache_key()
|
|
145
|
+
return await _profile_cache.get_profiles(key, _fetch_profiles, force_refresh=force_refresh)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def get_profiles_cached(
|
|
149
|
+
force_refresh: bool = False,
|
|
150
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
151
|
+
"""Public wrapper for fetching cached profiles."""
|
|
152
|
+
return await _get_profiles_cached(force_refresh=force_refresh)
|
|
153
|
+
|
|
154
|
+
async def summarize_profiles() -> Dict[str, Any]:
|
|
155
|
+
profiles, stale = await _get_profiles_cached()
|
|
156
|
+
|
|
157
|
+
by_country: Dict[str, int] = {}
|
|
158
|
+
by_type: Dict[str, int] = {}
|
|
159
|
+
for profile in profiles:
|
|
160
|
+
country = str(profile.get("countryCode", ""))
|
|
161
|
+
account_type = str((profile.get("accountInfo") or {}).get("type", ""))
|
|
162
|
+
if country:
|
|
163
|
+
by_country[country] = by_country.get(country, 0) + 1
|
|
164
|
+
if account_type:
|
|
165
|
+
by_type[account_type] = by_type.get(account_type, 0) + 1
|
|
166
|
+
|
|
167
|
+
message = "Ask for a country or advertiser name to narrow results."
|
|
168
|
+
if stale:
|
|
169
|
+
message = "Using cached profile list; data may be stale."
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"total_count": len(profiles),
|
|
173
|
+
"by_country": by_country,
|
|
174
|
+
"by_type": by_type,
|
|
175
|
+
"message": message,
|
|
176
|
+
"stale": stale,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def search_profiles(
|
|
181
|
+
query: Optional[str] = None,
|
|
182
|
+
country_code: Optional[str] = None,
|
|
183
|
+
account_type: Optional[str] = None,
|
|
184
|
+
limit: Optional[int] = None,
|
|
185
|
+
) -> Dict[str, Any]:
|
|
186
|
+
profiles, stale = await _get_profiles_cached()
|
|
187
|
+
filtered = [
|
|
188
|
+
profile
|
|
189
|
+
for profile in profiles
|
|
190
|
+
if _matches_filters(profile, query, country_code, account_type)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
limit_value = _apply_limit(limit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT)
|
|
194
|
+
items = [_normalize_profile(profile) for profile in filtered[:limit_value]]
|
|
195
|
+
|
|
196
|
+
message = None
|
|
197
|
+
if stale:
|
|
198
|
+
message = "Using cached profile list; data may be stale."
|
|
199
|
+
|
|
200
|
+
total_count = len(filtered)
|
|
201
|
+
returned_count = len(items)
|
|
202
|
+
return {
|
|
203
|
+
"items": items,
|
|
204
|
+
"total_count": total_count,
|
|
205
|
+
"returned_count": returned_count,
|
|
206
|
+
"has_more": returned_count < total_count,
|
|
207
|
+
"message": message,
|
|
208
|
+
"stale": stale,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def page_profiles(
|
|
213
|
+
country_code: Optional[str] = None,
|
|
214
|
+
account_type: Optional[str] = None,
|
|
215
|
+
offset: int = 0,
|
|
216
|
+
limit: Optional[int] = None,
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
profiles, stale = await _get_profiles_cached()
|
|
219
|
+
filtered = [
|
|
220
|
+
profile
|
|
221
|
+
for profile in profiles
|
|
222
|
+
if _matches_filters(profile, None, country_code, account_type)
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
limit_value = _apply_limit(limit, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT)
|
|
226
|
+
try:
|
|
227
|
+
offset_value = int(offset)
|
|
228
|
+
except (TypeError, ValueError):
|
|
229
|
+
offset_value = 0
|
|
230
|
+
offset_value = max(0, offset_value)
|
|
231
|
+
page = filtered[offset_value : offset_value + limit_value]
|
|
232
|
+
items = [_normalize_profile(profile) for profile in page]
|
|
233
|
+
|
|
234
|
+
message = None
|
|
235
|
+
if stale:
|
|
236
|
+
message = "Using cached profile list; data may be stale."
|
|
237
|
+
|
|
238
|
+
total_count = len(filtered)
|
|
239
|
+
returned_count = len(items)
|
|
240
|
+
has_more = (offset_value + returned_count) < total_count
|
|
241
|
+
next_offset = offset_value + returned_count if has_more else None
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"items": items,
|
|
245
|
+
"total_count": total_count,
|
|
246
|
+
"returned_count": returned_count,
|
|
247
|
+
"has_more": has_more,
|
|
248
|
+
"next_offset": next_offset,
|
|
249
|
+
"message": message,
|
|
250
|
+
"stale": stale,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def refresh_profiles_cache() -> Dict[str, Any]:
|
|
255
|
+
"""Force refresh the cached profile list for the current identity and region."""
|
|
256
|
+
key = _get_cache_key()
|
|
257
|
+
try:
|
|
258
|
+
profiles, stale = await _get_profiles_cached(force_refresh=True)
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
return {
|
|
261
|
+
"success": False,
|
|
262
|
+
"total_count": 0,
|
|
263
|
+
"cache_timestamp": None,
|
|
264
|
+
"message": f"Failed to refresh profiles: {exc}",
|
|
265
|
+
"stale": False,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
entry = _profile_cache.get_entry(key)
|
|
269
|
+
cache_timestamp = entry.timestamp if entry else None
|
|
270
|
+
if stale:
|
|
271
|
+
return {
|
|
272
|
+
"success": False,
|
|
273
|
+
"total_count": len(profiles),
|
|
274
|
+
"cache_timestamp": cache_timestamp,
|
|
275
|
+
"message": "Refresh failed; using cached profile list.",
|
|
276
|
+
"stale": True,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
"success": True,
|
|
281
|
+
"total_count": len(profiles),
|
|
282
|
+
"cache_timestamp": cache_timestamp,
|
|
283
|
+
"message": "Profile cache refreshed.",
|
|
284
|
+
"stale": False,
|
|
285
|
+
}
|