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.

Files changed (33) hide show
  1. airbyte_agent_asana/__init__.py +6 -6
  2. airbyte_agent_asana/_vendored/connector_sdk/auth_strategies.py +2 -5
  3. airbyte_agent_asana/_vendored/connector_sdk/auth_template.py +1 -1
  4. airbyte_agent_asana/_vendored/connector_sdk/cloud_utils/client.py +26 -26
  5. airbyte_agent_asana/_vendored/connector_sdk/connector_model_loader.py +11 -4
  6. airbyte_agent_asana/_vendored/connector_sdk/constants.py +1 -1
  7. airbyte_agent_asana/_vendored/connector_sdk/executor/hosted_executor.py +10 -11
  8. airbyte_agent_asana/_vendored/connector_sdk/executor/local_executor.py +94 -25
  9. airbyte_agent_asana/_vendored/connector_sdk/extensions.py +43 -5
  10. airbyte_agent_asana/_vendored/connector_sdk/http/response.py +2 -0
  11. airbyte_agent_asana/_vendored/connector_sdk/introspection.py +262 -0
  12. airbyte_agent_asana/_vendored/connector_sdk/logging/logger.py +9 -9
  13. airbyte_agent_asana/_vendored/connector_sdk/logging/types.py +10 -10
  14. airbyte_agent_asana/_vendored/connector_sdk/observability/config.py +179 -0
  15. airbyte_agent_asana/_vendored/connector_sdk/observability/models.py +6 -6
  16. airbyte_agent_asana/_vendored/connector_sdk/observability/session.py +41 -32
  17. airbyte_agent_asana/_vendored/connector_sdk/performance/metrics.py +3 -3
  18. airbyte_agent_asana/_vendored/connector_sdk/schema/base.py +17 -17
  19. airbyte_agent_asana/_vendored/connector_sdk/schema/components.py +59 -58
  20. airbyte_agent_asana/_vendored/connector_sdk/schema/extensions.py +9 -9
  21. airbyte_agent_asana/_vendored/connector_sdk/schema/operations.py +32 -32
  22. airbyte_agent_asana/_vendored/connector_sdk/schema/security.py +44 -34
  23. airbyte_agent_asana/_vendored/connector_sdk/secrets.py +2 -2
  24. airbyte_agent_asana/_vendored/connector_sdk/telemetry/events.py +9 -8
  25. airbyte_agent_asana/_vendored/connector_sdk/telemetry/tracker.py +9 -5
  26. airbyte_agent_asana/_vendored/connector_sdk/types.py +7 -3
  27. airbyte_agent_asana/connector.py +88 -3
  28. airbyte_agent_asana/connector_model.py +6 -0
  29. airbyte_agent_asana/models.py +29 -29
  30. airbyte_agent_asana/types.py +1 -1
  31. {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/METADATA +11 -11
  32. {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/RECORD +33 -31
  33. {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/WHEEL +0 -0
@@ -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", "ProjectCurrentStatusCreatedBy", "ProjectCurrentStatusAuthor", "ProjectCurrentStatus", "ProjectMembersItem", "ProjectWorkspace", "ProjectCurrentStatusUpdate", "ProjectOwner", "ProjectTeam", "ProjectFollowersItem", "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"]
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. " 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:
@@ -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 connector instances for users
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 instance
26
- instance_id = await client.get_connector_instance_id(
25
+ # Get a connector ID
26
+ connector_id = await client.get_connector_id(
27
27
  external_user_id="user-123",
28
- connector_definition_id="stripe-def-456"
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
- instance_id=instance_id,
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 get_connector_instance_id(
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 instance ID for a user.
113
+ """Get connector ID for a user.
114
114
 
115
- Looks up the connector instance that belongs to the specified user
116
- and connector definition. Validates that exactly one instance exists.
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 instance ID (UUID string)
123
+ Connector ID (UUID string)
124
124
 
125
125
  Raises:
126
- ValueError: If 0 or more than 1 instance is found
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
- instance_id = await client.get_connector_instance_id(
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/instances_for_user"
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
- instances = data["instances"]
149
+ connectors = data["connectors"]
150
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}'")
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(instances) > 1:
154
+ if len(connectors) > 1:
155
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)}"
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
- instance_id = instances[0]["id"]
162
- return instance_id
161
+ connector_id = connectors[0]["id"]
162
+ return connector_id
163
163
 
164
164
  async def execute_connector(
165
165
  self,
166
- instance_id: str,
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
- instance_id: Connector instance UUID
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
- instance_id="inst-123",
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/instances/{instance_id}/execute"
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(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
+ )
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
@@ -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."""
@@ -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 instance
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 instance.
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 connector model
90
- 2. Look up the user's connector instance ID
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 instance or multiple instances found for user
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 instance ID for this user
130
- instance_id = await self._cloud_client.get_connector_instance_id(
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.instance_id", instance_id)
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
- instance_id=instance_id,
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
- # Instance lookup validation error (0 or >1 instances)
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
- return self._build_graphql_body(endpoint.graphql_body, params)
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
- return {"data": body}
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(self, graphql_config: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
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(self, variables: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
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
- - Unsubstituted placeholders: "{{ states }}" → None (for optional params)
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.SEARCH)
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
- # Check if endpoint has body fields defined
1155
- if not endpoint.body_fields:
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
- # For now, we treat all body_fields as potentially required for CREATE/UPDATE
1159
- # In a more advanced implementation, we could parse the request schema
1160
- # to identify truly required fields
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, SEARCH, AUTHORIZE)."""
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.SEARCH,
1274
+ Action.API_SEARCH,
1206
1275
  Action.AUTHORIZE,
1207
1276
  }
1208
1277