airbyte-agent-amazon-ads 0.1.6__py3-none-any.whl → 0.1.12__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.
@@ -4,8 +4,11 @@ Amazon-Ads connector.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import inspect
8
+ import json
7
9
  import logging
8
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload
10
+ from functools import wraps
11
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, TypeVar, overload
9
12
  try:
10
13
  from typing import Literal
11
14
  except ImportError:
@@ -21,6 +24,9 @@ from .types import (
21
24
  SponsoredProductCampaignsGetParams,
22
25
  SponsoredProductCampaignsListParams,
23
26
  SponsoredProductCampaignsListParamsStatefilter,
27
+ AirbyteSearchParams,
28
+ ProfilesSearchFilter,
29
+ ProfilesSearchQuery,
24
30
  )
25
31
  if TYPE_CHECKING:
26
32
  from .models import AmazonAdsAuthConfig
@@ -34,11 +40,47 @@ from .models import (
34
40
  Portfolio,
35
41
  Profile,
36
42
  SponsoredProductCampaign,
43
+ AirbyteSearchHit,
44
+ AirbyteSearchResult,
45
+ ProfilesSearchData,
46
+ ProfilesSearchResult,
37
47
  )
38
48
 
39
49
  # TypeVar for decorator type preservation
40
50
  _F = TypeVar("_F", bound=Callable[..., Any])
41
51
 
52
+ DEFAULT_MAX_OUTPUT_CHARS = 50_000 # ~50KB default, configurable per-tool
53
+
54
+
55
+ def _raise_output_too_large(message: str) -> None:
56
+ try:
57
+ from pydantic_ai import ModelRetry # type: ignore[import-not-found]
58
+ except Exception as exc:
59
+ raise RuntimeError(message) from exc
60
+ raise ModelRetry(message)
61
+
62
+
63
+ def _check_output_size(result: Any, max_chars: int | None, tool_name: str) -> Any:
64
+ if max_chars is None or max_chars <= 0:
65
+ return result
66
+
67
+ try:
68
+ serialized = json.dumps(result, default=str)
69
+ except (TypeError, ValueError):
70
+ return result
71
+
72
+ if len(serialized) > max_chars:
73
+ truncated_preview = serialized[:500] + "..." if len(serialized) > 500 else serialized
74
+ _raise_output_too_large(
75
+ f"Tool '{tool_name}' output too large ({len(serialized):,} chars, limit {max_chars:,}). "
76
+ "Please narrow your query by: using the 'fields' parameter to select only needed fields, "
77
+ "adding filters, or reducing the 'limit'. "
78
+ f"Preview: {truncated_preview}"
79
+ )
80
+
81
+ return result
82
+
83
+
42
84
 
43
85
 
44
86
  class AmazonAdsConnector:
@@ -49,7 +91,7 @@ class AmazonAdsConnector:
49
91
  """
50
92
 
51
93
  connector_name = "amazon-ads"
52
- connector_version = "1.0.1"
94
+ connector_version = "1.0.3"
53
95
  vendored_sdk_version = "0.1.0" # Version of vendored connector-sdk
54
96
 
55
97
  # Map of (entity, action) -> needs_envelope for envelope wrapping decision
@@ -80,7 +122,7 @@ class AmazonAdsConnector:
80
122
  airbyte_client_id: str | None = None,
81
123
  airbyte_client_secret: str | None = None,
82
124
  on_token_refresh: Any | None = None,
83
- region_url: str | None = None ):
125
+ region: str | None = None ):
84
126
  """
85
127
  Initialize a new amazon-ads connector instance.
86
128
 
@@ -95,7 +137,7 @@ class AmazonAdsConnector:
95
137
  airbyte_client_secret: Airbyte OAuth client secret (required for hosted mode)
96
138
  on_token_refresh: Optional callback for OAuth2 token refresh persistence.
97
139
  Called with new_tokens dict when tokens are refreshed. Can be sync or async.
98
- Example: lambda tokens: save_to_database(tokens) region_url: The Amazon Ads API endpoint URL based on region:
140
+ Example: lambda tokens: save_to_database(tokens) region: The Amazon Ads API endpoint URL based on region:
99
141
  - NA (North America): https://advertising-api.amazon.com
