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.
Files changed (33) hide show
  1. airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_strategies.py +2 -5
  2. airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_template.py +1 -1
  3. airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  4. airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/client.py +213 -0
  5. airbyte_agent_zendesk_support/_vendored/connector_sdk/connector_model_loader.py +32 -6
  6. airbyte_agent_zendesk_support/_vendored/connector_sdk/constants.py +1 -1
  7. airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/hosted_executor.py +92 -84
  8. airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/local_executor.py +94 -25
  9. airbyte_agent_zendesk_support/_vendored/connector_sdk/extensions.py +43 -5
  10. airbyte_agent_zendesk_support/_vendored/connector_sdk/http/response.py +2 -0
  11. airbyte_agent_zendesk_support/_vendored/connector_sdk/http_client.py +50 -43
  12. airbyte_agent_zendesk_support/_vendored/connector_sdk/introspection.py +262 -0
  13. airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/logger.py +9 -9
  14. airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/types.py +10 -10
  15. airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/config.py +179 -0
  16. airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/models.py +6 -6
  17. airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/session.py +41 -32
  18. airbyte_agent_zendesk_support/_vendored/connector_sdk/performance/metrics.py +3 -3
  19. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/base.py +18 -17
  20. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/components.py +59 -58
  21. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/extensions.py +9 -9
  22. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/operations.py +32 -32
  23. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/security.py +44 -34
  24. airbyte_agent_zendesk_support/_vendored/connector_sdk/secrets.py +2 -2
  25. airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/events.py +9 -8
  26. airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/tracker.py +9 -5
  27. airbyte_agent_zendesk_support/_vendored/connector_sdk/types.py +9 -3
  28. airbyte_agent_zendesk_support/connector.py +98 -15
  29. airbyte_agent_zendesk_support/connector_model.py +7 -1
  30. airbyte_agent_zendesk_support/types.py +1 -1
  31. {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/METADATA +47 -26
  32. {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/RECORD +33 -29
  33. {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. " f"Available fields: {available_fields}")
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,5 @@
1
+ """Cloud API utilities for Airbyte Platform integration."""
2
+
3
+ from .client import AirbyteCloudClient
4
+
5
+ __all__ = ["AirbyteCloudClient"]
@@ -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 (jsonref.JsonRefError, KeyError, RecursionError):
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(name=entity_name, actions=actions, endpoints=endpoints_dict, schema=schema)
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", "")),
@@ -74,5 +74,5 @@ except PackageNotFoundError:
74
74
  SDK_VERSION = "0.0.0-dev"
75
75
  """Current version of the Airbyte SDK."""
76
76
 
77
- MINIMUM_PYTHON_VERSION = "3.9"
77
+ MINIMUM_PYTHON_VERSION = "3.13"
78
78
  """Minimum Python version required to run the SDK."""
@@ -1,12 +1,11 @@
1
- """Hosted executor for proxying operations through the backend API."""
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 Sonar backend API.
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 backend API
20
- instead of directly calling external services. The backend handles all
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 API URL is configured at initialization via the api_url parameter,
24
- which defaults to the AIRBYTE_CONNECTOR_API_URL environment variable.
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
- connector_id="stripe-prod-123",
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
- config = ExecutionConfig(
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(config)
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
- connector_id: str,
54
+ external_user_id: str,
50
55
  airbyte_client_id: str,
51
56
  airbyte_client_secret: str,
52
- api_url: str | None = None,
57
+ connector_definition_id: str,
53
58
  ):
54
59
  """Initialize hosted executor.
55
60
 
56
61
  Args:
57
- connector_id: ID of the connector to execute (e.g., "stripe-prod-123")
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
- api_url: API URL for the hosted executor backend. Defaults to
61
- AIRBYTE_CONNECTOR_API_URL environment variable or "http://localhost:8001"
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
- connector_id="my-connector-id",
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
- api_url="https://api.production.com"
73
+ connector_definition_id="abc123-def456-ghi789",
76
74
  )
77
75
  """
78
- self.connector_id = connector_id
79
- self.airbyte_client_id = airbyte_client_id
80
- self.airbyte_client_secret = airbyte_client_secret
81
- self.api_url = api_url or os.getenv("AIRBYTE_CONNECTOR_API_URL", "http://localhost:8001")
82
-
83
- # Create synchronous HTTP client
84
- # We use sync client even though execute() is async for simplicity
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 backend API (ExecutorProtocol implementation).
86
+ """Execute connector via cloud API (ExecutorProtocol implementation).
93
87
 
94
- Makes an HTTP POST request to /connectors/{connector_id}/execute with
95
- OAuth authentication and the configuration in the request body.
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.id", self.connector_id)
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("connector.api_url", self.api_url)
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
- # Make synchronous HTTP request
140
- # (wrapped in async method for protocol compatibility)
141
- response = self.client.post(url, json=request_body)
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
- # Add response status code to span
144
- span.set_attribute("http.status_code", response.status_code)
135
+ span.set_attribute("connector.connector_id", connector_id)
145
136
 
146
- # Raise exception for 4xx/5xx status codes
147
- response.raise_for_status()
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 JSON response
150
- result_data = response.json()
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", True)
149
+ span.set_attribute("connector.success", result.success)
154
150
 
155
- # Return success result
156
- return ExecutionResult(success=True, data=result_data, error=None)
151
+ return result
157
152
 
158
- except httpx.HTTPStatusError as e:
159
- # HTTP error (4xx, 5xx) - record and re-raise
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", "HTTPStatusError")
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
- # Catch-all for any other unexpected exceptions
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 close(self):
174
- """Close the HTTP client.
167
+ def _parse_execution_result(self, response: dict) -> ExecutionResult:
168
+ """Parse API response into ExecutionResult.
175
169
 
176
- Call this when you're done using the executor to clean up resources.
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.client.close()
196
+ await self._cloud_client.close()