airbyte-agent-zendesk-support 0.18.18__py3-none-any.whl → 0.18.39__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_zendesk_support/_vendored/connector_sdk/auth_strategies.py +2 -5
- airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_template.py +1 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/connector_model_loader.py +32 -6
- airbyte_agent_zendesk_support/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/hosted_executor.py +92 -84
- airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/local_executor.py +94 -25
- airbyte_agent_zendesk_support/_vendored/connector_sdk/extensions.py +43 -5
- airbyte_agent_zendesk_support/_vendored/connector_sdk/http/response.py +2 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/http_client.py +50 -43
- airbyte_agent_zendesk_support/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/logger.py +9 -9
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/types.py +10 -10
- airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/models.py +6 -6
- airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/session.py +41 -32
- airbyte_agent_zendesk_support/_vendored/connector_sdk/performance/metrics.py +3 -3
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/base.py +18 -17
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/components.py +59 -58
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/extensions.py +9 -9
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/operations.py +32 -32
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/security.py +44 -34
- airbyte_agent_zendesk_support/_vendored/connector_sdk/secrets.py +2 -2
- airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/events.py +9 -8
- airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/tracker.py +9 -5
- airbyte_agent_zendesk_support/_vendored/connector_sdk/types.py +9 -3
- airbyte_agent_zendesk_support/connector.py +98 -15
- airbyte_agent_zendesk_support/connector_model.py +7 -1
- airbyte_agent_zendesk_support/types.py +1 -1
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/METADATA +47 -26
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/RECORD +33 -29
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/WHEEL +0 -0
|
@@ -610,9 +610,7 @@ class OAuth2AuthStrategy(AuthStrategy):
|
|
|
610
610
|
has_refresh_token = bool(secrets.get("refresh_token"))
|
|
611
611
|
|
|
612
612
|
if not has_access_token and not has_refresh_token:
|
|
613
|
-
raise AuthenticationError(
|
|
614
|
-
"Missing OAuth2 credentials. Provide either 'access_token' " "or 'refresh_token' (for refresh-token-only mode)."
|
|
615
|
-
)
|
|
613
|
+
raise AuthenticationError("Missing OAuth2 credentials. Provide either 'access_token' or 'refresh_token' (for refresh-token-only mode).")
|
|
616
614
|
|
|
617
615
|
def can_refresh(self, secrets: OAuth2RefreshSecrets) -> bool:
|
|
618
616
|
"""Check if token refresh is possible.
|
|
@@ -1106,8 +1104,7 @@ class AuthStrategyFactory:
|
|
|
1106
1104
|
strategy = cls._strategies.get(auth_type)
|
|
1107
1105
|
if strategy is None:
|
|
1108
1106
|
raise AuthenticationError(
|
|
1109
|
-
f"Authentication type '{auth_type.value}' is not implemented. "
|
|
1110
|
-
f"Supported types: {', '.join(s.value for s in cls._strategies.keys())}"
|
|
1107
|
+
f"Authentication type '{auth_type.value}' is not implemented. Supported types: {', '.join(s.value for s in cls._strategies.keys())}"
|
|
1111
1108
|
)
|
|
1112
1109
|
return strategy
|
|
1113
1110
|
|
|
@@ -17,7 +17,7 @@ class MissingVariableError(ValueError):
|
|
|
17
17
|
def __init__(self, var_name: str, available_fields: list):
|
|
18
18
|
self.var_name = var_name
|
|
19
19
|
self.available_fields = available_fields
|
|
20
|
-
super().__init__(f"Template variable '${{{var_name}}}' not found in config.
|
|
20
|
+
super().__init__(f"Template variable '${{{var_name}}}' not found in config. Available fields: {available_fields}")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def apply_template(template: str, values: Dict[str, str]) -> str:
|
|
@@ -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 connectors 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 ID
|
|
26
|
+
connector_id = await client.get_connector_id(
|
|
27
|
+
external_user_id="user-123",
|
|
28
|
+
connector_definition_id="550e8400-e29b-41d4-a716-446655440000"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Execute the connector
|
|
32
|
+
result = await client.execute_connector(
|
|
33
|
+
connector_id=connector_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_id(
|
|
109
|
+
self,
|
|
110
|
+
external_user_id: str,
|
|
111
|
+
connector_definition_id: str,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Get connector ID for a user.
|
|
114
|
+
|
|
115
|
+
Looks up the connector that belongs to the specified user
|
|
116
|
+
and connector definition. Validates that exactly one connector 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 ID (UUID string)
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If 0 or more than 1 connector is found
|
|
127
|
+
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
128
|
+
httpx.RequestError: If network request fails
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
connector_id = await client.get_connector_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/connectors_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
|
+
connectors = data["connectors"]
|
|
150
|
+
|
|
151
|
+
if len(connectors) == 0:
|
|
152
|
+
raise ValueError(f"No connector found for user '{external_user_id}' and connector definition '{connector_definition_id}'")
|
|
153
|
+
|
|
154
|
+
if len(connectors) > 1:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Multiple connectors found for user '{external_user_id}' "
|
|
157
|
+
f"and connector definition '{connector_definition_id}'. Expected exactly 1, "
|
|
158
|
+
f"found {len(connectors)}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
connector_id = connectors[0]["id"]
|
|
162
|
+
return connector_id
|
|
163
|
+
|
|
164
|
+
async def execute_connector(
|
|
165
|
+
self,
|
|
166
|
+
connector_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
|
+
connector_id: Connector UUID (source ID)
|
|
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
|
+
connector_id="550e8400-e29b-41d4-a716-446655440000",
|
|
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/sources/{connector_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()
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import re
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
9
|
+
from uuid import UUID
|
|
8
10
|
|
|
9
11
|
import jsonref
|
|
10
12
|
import yaml
|
|
@@ -105,7 +107,7 @@ def resolve_schema_refs(schema: Any, spec_dict: dict) -> dict[str, Any]:
|
|
|
105
107
|
|
|
106
108
|
try:
|
|
107
109
|
# Resolve all references
|
|
108
|
-
resolved_spec = jsonref.replace_refs(
|
|
110
|
+
resolved_spec = jsonref.replace_refs( # type: ignore[union-attr]
|
|
109
111
|
temp_spec,
|
|
110
112
|
base_uri="",
|
|
111
113
|
jsonschema=True, # Use JSONSchema draft 7 semantics
|
|
@@ -117,9 +119,11 @@ def resolve_schema_refs(schema: Any, spec_dict: dict) -> dict[str, Any]:
|
|
|
117
119
|
|
|
118
120
|
# Remove any remaining jsonref proxy objects by converting to plain dict
|
|
119
121
|
return _deproxy_schema(resolved_schema)
|
|
120
|
-
except (
|
|
122
|
+
except (AttributeError, KeyError, RecursionError, Exception):
|
|
121
123
|
# If resolution fails, return the original schema
|
|
122
124
|
# This allows the system to continue even with malformed $refs
|
|
125
|
+
# AttributeError covers the case where jsonref might be None
|
|
126
|
+
# Exception catches jsonref.JsonRefError and other jsonref exceptions
|
|
123
127
|
return schema_dict
|
|
124
128
|
|
|
125
129
|
|
|
@@ -390,23 +394,35 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
390
394
|
for entity_name, endpoints_dict in entities_map.items():
|
|
391
395
|
actions = list(endpoints_dict.keys())
|
|
392
396
|
|
|
393
|
-
# Get schema from components if available
|
|
397
|
+
# Get schema and stream_name from components if available
|
|
394
398
|
schema = None
|
|
399
|
+
entity_stream_name = None
|
|
395
400
|
if spec.components:
|
|
396
401
|
# Look for a schema matching the entity name
|
|
397
402
|
for schema_name, schema_def in spec.components.schemas.items():
|
|
398
403
|
if schema_def.x_airbyte_entity_name == entity_name or schema_name.lower() == entity_name.lower():
|
|
399
404
|
schema = schema_def.model_dump(by_alias=True)
|
|
405
|
+
entity_stream_name = schema_def.x_airbyte_stream_name
|
|
400
406
|
break
|
|
401
407
|
|
|
402
|
-
entity = EntityDefinition(
|
|
408
|
+
entity = EntityDefinition(
|
|
409
|
+
name=entity_name,
|
|
410
|
+
stream_name=entity_stream_name,
|
|
411
|
+
actions=actions,
|
|
412
|
+
endpoints=endpoints_dict,
|
|
413
|
+
schema=schema,
|
|
414
|
+
)
|
|
403
415
|
entities.append(entity)
|
|
404
416
|
|
|
405
417
|
# Extract retry config from x-airbyte-retry-config extension
|
|
406
418
|
retry_config = spec.info.x_airbyte_retry_config
|
|
419
|
+
connector_id = spec.info.x_airbyte_connector_id
|
|
420
|
+
if not connector_id:
|
|
421
|
+
raise InvalidOpenAPIError("Missing required x-airbyte-connector-id field")
|
|
407
422
|
|
|
408
423
|
# Create ConnectorModel
|
|
409
424
|
model = ConnectorModel(
|
|
425
|
+
id=connector_id,
|
|
410
426
|
name=name,
|
|
411
427
|
version=version,
|
|
412
428
|
base_url=base_url,
|
|
@@ -753,8 +769,6 @@ def _parse_auth_from_openapi(spec: OpenAPIConnector) -> AuthConfig:
|
|
|
753
769
|
options.append(auth_option)
|
|
754
770
|
except Exception as e:
|
|
755
771
|
# Log warning but continue - skip invalid schemes
|
|
756
|
-
import logging
|
|
757
|
-
|
|
758
772
|
logger = logging.getLogger(__name__)
|
|
759
773
|
logger.warning(f"Skipping invalid security scheme '{scheme_name}': {e}")
|
|
760
774
|
continue
|
|
@@ -926,8 +940,20 @@ def load_connector_model(definition_path: str | Path) -> ConnectorModel:
|
|
|
926
940
|
)
|
|
927
941
|
entities.append(entity)
|
|
928
942
|
|
|
943
|
+
# Get connector ID
|
|
944
|
+
connector_id_value = connector_meta.get("id")
|
|
945
|
+
if connector_id_value:
|
|
946
|
+
# Try to parse as UUID (handles string UUIDs)
|
|
947
|
+
if isinstance(connector_id_value, str):
|
|
948
|
+
connector_id = UUID(connector_id_value)
|
|
949
|
+
else:
|
|
950
|
+
connector_id = connector_id_value
|
|
951
|
+
else:
|
|
952
|
+
raise ValueError
|
|
953
|
+
|
|
929
954
|
# Build ConnectorModel
|
|
930
955
|
model = ConnectorModel(
|
|
956
|
+
id=connector_id,
|
|
931
957
|
name=connector_meta["name"],
|
|
932
958
|
version=connector_meta.get("version", OPENAPI_DEFAULT_VERSION),
|
|
933
959
|
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
|
|
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.
|
|
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 definition id from executor config
|
|
90
|
+
2. Look up the user's connector 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,99 @@ class HostedExecutor:
|
|
|
101
98
|
ExecutionResult with success/failure status
|
|
102
99
|
|
|
103
100
|
Raises:
|
|
101
|
+
ValueError: If no connector or multiple connectors 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 ID for this user
|
|
130
|
+
connector_id = await self._cloud_client.get_connector_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.connector_id", connector_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
|
+
connector_id=connector_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
|
+
result = self._parse_execution_result(response)
|
|
151
147
|
|
|
152
148
|
# Mark span as successful
|
|
153
|
-
span.set_attribute("connector.success",
|
|
149
|
+
span.set_attribute("connector.success", result.success)
|
|
154
150
|
|
|
155
|
-
|
|
156
|
-
return ExecutionResult(success=True, data=result_data, error=None)
|
|
151
|
+
return result
|
|
157
152
|
|
|
158
|
-
except
|
|
159
|
-
#
|
|
153
|
+
except ValueError as e:
|
|
154
|
+
# Connector lookup validation error (0 or >1 connectors)
|
|
160
155
|
span.set_attribute("connector.success", False)
|
|
161
|
-
span.set_attribute("connector.error_type", "
|
|
162
|
-
span.set_attribute("http.status_code", e.response.status_code)
|
|
156
|
+
span.set_attribute("connector.error_type", "ValueError")
|
|
163
157
|
span.record_exception(e)
|
|
164
158
|
raise
|
|
165
159
|
|
|
166
160
|
except Exception as e:
|
|
167
|
-
#
|
|
161
|
+
# HTTP errors and other exceptions
|
|
168
162
|
span.set_attribute("connector.success", False)
|
|
169
163
|
span.set_attribute("connector.error_type", type(e).__name__)
|
|
170
164
|
span.record_exception(e)
|
|
171
165
|
raise
|
|
172
166
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
167
|
+
def _parse_execution_result(self, response: dict) -> ExecutionResult:
|
|
168
|
+
"""Parse API response into ExecutionResult.
|
|
175
169
|
|
|
176
|
-
|
|
170
|
+
Args:
|
|
171
|
+
response_data: Raw JSON response from the cloud API
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
ExecutionResult with parsed data
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
return ExecutionResult(
|
|
178
|
+
success=True,
|
|
179
|
+
data=response["result"],
|
|
180
|
+
meta=response.get("connector_metadata"),
|
|
181
|
+
error=None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def close(self):
|
|
185
|
+
"""Close the cloud client and cleanup resources.
|
|
186
|
+
|
|
187
|
+
Call this when you're done using the executor to clean up HTTP connections.
|
|
177
188
|
|
|
178
189
|
Example:
|
|
179
|
-
executor = HostedExecutor(
|
|
180
|
-
workspace_id="workspace-123",
|
|
181
|
-
connector_id="my-connector"
|
|
182
|
-
)
|
|
190
|
+
executor = HostedExecutor(...)
|
|
183
191
|
try:
|
|
184
192
|
result = await executor.execute(config)
|
|
185
193
|
finally:
|
|
186
|
-
executor.close()
|
|
194
|
+
await executor.close()
|
|
187
195
|
"""
|
|
188
|
-
self.
|
|
196
|
+
await self._cloud_client.close()
|