100
142
  - EU (Europe): https://advertising-api-eu.amazon.com
101
143
  - FE (Far East): https://advertising-api-fe.amazon.com
@@ -142,8 +184,8 @@ class AmazonAdsConnector:
142
184
 
143
185
  # Build config_values dict from server variables
144
186
  config_values: dict[str, str] = {}
145
- if region_url:
146
- config_values["region_url"] = region_url
187
+ if region:
188
+ config_values["region"] = region
147
189
 
148
190
  self._executor = LocalExecutor(
149
191
  model=AmazonAdsConnectorModel,
@@ -154,8 +196,8 @@ class AmazonAdsConnector:
154
196
 
155
197
  # Update base_url with server variables if provided
156
198
  base_url = self._executor.http_client.base_url
157
- if region_url:
158
- base_url = base_url.replace("{region_url}", region_url)
199
+ if region:
200
+ base_url = base_url.replace("{region}", region)
159
201
  self._executor.http_client.base_url = base_url
160
202
 
161
203
  # Initialize entity query objects
@@ -218,15 +260,15 @@ class AmazonAdsConnector:
218
260
  async def execute(
219
261
  self,
220
262
  entity: str,
221
- action: str,
222
- params: dict[str, Any]
263
+ action: Literal["list", "get", "search"],
264
+ params: Mapping[str, Any]
223
265
  ) -> AmazonAdsExecuteResult[Any] | AmazonAdsExecuteResultWithMeta[Any, Any] | Any: ...
224
266
 
225
267
  async def execute(
226
268
  self,
227
269
  entity: str,
228
- action: str,
229
- params: dict[str, Any] | None = None
270
+ action: Literal["list", "get", "search"],
271
+ params: Mapping[str, Any] | None = None
230
272
  ) -> Any:
231
273
  """
232
274
  Execute an entity operation with full type safety.
@@ -254,16 +296,17 @@ class AmazonAdsConnector:
254
296
  from ._vendored.connector_sdk.executor import ExecutionConfig
255
297
 
256
298
  # Remap parameter names from snake_case (TypedDict keys) to API parameter names
257
- if params:
299
+ resolved_params = dict(params) if params is not None else None
300
+ if resolved_params:
258
301
  param_map = self._PARAM_MAP.get((entity, action), {})
259
302
  if param_map:
260
- params = {param_map.get(k, k): v for k, v in params.items()}
303
+ resolved_params = {param_map.get(k, k): v for k, v in resolved_params.items()}
261
304
 
262
305
  # Use ExecutionConfig for both local and hosted executors
263
306
  config = ExecutionConfig(
264
307
  entity=entity,
265
308
  action=action,
266
- params=params
309
+ params=resolved_params
267
310
  )
268
311
 
269
312
  result = await self._executor.execute(config)
@@ -290,41 +333,67 @@ class AmazonAdsConnector:
290
333
  # ===== INTROSPECTION METHODS =====
291
334
 
292
335
  @classmethod
293
- def describe(cls, func: _F) -> _F:
336
+ def tool_utils(
337
+ cls,
338
+ func: _F | None = None,
339
+ *,
340
+ update_docstring: bool = True,
341
+ max_output_chars: int | None = DEFAULT_MAX_OUTPUT_CHARS,
342
+ ) -> _F | Callable[[_F], _F]:
294
343
  """
295
- Decorator that populates a function's docstring with connector capabilities.
296
-
297
- This class method can be used as a decorator to automatically generate
298
- comprehensive documentation for AI tool functions.
344
+ Decorator that adds tool utilities like docstring augmentation and output limits.
299
345
 
300
346
  Usage:
301
347
  @mcp.tool()
302
- @AmazonAdsConnector.describe
348
+ @AmazonAdsConnector.tool_utils
303
349
  async def execute(entity: str, action: str, params: dict):
304
- '''Execute operations.'''
305
350
  ...
306
351
 
307
- The decorated function's __doc__ will be updated with:
308
- - Available entities and their actions
309
- - Parameter signatures with required (*) and optional (?) markers
310
- - Response structure documentation
311
- - Example questions (if available in OpenAPI spec)
352
+ @mcp.tool()
353
+ @AmazonAdsConnector.tool_utils(update_docstring=False, max_output_chars=None)
354
+ async def execute(entity: str, action: str, params: dict):
355
+ ...
312
356
 
313
357
  Args:
314
- func: The function to decorate
315
-
316
- Returns:
317
- The same function with updated __doc__
358
+ update_docstring: When True, append connector capabilities to __doc__.
359
+ max_output_chars: Max serialized output size before raising. Use None to disable.
318
360
  """
319
- description = generate_tool_description(AmazonAdsConnectorModel)
320
361
 
321
- original_doc = func.__doc__ or ""
322
- if original_doc.strip():
323
- func.__doc__ = f"{original_doc.strip()}\n{description}"
324
- else:
325
- func.__doc__ = description
362
+ def decorate(inner: _F) -> _F:
363
+ if update_docstring:
364
+ description = generate_tool_description(AmazonAdsConnectorModel)
365
+ original_doc = inner.__doc__ or ""
366
+ if original_doc.strip():
367
+ full_doc = f"{original_doc.strip()}\n{description}"
368
+ else:
369
+ full_doc = description
370
+ else:
371
+ full_doc = ""
326
372
 
327
- return func
373
+ if inspect.iscoroutinefunction(inner):
374
+
375
+ @wraps(inner)
376
+ async def aw(*args: Any, **kwargs: Any) -> Any:
377
+ result = await inner(*args, **kwargs)
378
+ return _check_output_size(result, max_output_chars, inner.__name__)
379
+
380
+ wrapped = aw
381
+ else:
382
+
383
+ @wraps(inner)
384
+ def sw(*args: Any, **kwargs: Any) -> Any:
385
+ result = inner(*args, **kwargs)
386
+ return _check_output_size(result, max_output_chars, inner.__name__)
387
+
388
+ wrapped = sw
389
+
390
+ if update_docstring:
391
+ wrapped.__doc__ = full_doc
392
+ return wrapped # type: ignore[return-value]
393
+
394
+ if func is not None:
395
+ return decorate(func)
396
+ return decorate
328
397
 
329
398
  def list_entities(self) -> list[dict[str, Any]]:
330
399
  """
@@ -441,6 +510,65 @@ information about the advertiser's account in a specific marketplace.
441
510
 
442
511
 
443
512
 
513
+ async def search(
514
+ self,
515
+ query: ProfilesSearchQuery,
516
+ limit: int | None = None,
517
+ cursor: str | None = None,
518
+ fields: list[list[str]] | None = None,
519
+ ) -> ProfilesSearchResult:
520
+ """
521
+ Search profiles records from Airbyte cache.
522
+
523
+ This operation searches cached data from Airbyte syncs.
524
+ Only available in hosted execution mode.
525
+
526
+ Available filter fields (ProfilesSearchFilter):
527
+ - account_info:
528
+ - country_code:
529
+ - currency_code:
530
+ - daily_budget:
531
+ - profile_id:
532
+ - timezone:
533
+
534
+ Args:
535
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
536
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
537
+ limit: Maximum results to return (default 1000)
538
+ cursor: Pagination cursor from previous response's next_cursor
539
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
540
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
541
+
542
+ Returns:
543
+ ProfilesSearchResult with hits (list of AirbyteSearchHit[ProfilesSearchData]) and pagination info
544
+
545
+ Raises:
546
+ NotImplementedError: If called in local execution mode
547
+ """
548
+ params: dict[str, Any] = {"query": query}
549
+ if limit is not None:
550
+ params["limit"] = limit
551
+ if cursor is not None:
552
+ params["cursor"] = cursor
553
+ if fields is not None:
554
+ params["fields"] = fields
555
+
556
+ result = await self._connector.execute("profiles", "search", params)
557
+
558
+ # Parse response into typed result
559
+ return ProfilesSearchResult(
560
+ hits=[
561
+ AirbyteSearchHit[ProfilesSearchData](
562
+ id=hit.get("id"),
563
+ score=hit.get("score"),
564
+ data=ProfilesSearchData(**hit.get("data", {}))
565
+ )
566
+ for hit in result.get("hits", [])
567
+ ],
568
+ next_cursor=result.get("next_cursor"),
569
+ took_ms=result.get("took_ms")
570
+ )
571
+
444
572
  class PortfoliosQuery:
445
573
  """
446
574
  Query class for Portfolios entity operations.