airbyte-agent-mcp 0.1.64__py3-none-any.whl → 0.1.68__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.
- airbyte_agent_mcp/_vendored/connector_sdk/executor/local_executor.py +109 -20
- airbyte_agent_mcp/_vendored/connector_sdk/http_client.py +11 -6
- {airbyte_agent_mcp-0.1.64.dist-info → airbyte_agent_mcp-0.1.68.dist-info}/METADATA +1 -1
- {airbyte_agent_mcp-0.1.64.dist-info → airbyte_agent_mcp-0.1.68.dist-info}/RECORD +5 -5
- {airbyte_agent_mcp-0.1.64.dist-info → airbyte_agent_mcp-0.1.68.dist-info}/WHEEL +0 -0
|
@@ -495,6 +495,14 @@ class LocalExecutor:
|
|
|
495
495
|
print(result.data)
|
|
496
496
|
"""
|
|
497
497
|
try:
|
|
498
|
+
# Check for hosted-only actions before converting to Action enum
|
|
499
|
+
if config.action == "search":
|
|
500
|
+
raise NotImplementedError(
|
|
501
|
+
"search is only available in hosted execution mode. "
|
|
502
|
+
"Initialize the connector with external_user_id, airbyte_client_id, "
|
|
503
|
+
"and airbyte_client_secret to use this feature."
|
|
504
|
+
)
|
|
505
|
+
|
|
498
506
|
# Convert config to internal format
|
|
499
507
|
action = Action(config.action) if isinstance(config.action, str) else config.action
|
|
500
508
|
params = config.params or {}
|
|
@@ -1214,15 +1222,22 @@ class LocalExecutor:
|
|
|
1214
1222
|
def _extract_metadata(
|
|
1215
1223
|
self,
|
|
1216
1224
|
response_data: dict[str, Any],
|
|
1225
|
+
response_headers: dict[str, str],
|
|
1217
1226
|
endpoint: EndpointDefinition,
|
|
1218
1227
|
) -> dict[str, Any] | None:
|
|
1219
1228
|
"""Extract metadata from response using meta extractor.
|
|
1220
1229
|
|
|
1221
|
-
Each field in meta_extractor dict is independently extracted using JSONPath
|
|
1230
|
+
Each field in meta_extractor dict is independently extracted using JSONPath
|
|
1231
|
+
for body extraction, or special prefixes for header extraction:
|
|
1232
|
+
- @link.{rel}: Extract URL from RFC 5988 Link header by rel type
|
|
1233
|
+
- @header.{name}: Extract raw header value by header name
|
|
1234
|
+
- Otherwise: JSONPath expression for body extraction
|
|
1235
|
+
|
|
1222
1236
|
Missing or invalid paths result in None for that field (no crash).
|
|
1223
1237
|
|
|
1224
1238
|
Args:
|
|
1225
1239
|
response_data: Full API response (before record extraction)
|
|
1240
|
+
response_headers: HTTP response headers
|
|
1226
1241
|
endpoint: Endpoint with optional meta extractor configuration
|
|
1227
1242
|
|
|
1228
1243
|
Returns:
|
|
@@ -1233,11 +1248,15 @@ class LocalExecutor:
|
|
|
1233
1248
|
Example:
|
|
1234
1249
|
meta_extractor = {
|
|
1235
1250
|
"pagination": "$.records",
|
|
1236
|
-
"request_id": "$.requestId"
|
|
1251
|
+
"request_id": "$.requestId",
|
|
1252
|
+
"next_page_url": "@link.next",
|
|
1253
|
+
"rate_limit": "@header.X-RateLimit-Remaining"
|
|
1237
1254
|
}
|
|
1238
1255
|
Returns: {
|
|
1239
1256
|
"pagination": {"cursor": "abc", "total": 100},
|
|
1240
|
-
"request_id": "xyz123"
|
|
1257
|
+
"request_id": "xyz123",
|
|
1258
|
+
"next_page_url": "https://api.example.com/data?cursor=abc",
|
|
1259
|
+
"rate_limit": "99"
|
|
1241
1260
|
}
|
|
1242
1261
|
"""
|
|
1243
1262
|
# Check if endpoint has meta extractor
|
|
@@ -1247,26 +1266,96 @@ class LocalExecutor:
|
|
|
1247
1266
|
extracted_meta: dict[str, Any] = {}
|
|
1248
1267
|
|
|
1249
1268
|
# Extract each field independently
|
|
1250
|
-
for field_name,
|
|
1269
|
+
for field_name, extractor_expr in endpoint.meta_extractor.items():
|
|
1251
1270
|
try:
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
#
|
|
1258
|
-
|
|
1271
|
+
if extractor_expr.startswith("@link."):
|
|
1272
|
+
# RFC 5988 Link header extraction
|
|
1273
|
+
rel = extractor_expr[6:]
|
|
1274
|
+
extracted_meta[field_name] = self._extract_link_url(response_headers, rel)
|
|
1275
|
+
elif extractor_expr.startswith("@header."):
|
|
1276
|
+
# Raw header value extraction (case-insensitive lookup)
|
|
1277
|
+
header_name = extractor_expr[8:]
|
|
1278
|
+
extracted_meta[field_name] = self._get_header_value(response_headers, header_name)
|
|
1259
1279
|
else:
|
|
1260
|
-
#
|
|
1261
|
-
|
|
1280
|
+
# JSONPath body extraction
|
|
1281
|
+
jsonpath_expr = parse_jsonpath(extractor_expr)
|
|
1282
|
+
matches = [match.value for match in jsonpath_expr.find(response_data)]
|
|
1283
|
+
|
|
1284
|
+
if matches:
|
|
1285
|
+
# Return first match (most common case)
|
|
1286
|
+
extracted_meta[field_name] = matches[0]
|
|
1287
|
+
else:
|
|
1288
|
+
# Path not found - set to None
|
|
1289
|
+
extracted_meta[field_name] = None
|
|
1262
1290
|
|
|
1263
1291
|
except Exception as e:
|
|
1264
1292
|
# Log error but continue with other fields
|
|
1265
|
-
logging.warning(f"Failed to apply meta extractor for field '{field_name}' with
|
|
1293
|
+
logging.warning(f"Failed to apply meta extractor for field '{field_name}' with expression '{extractor_expr}': {e}. Setting to None.")
|
|
1266
1294
|
extracted_meta[field_name] = None
|
|
1267
1295
|
|
|
1268
1296
|
return extracted_meta
|
|
1269
1297
|
|
|
1298
|
+
@staticmethod
|
|
1299
|
+
def _extract_link_url(headers: dict[str, str], rel: str) -> str | None:
|
|
1300
|
+
"""Extract URL from RFC 5988 Link header by rel type.
|
|
1301
|
+
|
|
1302
|
+
Parses Link header format: <url>; param1="value1"; rel="next"; param2="value2"
|
|
1303
|
+
|
|
1304
|
+
Supports:
|
|
1305
|
+
- Multiple parameters per link in any order
|
|
1306
|
+
- Both quoted and unquoted rel values
|
|
1307
|
+
- Multiple links separated by commas
|
|
1308
|
+
|
|
1309
|
+
Args:
|
|
1310
|
+
headers: Response headers dict
|
|
1311
|
+
rel: The rel type to extract (e.g., "next", "prev", "first", "last")
|
|
1312
|
+
|
|
1313
|
+
Returns:
|
|
1314
|
+
The URL for the specified rel type, or None if not found
|
|
1315
|
+
"""
|
|
1316
|
+
link_header = headers.get("Link") or headers.get("link", "")
|
|
1317
|
+
if not link_header:
|
|
1318
|
+
return None
|
|
1319
|
+
|
|
1320
|
+
for link_segment in re.split(r",(?=\s*<)", link_header):
|
|
1321
|
+
link_segment = link_segment.strip()
|
|
1322
|
+
|
|
1323
|
+
url_match = re.match(r"<([^>]+)>", link_segment)
|
|
1324
|
+
if not url_match:
|
|
1325
|
+
continue
|
|
1326
|
+
|
|
1327
|
+
url = url_match.group(1)
|
|
1328
|
+
params_str = link_segment[url_match.end() :]
|
|
1329
|
+
|
|
1330
|
+
rel_match = re.search(r';\s*rel="?([^";,]+)"?', params_str, re.IGNORECASE)
|
|
1331
|
+
if rel_match and rel_match.group(1).strip() == rel:
|
|
1332
|
+
return url
|
|
1333
|
+
|
|
1334
|
+
return None
|
|
1335
|
+
|
|
1336
|
+
@staticmethod
|
|
1337
|
+
def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
|
|
1338
|
+
"""Get header value with case-insensitive lookup.
|
|
1339
|
+
|
|
1340
|
+
Args:
|
|
1341
|
+
headers: Response headers dict
|
|
1342
|
+
header_name: Header name to look up
|
|
1343
|
+
|
|
1344
|
+
Returns:
|
|
1345
|
+
Header value or None if not found
|
|
1346
|
+
"""
|
|
1347
|
+
# Try exact match first
|
|
1348
|
+
if header_name in headers:
|
|
1349
|
+
return headers[header_name]
|
|
1350
|
+
|
|
1351
|
+
# Case-insensitive lookup
|
|
1352
|
+
header_name_lower = header_name.lower()
|
|
1353
|
+
for key, value in headers.items():
|
|
1354
|
+
if key.lower() == header_name_lower:
|
|
1355
|
+
return value
|
|
1356
|
+
|
|
1357
|
+
return None
|
|
1358
|
+
|
|
1270
1359
|
def _validate_required_body_fields(self, endpoint: Any, params: dict[str, Any], action: Action, entity: str) -> None:
|
|
1271
1360
|
"""Validate that required body fields are present for CREATE/UPDATE operations.
|
|
1272
1361
|
|
|
@@ -1394,7 +1483,7 @@ class _StandardOperationHandler:
|
|
|
1394
1483
|
request_kwargs = self.ctx.determine_request_format(endpoint, body)
|
|
1395
1484
|
|
|
1396
1485
|
# Execute async HTTP request
|
|
1397
|
-
|
|
1486
|
+
response_data, response_headers = await self.ctx.http_client.request(
|
|
1398
1487
|
method=endpoint.method,
|
|
1399
1488
|
path=path,
|
|
1400
1489
|
params=query_params if query_params else None,
|
|
@@ -1403,10 +1492,10 @@ class _StandardOperationHandler:
|
|
|
1403
1492
|
)
|
|
1404
1493
|
|
|
1405
1494
|
# Extract metadata from original response (before record extraction)
|
|
1406
|
-
metadata = self.ctx.executor._extract_metadata(
|
|
1495
|
+
metadata = self.ctx.executor._extract_metadata(response_data, response_headers, endpoint)
|
|
1407
1496
|
|
|
1408
1497
|
# Extract records if extractor configured
|
|
1409
|
-
response = self.ctx.extract_records(
|
|
1498
|
+
response = self.ctx.extract_records(response_data, endpoint)
|
|
1410
1499
|
|
|
1411
1500
|
# Assume success with 200 status code if no exception raised
|
|
1412
1501
|
status_code = 200
|
|
@@ -1532,7 +1621,7 @@ class _DownloadOperationHandler:
|
|
|
1532
1621
|
request_format = self.ctx.determine_request_format(operation, request_body)
|
|
1533
1622
|
self.ctx.validate_required_body_fields(operation, params, action, entity)
|
|
1534
1623
|
|
|
1535
|
-
metadata_response = await self.ctx.http_client.request(
|
|
1624
|
+
metadata_response, _ = await self.ctx.http_client.request(
|
|
1536
1625
|
method=operation.method,
|
|
1537
1626
|
path=path,
|
|
1538
1627
|
params=query_params,
|
|
@@ -1547,7 +1636,7 @@ class _DownloadOperationHandler:
|
|
|
1547
1636
|
)
|
|
1548
1637
|
|
|
1549
1638
|
# Step 3: Stream file from extracted URL
|
|
1550
|
-
file_response = await self.ctx.http_client.request(
|
|
1639
|
+
file_response, _ = await self.ctx.http_client.request(
|
|
1551
1640
|
method="GET",
|
|
1552
1641
|
path=file_url,
|
|
1553
1642
|
headers=headers,
|
|
@@ -1555,7 +1644,7 @@ class _DownloadOperationHandler:
|
|
|
1555
1644
|
)
|
|
1556
1645
|
else:
|
|
1557
1646
|
# One-step direct download: stream file directly from endpoint
|
|
1558
|
-
file_response = await self.ctx.http_client.request(
|
|
1647
|
+
file_response, _ = await self.ctx.http_client.request(
|
|
1559
1648
|
method=operation.method,
|
|
1560
1649
|
path=path,
|
|
1561
1650
|
params=query_params,
|
|
@@ -421,10 +421,14 @@ class HTTPClient:
|
|
|
421
421
|
headers: dict[str, str] | None = None,
|
|
422
422
|
*,
|
|
423
423
|
stream: bool = False,
|
|
424
|
-
):
|
|
424
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
425
425
|
"""Execute a single HTTP request attempt (no retries).
|
|
426
426
|
|
|
427
427
|
This is the core request logic, separated from retry handling.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Tuple of (response_data, response_headers) for non-streaming requests.
|
|
431
|
+
For streaming requests, returns (response_object, response_headers).
|
|
428
432
|
"""
|
|
429
433
|
# Ensure auth credentials are initialized (proactive refresh if needed)
|
|
430
434
|
await self._ensure_auth_initialized()
|
|
@@ -475,7 +479,7 @@ class HTTPClient:
|
|
|
475
479
|
status_code=status_code,
|
|
476
480
|
response_body=f"<binary content, {response.headers.get('content-length', 'unknown')} bytes>",
|
|
477
481
|
)
|
|
478
|
-
return response
|
|
482
|
+
return response, dict(response.headers)
|
|
479
483
|
|
|
480
484
|
# Parse response - handle non-JSON responses gracefully
|
|
481
485
|
content_type = response.headers.get("content-type", "")
|
|
@@ -501,7 +505,7 @@ class HTTPClient:
|
|
|
501
505
|
status_code=status_code,
|
|
502
506
|
response_body=response_data,
|
|
503
507
|
)
|
|
504
|
-
return response_data
|
|
508
|
+
return response_data, dict(response.headers)
|
|
505
509
|
|
|
506
510
|
except AuthenticationError as e:
|
|
507
511
|
# Auth error (401, 403) - handle token refresh
|
|
@@ -631,7 +635,7 @@ class HTTPClient:
|
|
|
631
635
|
*,
|
|
632
636
|
stream: bool = False,
|
|
633
637
|
_auth_retry_attempted: bool = False,
|
|
634
|
-
):
|
|
638
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
635
639
|
"""Make an async HTTP request with optional streaming and automatic retries.
|
|
636
640
|
|
|
637
641
|
Args:
|
|
@@ -644,8 +648,9 @@ class HTTPClient:
|
|
|
644
648
|
stream: If True, do not eagerly read the body (useful for downloads)
|
|
645
649
|
|
|
646
650
|
Returns:
|
|
647
|
-
|
|
648
|
-
- If stream=
|
|
651
|
+
Tuple of (response_data, response_headers):
|
|
652
|
+
- If stream=False: (parsed JSON dict or empty dict, response headers dict)
|
|
653
|
+
- If stream=True: (response object suitable for streaming, response headers dict)
|
|
649
654
|
|
|
650
655
|
Raises:
|
|
651
656
|
HTTPStatusError: If request fails with 4xx/5xx status after all retries
|
|
@@ -14,7 +14,7 @@ airbyte_agent_mcp/_vendored/connector_sdk/connector_model_loader.py,sha256=b88ao
|
|
|
14
14
|
airbyte_agent_mcp/_vendored/connector_sdk/constants.py,sha256=AtzOvhDMWbRJgpsQNWl5tkogHD6mWgEY668PgRmgtOY,2737
|
|
15
15
|
airbyte_agent_mcp/_vendored/connector_sdk/exceptions.py,sha256=ss5MGv9eVPmsbLcLWetuu3sDmvturwfo6Pw3M37Oq5k,481
|
|
16
16
|
airbyte_agent_mcp/_vendored/connector_sdk/extensions.py,sha256=XWRRoJOOrwUHSKbuQt5DU7CCu8ePzhd_HuP7c_uD77w,21376
|
|
17
|
-
airbyte_agent_mcp/_vendored/connector_sdk/http_client.py,sha256=
|
|
17
|
+
airbyte_agent_mcp/_vendored/connector_sdk/http_client.py,sha256=S0eECrPVkjjro9xOhGA3QEwi2H0aWOwwoNZdfXd8OkE,27882
|
|
18
18
|
airbyte_agent_mcp/_vendored/connector_sdk/introspection.py,sha256=2CyKXZHT74-1Id97uw1RLeyOi6TV24_hoNbQ6-6y7uI,10335
|
|
19
19
|
airbyte_agent_mcp/_vendored/connector_sdk/secrets.py,sha256=J9ezMu4xNnLW11xY5RCre6DHP7YMKZCqwGJfk7ufHAM,6855
|
|
20
20
|
airbyte_agent_mcp/_vendored/connector_sdk/types.py,sha256=CStkOsLtmZZdXylkdCsbYORDzughxygt1-Ucma0j-qE,8287
|
|
@@ -24,7 +24,7 @@ airbyte_agent_mcp/_vendored/connector_sdk/cloud_utils/__init__.py,sha256=4799Hv9
|
|
|
24
24
|
airbyte_agent_mcp/_vendored/connector_sdk/cloud_utils/client.py,sha256=YxdRpQr9XjDzih6csSseBVGn9kfMtaqbOCXP0TPuzFY,7189
|
|
25
25
|
airbyte_agent_mcp/_vendored/connector_sdk/executor/__init__.py,sha256=EmG9YQNAjSuYCVB4D5VoLm4qpD1KfeiiOf7bpALj8p8,702
|
|
26
26
|
airbyte_agent_mcp/_vendored/connector_sdk/executor/hosted_executor.py,sha256=ydHcG-biRS1ITT5ELwPShdJW-KYpvK--Fos1ipNgHho,6995
|
|
27
|
-
airbyte_agent_mcp/_vendored/connector_sdk/executor/local_executor.py,sha256=
|
|
27
|
+
airbyte_agent_mcp/_vendored/connector_sdk/executor/local_executor.py,sha256=_RQXpBVXMlyvI2sL-F5N82nBKq7osZIn6H27Q1fse0g,71225
|
|
28
28
|
airbyte_agent_mcp/_vendored/connector_sdk/executor/models.py,sha256=lYVT_bNcw-PoIks4WHNyl2VY-lJVf2FntzINSOBIheE,5845
|
|
29
29
|
airbyte_agent_mcp/_vendored/connector_sdk/http/__init__.py,sha256=y8fbzZn-3yV9OxtYz8Dy6FFGI5v6TOqADd1G3xHH3Hw,911
|
|
30
30
|
airbyte_agent_mcp/_vendored/connector_sdk/http/config.py,sha256=6J7YIIwHC6sRu9i-yKa5XvArwK2KU60rlnmxzDZq3lw,3283
|
|
@@ -55,6 +55,6 @@ airbyte_agent_mcp/_vendored/connector_sdk/telemetry/__init__.py,sha256=RaLgkBU4d
|
|
|
55
55
|
airbyte_agent_mcp/_vendored/connector_sdk/telemetry/config.py,sha256=tLmQwAFD0kP1WyBGWBS3ysaudN9H3e-3EopKZi6cGKg,885
|
|
56
56
|
airbyte_agent_mcp/_vendored/connector_sdk/telemetry/events.py,sha256=8Y1NbXiwISX-V_wRofY7PqcwEXD0dLMnntKkY6XFU2s,1328
|
|
57
57
|
airbyte_agent_mcp/_vendored/connector_sdk/telemetry/tracker.py,sha256=Ftrk0_ddfM7dZG8hF9xBuPwhbc9D6JZ7Q9qs5o3LEyA,5579
|
|
58
|
-
airbyte_agent_mcp-0.1.
|
|
59
|
-
airbyte_agent_mcp-0.1.
|
|
60
|
-
airbyte_agent_mcp-0.1.
|
|
58
|
+
airbyte_agent_mcp-0.1.68.dist-info/METADATA,sha256=lm4gWhk2YW_kH_UT1um7uUMhDZY8bhl49jygjPRW4_A,3009
|
|
59
|
+
airbyte_agent_mcp-0.1.68.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
60
|
+
airbyte_agent_mcp-0.1.68.dist-info/RECORD,,
|
|
File without changes
|