airbyte-agent-asana 0.19.27__py3-none-any.whl → 0.19.41__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.
Potentially problematic release.
This version of airbyte-agent-asana might be problematic. Click here for more details.
- airbyte_agent_asana/__init__.py +6 -6
- airbyte_agent_asana/_vendored/connector_sdk/auth_strategies.py +2 -5
- airbyte_agent_asana/_vendored/connector_sdk/auth_template.py +1 -1
- airbyte_agent_asana/_vendored/connector_sdk/cloud_utils/client.py +26 -26
- airbyte_agent_asana/_vendored/connector_sdk/connector_model_loader.py +11 -4
- airbyte_agent_asana/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_asana/_vendored/connector_sdk/executor/hosted_executor.py +10 -11
- airbyte_agent_asana/_vendored/connector_sdk/executor/local_executor.py +94 -25
- airbyte_agent_asana/_vendored/connector_sdk/extensions.py +43 -5
- airbyte_agent_asana/_vendored/connector_sdk/http/response.py +2 -0
- airbyte_agent_asana/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_asana/_vendored/connector_sdk/logging/logger.py +9 -9
- airbyte_agent_asana/_vendored/connector_sdk/logging/types.py +10 -10
- airbyte_agent_asana/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_asana/_vendored/connector_sdk/observability/models.py +6 -6
- airbyte_agent_asana/_vendored/connector_sdk/observability/session.py +41 -32
- airbyte_agent_asana/_vendored/connector_sdk/performance/metrics.py +3 -3
- airbyte_agent_asana/_vendored/connector_sdk/schema/base.py +17 -17
- airbyte_agent_asana/_vendored/connector_sdk/schema/components.py +59 -58
- airbyte_agent_asana/_vendored/connector_sdk/schema/extensions.py +9 -9
- airbyte_agent_asana/_vendored/connector_sdk/schema/operations.py +32 -32
- airbyte_agent_asana/_vendored/connector_sdk/schema/security.py +44 -34
- airbyte_agent_asana/_vendored/connector_sdk/secrets.py +2 -2
- airbyte_agent_asana/_vendored/connector_sdk/telemetry/events.py +9 -8
- airbyte_agent_asana/_vendored/connector_sdk/telemetry/tracker.py +9 -5
- airbyte_agent_asana/_vendored/connector_sdk/types.py +7 -3
- airbyte_agent_asana/connector.py +88 -3
- airbyte_agent_asana/connector_model.py +6 -0
- airbyte_agent_asana/models.py +29 -29
- airbyte_agent_asana/types.py +1 -1
- {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/METADATA +11 -11
- {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/RECORD +33 -31
- {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/WHEEL +0 -0
airbyte_agent_asana/__init__.py
CHANGED
|
@@ -14,15 +14,15 @@ from .models import (
|
|
|
14
14
|
TasksListNextPage,
|
|
15
15
|
TasksList,
|
|
16
16
|
ProjectCompact,
|
|
17
|
-
ProjectCurrentStatusCreatedBy,
|
|
18
|
-
ProjectCurrentStatusAuthor,
|
|
19
|
-
ProjectCurrentStatus,
|
|
20
17
|
ProjectMembersItem,
|
|
21
18
|
ProjectWorkspace,
|
|
22
|
-
ProjectCurrentStatusUpdate,
|
|
23
|
-
ProjectOwner,
|
|
24
19
|
ProjectTeam,
|
|
25
20
|
ProjectFollowersItem,
|
|
21
|
+
ProjectCurrentStatusCreatedBy,
|
|
22
|
+
ProjectCurrentStatusAuthor,
|
|
23
|
+
ProjectCurrentStatus,
|
|
24
|
+
ProjectOwner,
|
|
25
|
+
ProjectCurrentStatusUpdate,
|
|
26
26
|
Project,
|
|
27
27
|
ProjectResponse,
|
|
28
28
|
ProjectsListNextPage,
|
|
@@ -142,4 +142,4 @@ from .types import (
|
|
|
142
142
|
TaskDependentsListParams
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
__all__ = ["AsanaConnector", "AsanaAuthConfig", "TaskCompactCreatedBy", "TaskCompact", "Task", "TaskResponse", "TasksListNextPage", "TasksList", "ProjectCompact", "
|
|
145
|
+
__all__ = ["AsanaConnector", "AsanaAuthConfig", "TaskCompactCreatedBy", "TaskCompact", "Task", "TaskResponse", "TasksListNextPage", "TasksList", "ProjectCompact", "ProjectMembersItem", "ProjectWorkspace", "ProjectTeam", "ProjectFollowersItem", "ProjectCurrentStatusCreatedBy", "ProjectCurrentStatusAuthor", "ProjectCurrentStatus", "ProjectOwner", "ProjectCurrentStatusUpdate", "Project", "ProjectResponse", "ProjectsListNextPage", "ProjectsList", "WorkspaceCompact", "Workspace", "WorkspaceResponse", "WorkspacesListNextPage", "WorkspacesList", "UserCompact", "UserWorkspacesItem", "User", "UserResponse", "UsersListNextPage", "UsersList", "TeamCompact", "TeamOrganization", "Team", "TeamResponse", "TeamsListNextPage", "TeamsList", "AttachmentCompact", "AttachmentParent", "Attachment", "AttachmentResponse", "AttachmentsListNextPage", "AttachmentsList", "TagCompact", "TagWorkspace", "Tag", "TagResponse", "TagsListNextPage", "TagsList", "SectionCompact", "SectionProject", "Section", "SectionResponse", "SectionsListNextPage", "SectionsList", "TasksListResultMeta", "ProjectTasksListResultMeta", "WorkspaceTaskSearchListResultMeta", "ProjectsListResultMeta", "TaskProjectsListResultMeta", "TeamProjectsListResultMeta", "WorkspaceProjectsListResultMeta", "WorkspacesListResultMeta", "UsersListResultMeta", "WorkspaceUsersListResultMeta", "TeamUsersListResultMeta", "WorkspaceTeamsListResultMeta", "UserTeamsListResultMeta", "AttachmentsListResultMeta", "WorkspaceTagsListResultMeta", "ProjectSectionsListResultMeta", "TaskSubtasksListResultMeta", "TaskDependenciesListResultMeta", "TaskDependentsListResultMeta", "AsanaExecuteResult", "AsanaExecuteResultWithMeta", "TasksListResult", "ProjectTasksListResult", "TasksGetResult", "WorkspaceTaskSearchListResult", "ProjectsListResult", "ProjectsGetResult", "TaskProjectsListResult", "TeamProjectsListResult", "WorkspaceProjectsListResult", "WorkspacesListResult", "WorkspacesGetResult", "UsersListResult", "UsersGetResult", "WorkspaceUsersListResult", "TeamUsersListResult", "TeamsGetResult", "WorkspaceTeamsListResult", "UserTeamsListResult", "AttachmentsListResult", "AttachmentsGetResult", "WorkspaceTagsListResult", "TagsGetResult", "ProjectSectionsListResult", "SectionsGetResult", "TaskSubtasksListResult", "TaskDependenciesListResult", "TaskDependentsListResult", "TasksListParams", "ProjectTasksListParams", "TasksGetParams", "WorkspaceTaskSearchListParams", "ProjectsListParams", "ProjectsGetParams", "TaskProjectsListParams", "TeamProjectsListParams", "WorkspaceProjectsListParams", "WorkspacesListParams", "WorkspacesGetParams", "UsersListParams", "UsersGetParams", "WorkspaceUsersListParams", "TeamUsersListParams", "TeamsGetParams", "WorkspaceTeamsListParams", "UserTeamsListParams", "AttachmentsListParams", "AttachmentsGetParams", "AttachmentsDownloadParams", "WorkspaceTagsListParams", "TagsGetParams", "ProjectSectionsListParams", "SectionsGetParams", "TaskSubtasksListParams", "TaskDependenciesListParams", "TaskDependentsListParams"]
|
|
@@ -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:
|
|
@@ -13,7 +13,7 @@ class AirbyteCloudClient:
|
|
|
13
13
|
|
|
14
14
|
Handles authentication, token caching, and API calls to:
|
|
15
15
|
- Get bearer tokens for authentication
|
|
16
|
-
- Look up
|
|
16
|
+
- Look up connectors for users
|
|
17
17
|
- Execute connectors via the cloud API
|
|
18
18
|
|
|
19
19
|
Example:
|
|
@@ -22,15 +22,15 @@ class AirbyteCloudClient:
|
|
|
22
22
|
client_secret="your-client-secret"
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
# Get a connector
|
|
26
|
-
|
|
25
|
+
# Get a connector ID
|
|
26
|
+
connector_id = await client.get_connector_id(
|
|
27
27
|
external_user_id="user-123",
|
|
28
|
-
connector_definition_id="
|
|
28
|
+
connector_definition_id="550e8400-e29b-41d4-a716-446655440000"
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
# Execute the connector
|
|
32
32
|
result = await client.execute_connector(
|
|
33
|
-
|
|
33
|
+
connector_id=connector_id,
|
|
34
34
|
entity="customers",
|
|
35
35
|
action="list",
|
|
36
36
|
params={"limit": 10}
|
|
@@ -105,37 +105,37 @@ class AirbyteCloudClient:
|
|
|
105
105
|
|
|
106
106
|
return access_token
|
|
107
107
|
|
|
108
|
-
async def
|
|
108
|
+
async def get_connector_id(
|
|
109
109
|
self,
|
|
110
110
|
external_user_id: str,
|
|
111
111
|
connector_definition_id: str,
|
|
112
112
|
) -> str:
|
|
113
|
-
"""Get connector
|
|
113
|
+
"""Get connector ID for a user.
|
|
114
114
|
|
|
115
|
-
Looks up the connector
|
|
116
|
-
and connector definition. Validates that exactly one
|
|
115
|
+
Looks up the connector that belongs to the specified user
|
|
116
|
+
and connector definition. Validates that exactly one connector exists.
|
|
117
117
|
|
|
118
118
|
Args:
|
|
119
119
|
external_user_id: User identifier in the Airbyte system
|
|
120
120
|
connector_definition_id: UUID of the connector definition
|
|
121
121
|
|
|
122
122
|
Returns:
|
|
123
|
-
Connector
|
|
123
|
+
Connector ID (UUID string)
|
|
124
124
|
|
|
125
125
|
Raises:
|
|
126
|
-
ValueError: If 0 or more than 1
|
|
126
|
+
ValueError: If 0 or more than 1 connector is found
|
|
127
127
|
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
128
128
|
httpx.RequestError: If network request fails
|
|
129
129
|
|
|
130
130
|
Example:
|
|
131
|
-
|
|
131
|
+
connector_id = await client.get_connector_id(
|
|
132
132
|
external_user_id="user-123",
|
|
133
133
|
connector_definition_id="550e8400-e29b-41d4-a716-446655440000"
|
|
134
134
|
)
|
|
135
135
|
"""
|
|
136
136
|
|
|
137
137
|
token = await self.get_bearer_token()
|
|
138
|
-
url = f"{self.API_BASE_URL}/api/v1/connectors/
|
|
138
|
+
url = f"{self.API_BASE_URL}/api/v1/connectors/connectors_for_user"
|
|
139
139
|
params = {
|
|
140
140
|
"external_user_id": external_user_id,
|
|
141
141
|
"definition_id": connector_definition_id,
|
|
@@ -146,24 +146,24 @@ class AirbyteCloudClient:
|
|
|
146
146
|
response.raise_for_status()
|
|
147
147
|
|
|
148
148
|
data = response.json()
|
|
149
|
-
|
|
149
|
+
connectors = data["connectors"]
|
|
150
150
|
|
|
151
|
-
if len(
|
|
152
|
-
raise ValueError(f"No connector
|
|
151
|
+
if len(connectors) == 0:
|
|
152
|
+
raise ValueError(f"No connector found for user '{external_user_id}' and connector definition '{connector_definition_id}'")
|
|
153
153
|
|
|
154
|
-
if len(
|
|
154
|
+
if len(connectors) > 1:
|
|
155
155
|
raise ValueError(
|
|
156
|
-
f"Multiple
|
|
157
|
-
f"and connector '{connector_definition_id}'. Expected exactly 1, "
|
|
158
|
-
f"found {len(
|
|
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
159
|
)
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
return
|
|
161
|
+
connector_id = connectors[0]["id"]
|
|
162
|
+
return connector_id
|
|
163
163
|
|
|
164
164
|
async def execute_connector(
|
|
165
165
|
self,
|
|
166
|
-
|
|
166
|
+
connector_id: str,
|
|
167
167
|
entity: str,
|
|
168
168
|
action: str,
|
|
169
169
|
params: dict[str, Any] | None,
|
|
@@ -171,7 +171,7 @@ class AirbyteCloudClient:
|
|
|
171
171
|
"""Execute a connector operation.
|
|
172
172
|
|
|
173
173
|
Args:
|
|
174
|
-
|
|
174
|
+
connector_id: Connector UUID (source ID)
|
|
175
175
|
entity: Entity name (e.g., "customers", "invoices")
|
|
176
176
|
action: Operation action (e.g., "list", "get", "create")
|
|
177
177
|
params: Optional parameters for the operation
|
|
@@ -185,14 +185,14 @@ class AirbyteCloudClient:
|
|
|
185
185
|
|
|
186
186
|
Example:
|
|
187
187
|
result = await client.execute_connector(
|
|
188
|
-
|
|
188
|
+
connector_id="550e8400-e29b-41d4-a716-446655440000",
|
|
189
189
|
entity="customers",
|
|
190
190
|
action="list",
|
|
191
191
|
params={"limit": 10}
|
|
192
192
|
)
|
|
193
193
|
"""
|
|
194
194
|
token = await self.get_bearer_token()
|
|
195
|
-
url = f"{self.API_BASE_URL}/api/v1/connectors/
|
|
195
|
+
url = f"{self.API_BASE_URL}/api/v1/connectors/sources/{connector_id}/execute"
|
|
196
196
|
headers = {"Authorization": f"Bearer {token}"}
|
|
197
197
|
request_body = {
|
|
198
198
|
"entity": entity,
|
|
@@ -2,6 +2,7 @@
|
|
|
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
|
|
@@ -393,16 +394,24 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
393
394
|
for entity_name, endpoints_dict in entities_map.items():
|
|
394
395
|
actions = list(endpoints_dict.keys())
|
|
395
396
|
|
|
396
|
-
# Get schema from components if available
|
|
397
|
+
# Get schema and stream_name from components if available
|
|
397
398
|
schema = None
|
|
399
|
+
entity_stream_name = None
|
|
398
400
|
if spec.components:
|
|
399
401
|
# Look for a schema matching the entity name
|
|
400
402
|
for schema_name, schema_def in spec.components.schemas.items():
|
|
401
403
|
if schema_def.x_airbyte_entity_name == entity_name or schema_name.lower() == entity_name.lower():
|
|
402
404
|
schema = schema_def.model_dump(by_alias=True)
|
|
405
|
+
entity_stream_name = schema_def.x_airbyte_stream_name
|
|
403
406
|
break
|
|
404
407
|
|
|
405
|
-
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
|
+
)
|
|
406
415
|
entities.append(entity)
|
|
407
416
|
|
|
408
417
|
# Extract retry config from x-airbyte-retry-config extension
|
|
@@ -760,8 +769,6 @@ def _parse_auth_from_openapi(spec: OpenAPIConnector) -> AuthConfig:
|
|
|
760
769
|
options.append(auth_option)
|
|
761
770
|
except Exception as e:
|
|
762
771
|
# Log warning but continue - skip invalid schemes
|
|
763
|
-
import logging
|
|
764
|
-
|
|
765
772
|
logger = logging.getLogger(__name__)
|
|
766
773
|
logger.warning(f"Skipping invalid security scheme '{scheme_name}': {e}")
|
|
767
774
|
continue
|
|
@@ -21,7 +21,7 @@ class HostedExecutor:
|
|
|
21
21
|
|
|
22
22
|
The executor takes an external_user_id and uses the AirbyteCloudClient to:
|
|
23
23
|
1. Authenticate with the Airbyte Platform (bearer token with caching)
|
|
24
|
-
2. Look up the user's connector
|
|
24
|
+
2. Look up the user's connector
|
|
25
25
|
3. Execute the connector operation via the cloud API
|
|
26
26
|
|
|
27
27
|
Implements ExecutorProtocol.
|
|
@@ -63,7 +63,7 @@ class HostedExecutor:
|
|
|
63
63
|
airbyte_client_id: Airbyte client ID for authentication
|
|
64
64
|
airbyte_client_secret: Airbyte client secret for authentication
|
|
65
65
|
connector_definition_id: Connector definition ID used to look up
|
|
66
|
-
the user's connector
|
|
66
|
+
the user's connector.
|
|
67
67
|
|
|
68
68
|
Example:
|
|
69
69
|
executor = HostedExecutor(
|
|
@@ -86,8 +86,8 @@ class HostedExecutor:
|
|
|
86
86
|
"""Execute connector via cloud API (ExecutorProtocol implementation).
|
|
87
87
|
|
|
88
88
|
Flow:
|
|
89
|
-
1. Get connector id from
|
|
90
|
-
2. Look up the user's connector
|
|
89
|
+
1. Get connector definition id from executor config
|
|
90
|
+
2. Look up the user's connector ID
|
|
91
91
|
3. Execute the connector operation via the cloud API
|
|
92
92
|
4. Parse the response into ExecutionResult
|
|
93
93
|
|
|
@@ -98,7 +98,7 @@ class HostedExecutor:
|
|
|
98
98
|
ExecutionResult with success/failure status
|
|
99
99
|
|
|
100
100
|
Raises:
|
|
101
|
-
ValueError: If no
|
|
101
|
+
ValueError: If no connector or multiple connectors found for user
|
|
102
102
|
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
103
103
|
httpx.RequestError: If network request fails
|
|
104
104
|
|
|
@@ -126,24 +126,23 @@ class HostedExecutor:
|
|
|
126
126
|
# Step 1: Get connector definition id
|
|
127
127
|
connector_definition_id = self._connector_definition_id
|
|
128
128
|
|
|
129
|
-
# Step 2: Get the connector
|
|
130
|
-
|
|
129
|
+
# Step 2: Get the connector ID for this user
|
|
130
|
+
connector_id = await self._cloud_client.get_connector_id(
|
|
131
131
|
external_user_id=self._external_user_id,
|
|
132
132
|
connector_definition_id=connector_definition_id,
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
-
span.set_attribute("connector.
|
|
135
|
+
span.set_attribute("connector.connector_id", connector_id)
|
|
136
136
|
|
|
137
137
|
# Step 3: Execute the connector via the cloud API
|
|
138
138
|
response = await self._cloud_client.execute_connector(
|
|
139
|
-
|
|
139
|
+
connector_id=connector_id,
|
|
140
140
|
entity=config.entity,
|
|
141
141
|
action=config.action,
|
|
142
142
|
params=config.params,
|
|
143
143
|
)
|
|
144
144
|
|
|
145
145
|
# Step 4: Parse the response into ExecutionResult
|
|
146
|
-
# The response_data is a dict from the API
|
|
147
146
|
result = self._parse_execution_result(response)
|
|
148
147
|
|
|
149
148
|
# Mark span as successful
|
|
@@ -152,7 +151,7 @@ class HostedExecutor:
|
|
|
152
151
|
return result
|
|
153
152
|
|
|
154
153
|
except ValueError as e:
|
|
155
|
-
#
|
|
154
|
+
# Connector lookup validation error (0 or >1 connectors)
|
|
156
155
|
span.set_attribute("connector.success", False)
|
|
157
156
|
span.set_attribute("connector.error_type", "ValueError")
|
|
158
157
|
span.record_exception(e)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import inspect
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
@@ -11,6 +12,7 @@ from collections.abc import AsyncIterator
|
|
|
11
12
|
from typing import Any, Protocol
|
|
12
13
|
from urllib.parse import quote
|
|
13
14
|
|
|
15
|
+
from jinja2 import Environment, StrictUndefined
|
|
14
16
|
from jsonpath_ng import parse as parse_jsonpath
|
|
15
17
|
from opentelemetry import trace
|
|
16
18
|
|
|
@@ -506,8 +508,6 @@ class LocalExecutor:
|
|
|
506
508
|
result = handler.execute_operation(config.entity, action, params)
|
|
507
509
|
|
|
508
510
|
# Check if it's an async generator (download) or awaitable (standard)
|
|
509
|
-
import inspect
|
|
510
|
-
|
|
511
511
|
if inspect.isasyncgen(result):
|
|
512
512
|
# Download operation: return generator directly
|
|
513
513
|
return ExecutionResult(
|
|
@@ -674,16 +674,16 @@ class LocalExecutor:
|
|
|
674
674
|
return {key: value for key, value in params.items() if key in allowed_params}
|
|
675
675
|
|
|
676
676
|
def _extract_body(self, allowed_fields: list[str], params: dict[str, Any]) -> dict[str, Any]:
|
|
677
|
-
"""Extract body fields from params.
|
|
677
|
+
"""Extract body fields from params, filtering out None values.
|
|
678
678
|
|
|
679
679
|
Args:
|
|
680
680
|
allowed_fields: List of allowed body field names
|
|
681
681
|
params: All parameters
|
|
682
682
|
|
|
683
683
|
Returns:
|
|
684
|
-
Dictionary of body fields
|
|
684
|
+
Dictionary of body fields with None values filtered out
|
|
685
685
|
"""
|
|
686
|
-
return {key: value for key, value in params.items() if key in allowed_fields}
|
|
686
|
+
return {key: value for key, value in params.items() if key in allowed_fields and value is not None}
|
|
687
687
|
|
|
688
688
|
def _serialize_deep_object_params(self, params: dict[str, Any], deep_object_param_names: list[str]) -> dict[str, Any]:
|
|
689
689
|
"""Serialize deepObject parameters to bracket notation format.
|
|
@@ -814,7 +814,6 @@ class LocalExecutor:
|
|
|
814
814
|
>>> _substitute_file_field_params("attachments[{attachment_index}].url", {"attachment_index": 0})
|
|
815
815
|
"attachments[0].url"
|
|
816
816
|
"""
|
|
817
|
-
from jinja2 import Environment, StrictUndefined
|
|
818
817
|
|
|
819
818
|
# Use custom delimiters to match OpenAPI path parameter syntax {var}
|
|
820
819
|
# StrictUndefined raises clear error if a template variable is missing
|
|
@@ -837,15 +836,65 @@ class LocalExecutor:
|
|
|
837
836
|
Request body dict or None if no body needed
|
|
838
837
|
"""
|
|
839
838
|
if endpoint.graphql_body:
|
|
840
|
-
|
|
839
|
+
# Extract defaults from query_params_schema for GraphQL variable interpolation
|
|
840
|
+
param_defaults = {name: schema.get("default") for name, schema in endpoint.query_params_schema.items() if "default" in schema}
|
|
841
|
+
return self._build_graphql_body(endpoint.graphql_body, params, param_defaults)
|
|
841
842
|
elif endpoint.body_fields:
|
|
842
843
|
return self._extract_body(endpoint.body_fields, params)
|
|
843
844
|
return None
|
|
844
845
|
|
|
846
|
+
def _flatten_form_data(self, data: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
|
|
847
|
+
"""Flatten nested dict/list structures into bracket notation for form encoding.
|
|
848
|
+
|
|
849
|
+
Stripe and similar APIs require nested arrays/objects to be encoded using bracket
|
|
850
|
+
notation when using application/x-www-form-urlencoded content type.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
data: Nested dict with arrays/objects to flatten
|
|
854
|
+
parent_key: Parent key for nested structures (used in recursion)
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
Flattened dict with bracket notation keys
|
|
858
|
+
|
|
859
|
+
Examples:
|
|
860
|
+
>>> _flatten_form_data({"items": [{"price": "p1", "qty": 1}]})
|
|
861
|
+
{"items[0][price]": "p1", "items[0][qty]": 1}
|
|
862
|
+
|
|
863
|
+
>>> _flatten_form_data({"customer": "cus_123", "metadata": {"key": "value"}})
|
|
864
|
+
{"customer": "cus_123", "metadata[key]": "value"}
|
|
865
|
+
"""
|
|
866
|
+
flattened = {}
|
|
867
|
+
|
|
868
|
+
for key, value in data.items():
|
|
869
|
+
new_key = f"{parent_key}[{key}]" if parent_key else key
|
|
870
|
+
|
|
871
|
+
if isinstance(value, dict):
|
|
872
|
+
# Recursively flatten nested dicts
|
|
873
|
+
flattened.update(self._flatten_form_data(value, new_key))
|
|
874
|
+
elif isinstance(value, list):
|
|
875
|
+
# Flatten arrays with indexed bracket notation
|
|
876
|
+
for i, item in enumerate(value):
|
|
877
|
+
indexed_key = f"{new_key}[{i}]"
|
|
878
|
+
if isinstance(item, dict):
|
|
879
|
+
# Nested dict in array - recurse
|
|
880
|
+
flattened.update(self._flatten_form_data(item, indexed_key))
|
|
881
|
+
elif isinstance(item, list):
|
|
882
|
+
# Nested list in array - recurse
|
|
883
|
+
flattened.update(self._flatten_form_data({str(i): item}, new_key))
|
|
884
|
+
else:
|
|
885
|
+
# Primitive value in array
|
|
886
|
+
flattened[indexed_key] = item
|
|
887
|
+
else:
|
|
888
|
+
# Primitive value - add directly
|
|
889
|
+
flattened[new_key] = value
|
|
890
|
+
|
|
891
|
+
return flattened
|
|
892
|
+
|
|
845
893
|
def _determine_request_format(self, endpoint: EndpointDefinition, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
846
894
|
"""Determine json/data parameters for HTTP request.
|
|
847
895
|
|
|
848
896
|
GraphQL always uses JSON, regardless of content_type setting.
|
|
897
|
+
For form-encoded requests, nested structures are flattened into bracket notation.
|
|
849
898
|
|
|
850
899
|
Args:
|
|
851
900
|
endpoint: Endpoint definition
|
|
@@ -862,7 +911,9 @@ class LocalExecutor:
|
|
|
862
911
|
if is_graphql or endpoint.content_type.value == "application/json":
|
|
863
912
|
return {"json": body}
|
|
864
913
|
elif endpoint.content_type.value == "application/x-www-form-urlencoded":
|
|
865
|
-
|
|
914
|
+
# Flatten nested structures for form encoding
|
|
915
|
+
flattened_body = self._flatten_form_data(body)
|
|
916
|
+
return {"data": flattened_body}
|
|
866
917
|
|
|
867
918
|
return {}
|
|
868
919
|
|
|
@@ -903,12 +954,18 @@ class LocalExecutor:
|
|
|
903
954
|
|
|
904
955
|
return query
|
|
905
956
|
|
|
906
|
-
def _build_graphql_body(
|
|
957
|
+
def _build_graphql_body(
|
|
958
|
+
self,
|
|
959
|
+
graphql_config: dict[str, Any],
|
|
960
|
+
params: dict[str, Any],
|
|
961
|
+
param_defaults: dict[str, Any] | None = None,
|
|
962
|
+
) -> dict[str, Any]:
|
|
907
963
|
"""Build GraphQL request body with variable substitution and field selection.
|
|
908
964
|
|
|
909
965
|
Args:
|
|
910
966
|
graphql_config: GraphQL configuration from x-airbyte-body-type extension
|
|
911
967
|
params: Parameters from execute() call
|
|
968
|
+
param_defaults: Default values for params from query_params_schema
|
|
912
969
|
|
|
913
970
|
Returns:
|
|
914
971
|
GraphQL request body: {"query": "...", "variables": {...}}
|
|
@@ -922,7 +979,7 @@ class LocalExecutor:
|
|
|
922
979
|
|
|
923
980
|
# Substitute variables from params
|
|
924
981
|
if "variables" in graphql_config and graphql_config["variables"]:
|
|
925
|
-
body["variables"] = self._interpolate_variables(graphql_config["variables"], params)
|
|
982
|
+
body["variables"] = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
|
|
926
983
|
|
|
927
984
|
# Add operation name if specified
|
|
928
985
|
if "operationName" in graphql_config:
|
|
@@ -981,7 +1038,12 @@ class LocalExecutor:
|
|
|
981
1038
|
fields_str = " ".join(graphql_fields)
|
|
982
1039
|
return query.replace("{{ fields }}", fields_str)
|
|
983
1040
|
|
|
984
|
-
def _interpolate_variables(
|
|
1041
|
+
def _interpolate_variables(
|
|
1042
|
+
self,
|
|
1043
|
+
variables: dict[str, Any],
|
|
1044
|
+
params: dict[str, Any],
|
|
1045
|
+
param_defaults: dict[str, Any] | None = None,
|
|
1046
|
+
) -> dict[str, Any]:
|
|
985
1047
|
"""Recursively interpolate variables using params.
|
|
986
1048
|
|
|
987
1049
|
Preserves types (doesn't stringify everything).
|
|
@@ -990,15 +1052,18 @@ class LocalExecutor:
|
|
|
990
1052
|
- Direct replacement: "{{ owner }}" → params["owner"] (preserves type)
|
|
991
1053
|
- Nested objects: {"input": {"name": "{{ name }}"}}
|
|
992
1054
|
- Arrays: [{"id": "{{ id }}"}]
|
|
993
|
-
-
|
|
1055
|
+
- Default values: "{{ per_page }}" → param_defaults["per_page"] if not in params
|
|
1056
|
+
- Unsubstituted placeholders: "{{ states }}" → None (for optional params without defaults)
|
|
994
1057
|
|
|
995
1058
|
Args:
|
|
996
1059
|
variables: Variables dict with template placeholders
|
|
997
1060
|
params: Parameters to substitute
|
|
1061
|
+
param_defaults: Default values for params from query_params_schema
|
|
998
1062
|
|
|
999
1063
|
Returns:
|
|
1000
1064
|
Interpolated variables dict with types preserved
|
|
1001
1065
|
"""
|
|
1066
|
+
defaults = param_defaults or {}
|
|
1002
1067
|
|
|
1003
1068
|
def interpolate_value(value: Any) -> Any:
|
|
1004
1069
|
if isinstance(value, str):
|
|
@@ -1012,8 +1077,15 @@ class LocalExecutor:
|
|
|
1012
1077
|
value = value.replace(placeholder, str(param_value))
|
|
1013
1078
|
|
|
1014
1079
|
# Check if any unsubstituted placeholders remain
|
|
1015
|
-
# If so, return None (treats as "not provided" for optional params)
|
|
1016
1080
|
if re.search(r"\{\{\s*\w+\s*\}\}", value):
|
|
1081
|
+
# Extract placeholder name and check for default value
|
|
1082
|
+
match = re.search(r"\{\{\s*(\w+)\s*\}\}", value)
|
|
1083
|
+
if match:
|
|
1084
|
+
param_name = match.group(1)
|
|
1085
|
+
if param_name in defaults:
|
|
1086
|
+
# Use default value (preserves type)
|
|
1087
|
+
return defaults[param_name]
|
|
1088
|
+
# No default found - return None (for optional params)
|
|
1017
1089
|
return None
|
|
1018
1090
|
|
|
1019
1091
|
return value
|
|
@@ -1056,7 +1128,7 @@ class LocalExecutor:
|
|
|
1056
1128
|
if not action:
|
|
1057
1129
|
return response_data
|
|
1058
1130
|
|
|
1059
|
-
is_array_action = action in (Action.LIST, Action.
|
|
1131
|
+
is_array_action = action in (Action.LIST, Action.API_SEARCH)
|
|
1060
1132
|
|
|
1061
1133
|
try:
|
|
1062
1134
|
# Parse and apply JSONPath expression
|
|
@@ -1151,17 +1223,14 @@ class LocalExecutor:
|
|
|
1151
1223
|
if action not in (Action.CREATE, Action.UPDATE):
|
|
1152
1224
|
return
|
|
1153
1225
|
|
|
1154
|
-
#
|
|
1155
|
-
|
|
1226
|
+
# Get the request schema to find truly required fields
|
|
1227
|
+
request_schema = endpoint.request_schema
|
|
1228
|
+
if not request_schema:
|
|
1156
1229
|
return
|
|
1157
1230
|
|
|
1158
|
-
#
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
missing_fields = []
|
|
1162
|
-
for field in endpoint.body_fields:
|
|
1163
|
-
if field not in params:
|
|
1164
|
-
missing_fields.append(field)
|
|
1231
|
+
# Only validate fields explicitly marked as required in the schema
|
|
1232
|
+
required_fields = request_schema.get("required", [])
|
|
1233
|
+
missing_fields = [field for field in required_fields if field not in params]
|
|
1165
1234
|
|
|
1166
1235
|
if missing_fields:
|
|
1167
1236
|
raise MissingParameterError(
|
|
@@ -1189,7 +1258,7 @@ class LocalExecutor:
|
|
|
1189
1258
|
|
|
1190
1259
|
|
|
1191
1260
|
class _StandardOperationHandler:
|
|
1192
|
-
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE,
|
|
1261
|
+
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE, API_SEARCH, AUTHORIZE)."""
|
|
1193
1262
|
|
|
1194
1263
|
def __init__(self, context: _OperationContext):
|
|
1195
1264
|
self.ctx = context
|
|
@@ -1202,7 +1271,7 @@ class _StandardOperationHandler:
|
|
|
1202
1271
|
Action.CREATE,
|
|
1203
1272
|
Action.UPDATE,
|
|
1204
1273
|
Action.DELETE,
|
|
1205
|
-
Action.
|
|
1274
|
+
Action.API_SEARCH,
|
|
1206
1275
|
Action.AUTHORIZE,
|
|
1207
1276
|
}
|
|
1208
1277
|
|