airbyte-agent-mcp 0.1.30__py3-none-any.whl → 0.1.53__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/cloud_utils/__init__.py +5 -0
- airbyte_agent_mcp/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_mcp/_vendored/connector_sdk/connector_model_loader.py +31 -4
- airbyte_agent_mcp/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_mcp/_vendored/connector_sdk/executor/hosted_executor.py +93 -84
- airbyte_agent_mcp/_vendored/connector_sdk/executor/local_executor.py +93 -23
- airbyte_agent_mcp/_vendored/connector_sdk/extensions.py +42 -3
- airbyte_agent_mcp/_vendored/connector_sdk/http_client.py +50 -43
- airbyte_agent_mcp/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_mcp/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_mcp/_vendored/connector_sdk/observability/session.py +35 -28
- airbyte_agent_mcp/_vendored/connector_sdk/schema/base.py +2 -1
- airbyte_agent_mcp/_vendored/connector_sdk/schema/components.py +2 -1
- airbyte_agent_mcp/_vendored/connector_sdk/schema/operations.py +1 -1
- airbyte_agent_mcp/_vendored/connector_sdk/schema/security.py +10 -0
- airbyte_agent_mcp/_vendored/connector_sdk/telemetry/events.py +2 -1
- airbyte_agent_mcp/_vendored/connector_sdk/telemetry/tracker.py +3 -0
- airbyte_agent_mcp/_vendored/connector_sdk/types.py +7 -1
- airbyte_agent_mcp/config.py +1 -1
- {airbyte_agent_mcp-0.1.30.dist-info → airbyte_agent_mcp-0.1.53.dist-info}/METADATA +1 -1
- {airbyte_agent_mcp-0.1.30.dist-info → airbyte_agent_mcp-0.1.53.dist-info}/RECORD +22 -18
- {airbyte_agent_mcp-0.1.30.dist-info → airbyte_agent_mcp-0.1.53.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""AirbyteCloudClient for Airbyte Platform API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AirbyteCloudClient:
|
|
12
|
+
"""Client for interacting with Airbyte Platform APIs.
|
|
13
|
+
|
|
14
|
+
Handles authentication, token caching, and API calls to:
|
|
15
|
+
- Get bearer tokens for authentication
|
|
16
|
+
- Look up connector instances for users
|
|
17
|
+
- Execute connectors via the cloud API
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
client = AirbyteCloudClient(
|
|
21
|
+
client_id="your-client-id",
|
|
22
|
+
client_secret="your-client-secret"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Get a connector instance
|
|
26
|
+
instance_id = await client.get_connector_instance_id(
|
|
27
|
+
external_user_id="user-123",
|
|
28
|
+
connector_definition_id="stripe-def-456"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Execute the connector
|
|
32
|
+
result = await client.execute_connector(
|
|
33
|
+
instance_id=instance_id,
|
|
34
|
+
entity="customers",
|
|
35
|
+
action="list",
|
|
36
|
+
params={"limit": 10}
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
AUTH_BASE_URL = "https://cloud.airbyte.com" # For token endpoint
|
|
41
|
+
API_BASE_URL = "https://api.airbyte.ai" # For instance lookup & execution
|
|
42
|
+
|
|
43
|
+
def __init__(self, client_id: str, client_secret: str):
|
|
44
|
+
"""Initialize AirbyteCloudClient.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client_id: Airbyte client ID for authentication
|
|
48
|
+
client_secret: Airbyte client secret for authentication
|
|
49
|
+
"""
|
|
50
|
+
self._client_id = client_id
|
|
51
|
+
self._client_secret = client_secret
|
|
52
|
+
|
|
53
|
+
# Token cache (instance-level)
|
|
54
|
+
self._cached_token: str | None = None
|
|
55
|
+
self._token_expires_at: datetime | None = None
|
|
56
|
+
self._http_client = httpx.AsyncClient(
|
|
57
|
+
timeout=httpx.Timeout(300.0), # 5 minute timeout
|
|
58
|
+
follow_redirects=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def get_bearer_token(self) -> str:
|
|
62
|
+
"""Get bearer token for API authentication.
|
|
63
|
+
|
|
64
|
+
Caches the token and only requests a new one when the cached token
|
|
65
|
+
is expired or missing. Adds a 60-second buffer before expiration
|
|
66
|
+
to avoid edge cases.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Bearer token string
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
httpx.HTTPStatusError: If the token request fails with 4xx/5xx
|
|
73
|
+
httpx.RequestError: If the network request fails
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
token = await client.get_bearer_token()
|
|
77
|
+
# Use token in Authorization header: f"Bearer {token}"
|
|
78
|
+
"""
|
|
79
|
+
# Check if we have a cached token that hasn't expired
|
|
80
|
+
if self._cached_token and self._token_expires_at:
|
|
81
|
+
# Add 60 second buffer before expiration to avoid edge cases
|
|
82
|
+
now = datetime.now()
|
|
83
|
+
if now < self._token_expires_at:
|
|
84
|
+
# Token is still valid, return cached version
|
|
85
|
+
return self._cached_token
|
|
86
|
+
|
|
87
|
+
# Token is missing or expired, fetch a new one
|
|
88
|
+
url = f"{self.AUTH_BASE_URL}/api/v1/applications/token"
|
|
89
|
+
request_body = {
|
|
90
|
+
"client_id": self._client_id,
|
|
91
|
+
"client_secret": self._client_secret,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
response = await self._http_client.post(url, json=request_body)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
|
|
97
|
+
data = response.json()
|
|
98
|
+
access_token = data["access_token"]
|
|
99
|
+
expires_in = 15 * 60 # default 15 min expiry time * 60 seconds
|
|
100
|
+
|
|
101
|
+
# Calculate expiration time with 60 second buffer
|
|
102
|
+
expires_at = datetime.now() + timedelta(seconds=expires_in - 60)
|
|
103
|
+
self._cached_token = access_token
|
|
104
|
+
self._token_expires_at = expires_at
|
|
105
|
+
|
|
106
|
+
return access_token
|
|
107
|
+
|
|
108
|
+
async def get_connector_instance_id(
|
|
109
|
+
self,
|
|
110
|
+
external_user_id: str,
|
|
111
|
+
connector_definition_id: str,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Get connector instance ID for a user.
|
|
114
|
+
|
|
115
|
+
Looks up the connector instance that belongs to the specified user
|
|
116
|
+
and connector definition. Validates that exactly one instance exists.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
external_user_id: User identifier in the Airbyte system
|
|
120
|
+
connector_definition_id: UUID of the connector definition
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Connector instance ID (UUID string)
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If 0 or more than 1 instance is found
|
|
127
|
+
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
128
|
+
httpx.RequestError: If network request fails
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
instance_id = await client.get_connector_instance_id(
|
|
132
|
+
external_user_id="user-123",
|
|
133
|
+
connector_definition_id="550e8400-e29b-41d4-a716-446655440000"
|
|
134
|
+
)
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
token = await self.get_bearer_token()
|
|
138
|
+
url = f"{self.API_BASE_URL}/api/v1/connectors/instances_for_user"
|
|
139
|
+
params = {
|
|
140
|
+
"external_user_id": external_user_id,
|
|
141
|
+
"definition_id": connector_definition_id,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
145
|
+
response = await self._http_client.get(url, params=params, headers=headers)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
|
|
148
|
+
data = response.json()
|
|
149
|
+
instances = data["instances"]
|
|
150
|
+
|
|
151
|
+
if len(instances) == 0:
|
|
152
|
+
raise ValueError(f"No connector instance found for user '{external_user_id}' " f"and connector '{connector_definition_id}'")
|
|
153
|
+
|
|
154
|
+
if len(instances) > 1:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Multiple connector instances found for user '{external_user_id}' "
|
|
157
|
+
f"and connector '{connector_definition_id}'. Expected exactly 1, "
|
|
158
|
+
f"found {len(instances)}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
instance_id = instances[0]["id"]
|
|
162
|
+
return instance_id
|
|
163
|
+
|
|
164
|
+
async def execute_connector(
|
|
165
|
+
self,
|
|
166
|
+
instance_id: str,
|
|
167
|
+
entity: str,
|
|
168
|
+
action: str,
|
|
169
|
+
params: dict[str, Any] | None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Execute a connector operation.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
instance_id: Connector instance UUID
|
|
175
|
+
entity: Entity name (e.g., "customers", "invoices")
|
|
176
|
+
action: Operation action (e.g., "list", "get", "create")
|
|
177
|
+
params: Optional parameters for the operation
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Raw JSON response dict from the API
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
184
|
+
httpx.RequestError: If network request fails
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
result = await client.execute_connector(
|
|
188
|
+
instance_id="inst-123",
|
|
189
|
+
entity="customers",
|
|
190
|
+
action="list",
|
|
191
|
+
params={"limit": 10}
|
|
192
|
+
)
|
|
193
|
+
"""
|
|
194
|
+
token = await self.get_bearer_token()
|
|
195
|
+
url = f"{self.API_BASE_URL}/api/v1/connectors/instances/{instance_id}/execute"
|
|
196
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
197
|
+
request_body = {
|
|
198
|
+
"entity": entity,
|
|
199
|
+
"action": action,
|
|
200
|
+
"params": params,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
response = await self._http_client.post(url, json=request_body, headers=headers)
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
|
|
206
|
+
return response.json()
|
|
207
|
+
|
|
208
|
+
async def close(self):
|
|
209
|
+
"""Close the HTTP client.
|
|
210
|
+
|
|
211
|
+
Call this when you're done using the client to clean up resources.
|
|
212
|
+
"""
|
|
213
|
+
await self._http_client.aclose()
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
8
9
|
|
|
9
10
|
import jsonref
|
|
10
11
|
import yaml
|
|
@@ -105,7 +106,7 @@ def resolve_schema_refs(schema: Any, spec_dict: dict) -> dict[str, Any]:
|
|
|
105
106
|
|
|
106
107
|
try:
|
|
107
108
|
# Resolve all references
|
|
108
|
-
resolved_spec = jsonref.replace_refs(
|
|
109
|
+
resolved_spec = jsonref.replace_refs( # type: ignore[union-attr]
|
|
109
110
|
temp_spec,
|
|
110
111
|
base_uri="",
|
|
111
112
|
jsonschema=True, # Use JSONSchema draft 7 semantics
|
|
@@ -117,9 +118,11 @@ def resolve_schema_refs(schema: Any, spec_dict: dict) -> dict[str, Any]:
|
|
|
117
118
|
|
|
118
119
|
# Remove any remaining jsonref proxy objects by converting to plain dict
|
|
119
120
|
return _deproxy_schema(resolved_schema)
|
|
120
|
-
except (
|
|
121
|
+
except (AttributeError, KeyError, RecursionError, Exception):
|
|
121
122
|
# If resolution fails, return the original schema
|
|
122
123
|
# This allows the system to continue even with malformed $refs
|
|
124
|
+
# AttributeError covers the case where jsonref might be None
|
|
125
|
+
# Exception catches jsonref.JsonRefError and other jsonref exceptions
|
|
123
126
|
return schema_dict
|
|
124
127
|
|
|
125
128
|
|
|
@@ -390,23 +393,35 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
390
393
|
for entity_name, endpoints_dict in entities_map.items():
|
|
391
394
|
actions = list(endpoints_dict.keys())
|
|
392
395
|
|
|
393
|
-
# Get schema from components if available
|
|
396
|
+
# Get schema and stream_name from components if available
|
|
394
397
|
schema = None
|
|
398
|
+
entity_stream_name = None
|
|
395
399
|
if spec.components:
|
|
396
400
|
# Look for a schema matching the entity name
|
|
397
401
|
for schema_name, schema_def in spec.components.schemas.items():
|
|
398
402
|
if schema_def.x_airbyte_entity_name == entity_name or schema_name.lower() == entity_name.lower():
|
|
399
403
|
schema = schema_def.model_dump(by_alias=True)
|
|
404
|
+
entity_stream_name = schema_def.x_airbyte_stream_name
|
|
400
405
|
break
|
|
401
406
|
|
|
402
|
-
entity = EntityDefinition(
|
|
407
|
+
entity = EntityDefinition(
|
|
408
|
+
name=entity_name,
|
|
409
|
+
stream_name=entity_stream_name,
|
|
410
|
+
actions=actions,
|
|
411
|
+
endpoints=endpoints_dict,
|
|
412
|
+
schema=schema,
|
|
413
|
+
)
|
|
403
414
|
entities.append(entity)
|
|
404
415
|
|
|
405
416
|
# Extract retry config from x-airbyte-retry-config extension
|
|
406
417
|
retry_config = spec.info.x_airbyte_retry_config
|
|
418
|
+
connector_id = spec.info.x_airbyte_connector_id
|
|
419
|
+
if not connector_id:
|
|
420
|
+
raise InvalidOpenAPIError("Missing required x-airbyte-connector-id field")
|
|
407
421
|
|
|
408
422
|
# Create ConnectorModel
|
|
409
423
|
model = ConnectorModel(
|
|
424
|
+
id=connector_id,
|
|
410
425
|
name=name,
|
|
411
426
|
version=version,
|
|
412
427
|
base_url=base_url,
|
|
@@ -926,8 +941,20 @@ def load_connector_model(definition_path: str | Path) -> ConnectorModel:
|
|
|
926
941
|
)
|
|
927
942
|
entities.append(entity)
|
|
928
943
|
|
|
944
|
+
# Get connector ID
|
|
945
|
+
connector_id_value = connector_meta.get("id")
|
|
946
|
+
if connector_id_value:
|
|
947
|
+
# Try to parse as UUID (handles string UUIDs)
|
|
948
|
+
if isinstance(connector_id_value, str):
|
|
949
|
+
connector_id = UUID(connector_id_value)
|
|
950
|
+
else:
|
|
951
|
+
connector_id = connector_id_value
|
|
952
|
+
else:
|
|
953
|
+
raise ValueError
|
|
954
|
+
|
|
929
955
|
# Build ConnectorModel
|
|
930
956
|
model = ConnectorModel(
|
|
957
|
+
id=connector_id,
|
|
931
958
|
name=connector_meta["name"],
|
|
932
959
|
version=connector_meta.get("version", OPENAPI_DEFAULT_VERSION),
|
|
933
960
|
base_url=raw_definition.get("base_url", connector_meta.get("base_url", "")),
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
"""Hosted executor for proxying operations through the
|
|
1
|
+
"""Hosted executor for proxying operations through the cloud API."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
|
-
|
|
7
|
-
import httpx
|
|
8
5
|
from opentelemetry import trace
|
|
9
6
|
|
|
7
|
+
from ..cloud_utils import AirbyteCloudClient
|
|
8
|
+
|
|
10
9
|
from .models import (
|
|
11
10
|
ExecutionConfig,
|
|
12
11
|
ExecutionResult,
|
|
@@ -14,30 +13,36 @@ from .models import (
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class HostedExecutor:
|
|
17
|
-
"""Executor that proxies execution through the
|
|
16
|
+
"""Executor that proxies execution through the Airbyte Cloud API.
|
|
18
17
|
|
|
19
|
-
This is the "hosted mode" executor that makes HTTP calls to the
|
|
20
|
-
instead of directly calling external services. The
|
|
18
|
+
This is the "hosted mode" executor that makes HTTP calls to the cloud API
|
|
19
|
+
instead of directly calling external services. The cloud API handles all
|
|
21
20
|
connector logic, secrets management, and execution.
|
|
22
21
|
|
|
23
|
-
The
|
|
24
|
-
|
|
22
|
+
The executor takes an external_user_id and uses the AirbyteCloudClient to:
|
|
23
|
+
1. Authenticate with the Airbyte Platform (bearer token with caching)
|
|
24
|
+
2. Look up the user's connector instance
|
|
25
|
+
3. Execute the connector operation via the cloud API
|
|
25
26
|
|
|
26
27
|
Implements ExecutorProtocol.
|
|
27
28
|
|
|
28
29
|
Example:
|
|
30
|
+
# Create executor with user ID, credentials, and connector definition ID
|
|
29
31
|
executor = HostedExecutor(
|
|
30
|
-
|
|
32
|
+
external_user_id="user-123",
|
|
31
33
|
airbyte_client_id="client_abc123",
|
|
32
|
-
airbyte_client_secret="secret_xyz789"
|
|
34
|
+
airbyte_client_secret="secret_xyz789",
|
|
35
|
+
connector_definition_id="abc123-def456-ghi789",
|
|
33
36
|
)
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
# Execute an operation
|
|
39
|
+
execution_config = ExecutionConfig(
|
|
36
40
|
entity="customers",
|
|
37
|
-
action="list"
|
|
41
|
+
action="list",
|
|
42
|
+
params={"limit": 10}
|
|
38
43
|
)
|
|
39
44
|
|
|
40
|
-
result = await executor.execute(
|
|
45
|
+
result = await executor.execute(execution_config)
|
|
41
46
|
if result.success:
|
|
42
47
|
print(f"Data: {result.data}")
|
|
43
48
|
else:
|
|
@@ -46,53 +51,45 @@ class HostedExecutor:
|
|
|
46
51
|
|
|
47
52
|
def __init__(
|
|
48
53
|
self,
|
|
49
|
-
|
|
54
|
+
external_user_id: str,
|
|
50
55
|
airbyte_client_id: str,
|
|
51
56
|
airbyte_client_secret: str,
|
|
52
|
-
|
|
57
|
+
connector_definition_id: str,
|
|
53
58
|
):
|
|
54
59
|
"""Initialize hosted executor.
|
|
55
60
|
|
|
56
61
|
Args:
|
|
57
|
-
|
|
62
|
+
external_user_id: User identifier in the Airbyte system
|
|
58
63
|
airbyte_client_id: Airbyte client ID for authentication
|
|
59
64
|
airbyte_client_secret: Airbyte client secret for authentication
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
connector_definition_id: Connector definition ID used to look up
|
|
66
|
+
the user's connector instance.
|
|
62
67
|
|
|
63
68
|
Example:
|
|
64
69
|
executor = HostedExecutor(
|
|
65
|
-
|
|
66
|
-
airbyte_client_id="client_abc123",
|
|
67
|
-
airbyte_client_secret="secret_xyz789"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
# Or with custom API URL:
|
|
71
|
-
executor = HostedExecutor(
|
|
72
|
-
connector_id="my-connector-id",
|
|
70
|
+
external_user_id="user-123",
|
|
73
71
|
airbyte_client_id="client_abc123",
|
|
74
72
|
airbyte_client_secret="secret_xyz789",
|
|
75
|
-
|
|
73
|
+
connector_definition_id="abc123-def456-ghi789",
|
|
76
74
|
)
|
|
77
75
|
"""
|
|
78
|
-
self.
|
|
79
|
-
self.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# The async wrapper allows it to work with the protocol
|
|
86
|
-
self.client = httpx.Client(
|
|
87
|
-
timeout=httpx.Timeout(300.0), # 5 minute timeout
|
|
88
|
-
follow_redirects=True,
|
|
76
|
+
self._external_user_id = external_user_id
|
|
77
|
+
self._connector_definition_id = connector_definition_id
|
|
78
|
+
|
|
79
|
+
# Create AirbyteCloudClient for API interactions
|
|
80
|
+
self._cloud_client = AirbyteCloudClient(
|
|
81
|
+
client_id=airbyte_client_id,
|
|
82
|
+
client_secret=airbyte_client_secret,
|
|
89
83
|
)
|
|
90
84
|
|
|
91
85
|
async def execute(self, config: ExecutionConfig) -> ExecutionResult:
|
|
92
|
-
"""Execute connector via
|
|
86
|
+
"""Execute connector via cloud API (ExecutorProtocol implementation).
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
Flow:
|
|
89
|
+
1. Get connector id from connector model
|
|
90
|
+
2. Look up the user's connector instance ID
|
|
91
|
+
3. Execute the connector operation via the cloud API
|
|
92
|
+
4. Parse the response into ExecutionResult
|
|
96
93
|
|
|
97
94
|
Args:
|
|
98
95
|
config: Execution configuration (entity, action, params)
|
|
@@ -101,88 +98,100 @@ class HostedExecutor:
|
|
|
101
98
|
ExecutionResult with success/failure status
|
|
102
99
|
|
|
103
100
|
Raises:
|
|
101
|
+
ValueError: If no instance or multiple instances found for user
|
|
104
102
|
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
105
103
|
httpx.RequestError: If network request fails
|
|
106
104
|
|
|
107
105
|
Example:
|
|
108
106
|
config = ExecutionConfig(
|
|
109
107
|
entity="customers",
|
|
110
|
-
action="list"
|
|
108
|
+
action="list",
|
|
109
|
+
params={"limit": 10}
|
|
111
110
|
)
|
|
112
111
|
result = await executor.execute(config)
|
|
113
112
|
"""
|
|
114
113
|
tracer = trace.get_tracer("airbyte.connector-sdk.executor.hosted")
|
|
115
114
|
|
|
116
115
|
with tracer.start_as_current_span("airbyte.hosted_executor.execute") as span:
|
|
117
|
-
# Add span attributes
|
|
118
|
-
span.set_attribute("connector.
|
|
116
|
+
# Add span attributes for observability
|
|
117
|
+
span.set_attribute("connector.definition_id", self._connector_definition_id)
|
|
119
118
|
span.set_attribute("connector.entity", config.entity)
|
|
120
119
|
span.set_attribute("connector.action", config.action)
|
|
121
|
-
span.set_attribute("
|
|
120
|
+
span.set_attribute("user.external_id", self._external_user_id)
|
|
122
121
|
if config.params:
|
|
123
122
|
# Only add non-sensitive param keys
|
|
124
123
|
span.set_attribute("connector.param_keys", list(config.params.keys()))
|
|
125
124
|
|
|
126
|
-
# Build API URL from instance api_url
|
|
127
|
-
url = f"{self.api_url}/connectors/{self.connector_id}/execute"
|
|
128
|
-
span.set_attribute("http.url", url)
|
|
129
|
-
|
|
130
|
-
# Build request body matching ExecutionRequest model
|
|
131
|
-
# Extract entity, action, and params from config attributes
|
|
132
|
-
request_body = {
|
|
133
|
-
"entity": config.entity,
|
|
134
|
-
"action": config.action,
|
|
135
|
-
"params": config.params,
|
|
136
|
-
}
|
|
137
|
-
|
|
138
125
|
try:
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
126
|
+
# Step 1: Get connector definition id
|
|
127
|
+
connector_definition_id = self._connector_definition_id
|
|
128
|
+
|
|
129
|
+
# Step 2: Get the connector instance ID for this user
|
|
130
|
+
instance_id = await self._cloud_client.get_connector_instance_id(
|
|
131
|
+
external_user_id=self._external_user_id,
|
|
132
|
+
connector_definition_id=connector_definition_id,
|
|
133
|
+
)
|
|
142
134
|
|
|
143
|
-
|
|
144
|
-
span.set_attribute("http.status_code", response.status_code)
|
|
135
|
+
span.set_attribute("connector.instance_id", instance_id)
|
|
145
136
|
|
|
146
|
-
#
|
|
147
|
-
response.
|
|
137
|
+
# Step 3: Execute the connector via the cloud API
|
|
138
|
+
response = await self._cloud_client.execute_connector(
|
|
139
|
+
instance_id=instance_id,
|
|
140
|
+
entity=config.entity,
|
|
141
|
+
action=config.action,
|
|
142
|
+
params=config.params,
|
|
143
|
+
)
|
|
148
144
|
|
|
149
|
-
# Parse
|
|
150
|
-
|
|
145
|
+
# Step 4: Parse the response into ExecutionResult
|
|
146
|
+
# The response_data is a dict from the API
|
|
147
|
+
result = self._parse_execution_result(response)
|
|
151
148
|
|
|
152
149
|
# Mark span as successful
|
|
153
|
-
span.set_attribute("connector.success",
|
|
150
|
+
span.set_attribute("connector.success", result.success)
|
|
154
151
|
|
|
155
|
-
|
|
156
|
-
return ExecutionResult(success=True, data=result_data, error=None)
|
|
152
|
+
return result
|
|
157
153
|
|
|
158
|
-
except
|
|
159
|
-
#
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
# Instance lookup validation error (0 or >1 instances)
|
|
160
156
|
span.set_attribute("connector.success", False)
|
|
161
|
-
span.set_attribute("connector.error_type", "
|
|
162
|
-
span.set_attribute("http.status_code", e.response.status_code)
|
|
157
|
+
span.set_attribute("connector.error_type", "ValueError")
|
|
163
158
|
span.record_exception(e)
|
|
164
159
|
raise
|
|
165
160
|
|
|
166
161
|
except Exception as e:
|
|
167
|
-
#
|
|
162
|
+
# HTTP errors and other exceptions
|
|
168
163
|
span.set_attribute("connector.success", False)
|
|
169
164
|
span.set_attribute("connector.error_type", type(e).__name__)
|
|
170
165
|
span.record_exception(e)
|
|
171
166
|
raise
|
|
172
167
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
168
|
+
def _parse_execution_result(self, response: dict) -> ExecutionResult:
|
|
169
|
+
"""Parse API response into ExecutionResult.
|
|
175
170
|
|
|
176
|
-
|
|
171
|
+
Args:
|
|
172
|
+
response_data: Raw JSON response from the cloud API
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
ExecutionResult with parsed data
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
return ExecutionResult(
|
|
179
|
+
success=True,
|
|
180
|
+
data=response["result"],
|
|
181
|
+
meta=response.get("connector_metadata"),
|
|
182
|
+
error=None,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def close(self):
|
|
186
|
+
"""Close the cloud client and cleanup resources.
|
|
187
|
+
|
|
188
|
+
Call this when you're done using the executor to clean up HTTP connections.
|
|
177
189
|
|
|
178
190
|
Example:
|
|
179
|
-
executor = HostedExecutor(
|
|
180
|
-
workspace_id="workspace-123",
|
|
181
|
-
connector_id="my-connector"
|
|
182
|
-
)
|
|
191
|
+
executor = HostedExecutor(...)
|
|
183
192
|
try:
|
|
184
193
|
result = await executor.execute(config)
|
|
185
194
|
finally:
|
|
186
|
-
executor.close()
|
|
195
|
+
await executor.close()
|
|
187
196
|
"""
|
|
188
|
-
self.
|
|
197
|
+
await self._cloud_client.close()
|