airbyte-agent-shopify 0.1.1__py3-none-any.whl → 0.1.13__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_shopify/_vendored/connector_sdk/auth_strategies.py +55 -4
- airbyte_agent_shopify/_vendored/connector_sdk/connector_model_loader.py +167 -16
- airbyte_agent_shopify/_vendored/connector_sdk/executor/local_executor.py +51 -2
- airbyte_agent_shopify/_vendored/connector_sdk/introspection.py +222 -10
- airbyte_agent_shopify/_vendored/connector_sdk/schema/base.py +40 -3
- airbyte_agent_shopify/_vendored/connector_sdk/schema/components.py +5 -0
- airbyte_agent_shopify/_vendored/connector_sdk/schema/extensions.py +71 -0
- airbyte_agent_shopify/_vendored/connector_sdk/schema/security.py +14 -1
- airbyte_agent_shopify/_vendored/connector_sdk/types.py +10 -0
- airbyte_agent_shopify/connector.py +94 -32
- airbyte_agent_shopify/connector_model.py +1 -2
- {airbyte_agent_shopify-0.1.1.dist-info → airbyte_agent_shopify-0.1.13.dist-info}/METADATA +44 -10
- {airbyte_agent_shopify-0.1.1.dist-info → airbyte_agent_shopify-0.1.13.dist-info}/RECORD +14 -14
- {airbyte_agent_shopify-0.1.1.dist-info → airbyte_agent_shopify-0.1.13.dist-info}/WHEEL +0 -0
|
@@ -152,6 +152,11 @@ class OAuth2AuthConfig(TypedDict, total=False):
|
|
|
152
152
|
Note: Any config key can be used as a template variable in refresh_url.
|
|
153
153
|
Common patterns: subdomain (Zendesk), shop (Shopify), region (AWS-style APIs).
|
|
154
154
|
|
|
155
|
+
additional_headers: Extra headers to inject alongside the OAuth2 Bearer token.
|
|
156
|
+
Useful for APIs that require both OAuth and an API key/client ID header.
|
|
157
|
+
Values support Jinja2 {{ variable }} template syntax to reference secrets.
|
|
158
|
+
Example: {"Amazon-Advertising-API-ClientId": "{{ client_id }}"}
|
|
159
|
+
|
|
155
160
|
Examples:
|
|
156
161
|
GitHub (simple):
|
|
157
162
|
{"header": "Authorization", "prefix": "Bearer"}
|
|
@@ -169,6 +174,14 @@ class OAuth2AuthConfig(TypedDict, total=False):
|
|
|
169
174
|
"auth_style": "basic",
|
|
170
175
|
"body_format": "json"
|
|
171
176
|
}
|
|
177
|
+
|
|
178
|
+
Amazon Ads (OAuth + additional client ID header):
|
|
179
|
+
{
|
|
180
|
+
"refresh_url": "https://api.amazon.com/auth/o2/token",
|
|
181
|
+
"additional_headers": {
|
|
182
|
+
"Amazon-Advertising-API-ClientId": "{{ client_id }}"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
172
185
|
"""
|
|
173
186
|
|
|
174
187
|
header: str
|
|
@@ -177,6 +190,7 @@ class OAuth2AuthConfig(TypedDict, total=False):
|
|
|
177
190
|
auth_style: AuthStyle
|
|
178
191
|
body_format: BodyFormat
|
|
179
192
|
subdomain: str
|
|
193
|
+
additional_headers: dict[str, str]
|
|
180
194
|
|
|
181
195
|
|
|
182
196
|
class OAuth2AuthSecrets(TypedDict):
|
|
@@ -552,9 +566,10 @@ class OAuth2AuthStrategy(AuthStrategy):
|
|
|
552
566
|
config: OAuth2AuthConfig,
|
|
553
567
|
secrets: OAuth2AuthSecrets,
|
|
554
568
|
) -> dict[str, str]:
|
|
555
|
-
"""Inject OAuth2 access token
|
|
569
|
+
"""Inject OAuth2 access token and additional headers.
|
|
556
570
|
|
|
557
|
-
Creates a copy of the headers dict with the OAuth2 token added
|
|
571
|
+
Creates a copy of the headers dict with the OAuth2 token added,
|
|
572
|
+
plus any additional headers configured via additional_headers.
|
|
558
573
|
The original headers dict is not modified.
|
|
559
574
|
|
|
560
575
|
Args:
|
|
@@ -563,7 +578,7 @@ class OAuth2AuthStrategy(AuthStrategy):
|
|
|
563
578
|
secrets: OAuth2 credentials including access_token
|
|
564
579
|
|
|
565
580
|
Returns:
|
|
566
|
-
New headers dict with OAuth2 token
|
|
581
|
+
New headers dict with OAuth2 token and additional headers injected
|
|
567
582
|
|
|
568
583
|
Raises:
|
|
569
584
|
AuthenticationError: If access_token is missing
|
|
@@ -589,8 +604,44 @@ class OAuth2AuthStrategy(AuthStrategy):
|
|
|
589
604
|
# Extract secret value (handle both SecretStr and plain str)
|
|
590
605
|
token_value = extract_secret_value(access_token)
|
|
591
606
|
|
|
592
|
-
# Inject
|
|
607
|
+
# Inject OAuth2 Bearer token
|
|
593
608
|
headers[header_name] = f"{prefix} {token_value}"
|
|
609
|
+
|
|
610
|
+
# Inject additional headers if configured
|
|
611
|
+
additional_headers = config.get("additional_headers")
|
|
612
|
+
if additional_headers:
|
|
613
|
+
headers = self._inject_additional_headers(headers, additional_headers, secrets)
|
|
614
|
+
|
|
615
|
+
return headers
|
|
616
|
+
|
|
617
|
+
def _inject_additional_headers(
|
|
618
|
+
self,
|
|
619
|
+
headers: dict[str, str],
|
|
620
|
+
additional_headers: dict[str, str],
|
|
621
|
+
secrets: dict[str, Any],
|
|
622
|
+
) -> dict[str, str]:
|
|
623
|
+
"""Inject additional headers with Jinja2 template variable substitution.
|
|
624
|
+
|
|
625
|
+
Processes additional_headers config, substituting {{ variable }} patterns
|
|
626
|
+
with values from secrets using Jinja2 templating.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
headers: Headers dict to add to (modified in place)
|
|
630
|
+
additional_headers: Header name -> value template mapping
|
|
631
|
+
secrets: Secrets dict for variable substitution
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Headers dict with additional headers added
|
|
635
|
+
"""
|
|
636
|
+
# Build template context with extracted secret values
|
|
637
|
+
template_context = {key: extract_secret_value(value) for key, value in secrets.items()}
|
|
638
|
+
|
|
639
|
+
for header_name, value_template in additional_headers.items():
|
|
640
|
+
# Use Jinja2 templating for variable substitution
|
|
641
|
+
template = Template(value_template)
|
|
642
|
+
header_value = template.render(**template_context)
|
|
643
|
+
headers[header_name] = header_value
|
|
644
|
+
|
|
594
645
|
return headers
|
|
595
646
|
|
|
596
647
|
def validate_credentials(self, secrets: OAuth2AuthSecrets) -> None: # type: ignore[override]
|
|
@@ -62,6 +62,53 @@ class TokenExtractValidationError(ConnectorModelLoaderError):
|
|
|
62
62
|
pass
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
# Expected auth_mapping keys for each auth type.
|
|
66
|
+
# These are the auth parameters that each security scheme expects, NOT the user's credential field names.
|
|
67
|
+
EXPECTED_AUTH_MAPPING_KEYS: dict[AuthType, set[str]] = {
|
|
68
|
+
AuthType.BEARER: {"token"},
|
|
69
|
+
AuthType.BASIC: {"username", "password"},
|
|
70
|
+
AuthType.API_KEY: {"api_key"},
|
|
71
|
+
AuthType.OAUTH2: {"access_token", "refresh_token", "client_id", "client_secret"},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_auth_mapping_keys(
|
|
76
|
+
auth_type: AuthType,
|
|
77
|
+
auth_config: AirbyteAuthConfig | None,
|
|
78
|
+
scheme_name: str = "default",
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Validate that auth_mapping keys match expected parameters for the auth type.
|
|
81
|
+
|
|
82
|
+
The auth_mapping keys must be the parameters expected by the security scheme
|
|
83
|
+
(e.g., "token" for bearer), not the user's credential field names.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
auth_type: The authentication type
|
|
87
|
+
auth_config: The x-airbyte-auth-config containing auth_mapping
|
|
88
|
+
scheme_name: Name of the security scheme for error messages
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
InvalidOpenAPIError: If auth_mapping keys don't match expected parameters
|
|
92
|
+
"""
|
|
93
|
+
if auth_config is None or auth_config.auth_mapping is None:
|
|
94
|
+
return # No explicit auth_mapping, will use defaults
|
|
95
|
+
|
|
96
|
+
expected_keys = EXPECTED_AUTH_MAPPING_KEYS.get(auth_type)
|
|
97
|
+
if expected_keys is None:
|
|
98
|
+
return # Unknown auth type, skip validation
|
|
99
|
+
|
|
100
|
+
actual_keys = set(auth_config.auth_mapping.keys())
|
|
101
|
+
invalid_keys = actual_keys - expected_keys
|
|
102
|
+
|
|
103
|
+
if invalid_keys:
|
|
104
|
+
raise InvalidOpenAPIError(
|
|
105
|
+
f"Invalid auth_mapping keys for {auth_type.value} auth in scheme '{scheme_name}': {invalid_keys}. "
|
|
106
|
+
f"Expected keys for {auth_type.value}: {sorted(expected_keys)}. "
|
|
107
|
+
f"Note: auth_mapping keys must be the auth parameters (e.g., 'token' for bearer), "
|
|
108
|
+
f'not your credential field names. Use template syntax to map: token: "${{your_field}}"'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
65
112
|
def extract_path_params(path: str) -> list[str]:
|
|
66
113
|
"""Extract parameter names from path template.
|
|
67
114
|
|
|
@@ -145,6 +192,87 @@ def _deproxy_schema(obj: Any) -> Any:
|
|
|
145
192
|
return obj
|
|
146
193
|
|
|
147
194
|
|
|
195
|
+
def _type_includes(type_value: Any, target: str) -> bool:
|
|
196
|
+
if isinstance(type_value, list):
|
|
197
|
+
return target in type_value
|
|
198
|
+
return type_value == target
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _flatten_cache_properties(properties: dict[str, Any], prefix: str) -> list[str]:
|
|
202
|
+
entries: list[str] = []
|
|
203
|
+
for prop_name, prop in properties.items():
|
|
204
|
+
path = f"{prefix}{prop_name}" if prefix else prop_name
|
|
205
|
+
entries.append(path)
|
|
206
|
+
|
|
207
|
+
prop_type = getattr(prop, "type", None) if not isinstance(prop, dict) else prop.get("type")
|
|
208
|
+
prop_properties = getattr(prop, "properties", None) if not isinstance(prop, dict) else prop.get("properties")
|
|
209
|
+
|
|
210
|
+
if _type_includes(prop_type, "array"):
|
|
211
|
+
array_path = f"{path}[]"
|
|
212
|
+
entries.append(array_path)
|
|
213
|
+
if isinstance(prop_properties, dict):
|
|
214
|
+
entries.extend(_flatten_cache_properties(prop_properties, prefix=f"{array_path}."))
|
|
215
|
+
elif isinstance(prop_properties, dict):
|
|
216
|
+
entries.extend(_flatten_cache_properties(prop_properties, prefix=f"{path}."))
|
|
217
|
+
|
|
218
|
+
return entries
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _flatten_cache_field_paths(field: Any) -> list[str]:
|
|
222
|
+
field_name = getattr(field, "name", None) if not isinstance(field, dict) else field.get("name")
|
|
223
|
+
if not isinstance(field_name, str) or not field_name:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
field_type = getattr(field, "type", None) if not isinstance(field, dict) else field.get("type")
|
|
227
|
+
field_properties = getattr(field, "properties", None) if not isinstance(field, dict) else field.get("properties")
|
|
228
|
+
|
|
229
|
+
entries = [field_name]
|
|
230
|
+
if _type_includes(field_type, "array"):
|
|
231
|
+
array_path = f"{field_name}[]"
|
|
232
|
+
entries.append(array_path)
|
|
233
|
+
if isinstance(field_properties, dict):
|
|
234
|
+
entries.extend(_flatten_cache_properties(field_properties, prefix=f"{array_path}."))
|
|
235
|
+
elif isinstance(field_properties, dict):
|
|
236
|
+
entries.extend(_flatten_cache_properties(field_properties, prefix=f"{field_name}."))
|
|
237
|
+
|
|
238
|
+
return entries
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _dedupe_strings(values: list[str]) -> list[str]:
|
|
242
|
+
seen: set[str] = set()
|
|
243
|
+
ordered: list[str] = []
|
|
244
|
+
for value in values:
|
|
245
|
+
if value not in seen:
|
|
246
|
+
seen.add(value)
|
|
247
|
+
ordered.append(value)
|
|
248
|
+
return ordered
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _extract_search_field_paths(spec: OpenAPIConnector) -> dict[str, list[str]]:
|
|
252
|
+
cache_config = getattr(spec.info, "x_airbyte_cache", None)
|
|
253
|
+
entities = getattr(cache_config, "entities", None)
|
|
254
|
+
if not isinstance(entities, list):
|
|
255
|
+
return {}
|
|
256
|
+
|
|
257
|
+
search_fields: dict[str, list[str]] = {}
|
|
258
|
+
for entity in entities:
|
|
259
|
+
entity_name = getattr(entity, "entity", None) if not isinstance(entity, dict) else entity.get("entity")
|
|
260
|
+
if not isinstance(entity_name, str) or not entity_name:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
fields = getattr(entity, "fields", None) if not isinstance(entity, dict) else entity.get("fields")
|
|
264
|
+
if not isinstance(fields, list):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
field_paths: list[str] = []
|
|
268
|
+
for field in fields:
|
|
269
|
+
field_paths.extend(_flatten_cache_field_paths(field))
|
|
270
|
+
|
|
271
|
+
search_fields[entity_name] = _dedupe_strings(field_paths)
|
|
272
|
+
|
|
273
|
+
return search_fields
|
|
274
|
+
|
|
275
|
+
|
|
148
276
|
def parse_openapi_spec(raw_config: dict) -> OpenAPIConnector:
|
|
149
277
|
"""Parse OpenAPI specification from YAML.
|
|
150
278
|
|
|
@@ -188,7 +316,7 @@ def parse_openapi_spec(raw_config: dict) -> OpenAPIConnector:
|
|
|
188
316
|
|
|
189
317
|
def _extract_request_body_config(
|
|
190
318
|
request_body: RequestBody | None, spec_dict: dict[str, Any]
|
|
191
|
-
) -> tuple[list[str], dict[str, Any] | None, dict[str, Any] | None]:
|
|
319
|
+
) -> tuple[list[str], dict[str, Any] | None, dict[str, Any] | None, dict[str, Any]]:
|
|
192
320
|
"""Extract request body configuration (GraphQL or standard).
|
|
193
321
|
|
|
194
322
|
Args:
|
|
@@ -196,17 +324,19 @@ def _extract_request_body_config(
|
|
|
196
324
|
spec_dict: Full OpenAPI spec dict for $ref resolution
|
|
197
325
|
|
|
198
326
|
Returns:
|
|
199
|
-
Tuple of (body_fields, request_schema, graphql_body)
|
|
327
|
+
Tuple of (body_fields, request_schema, graphql_body, request_body_defaults)
|
|
200
328
|
- body_fields: List of field names for standard JSON/form bodies
|
|
201
329
|
- request_schema: Resolved request schema dict (for standard bodies)
|
|
202
330
|
- graphql_body: GraphQL body configuration dict (for GraphQL bodies)
|
|
331
|
+
- request_body_defaults: Default values for request body fields
|
|
203
332
|
"""
|
|
204
333
|
body_fields: list[str] = []
|
|
205
334
|
request_schema: dict[str, Any] | None = None
|
|
206
335
|
graphql_body: dict[str, Any] | None = None
|
|
336
|
+
request_body_defaults: dict[str, Any] = {}
|
|
207
337
|
|
|
208
338
|
if not request_body:
|
|
209
|
-
return body_fields, request_schema, graphql_body
|
|
339
|
+
return body_fields, request_schema, graphql_body, request_body_defaults
|
|
210
340
|
|
|
211
341
|
# Check for GraphQL extension and extract GraphQL body configuration
|
|
212
342
|
if request_body.x_airbyte_body_type:
|
|
@@ -216,7 +346,7 @@ def _extract_request_body_config(
|
|
|
216
346
|
if isinstance(body_type_config, GraphQLBodyConfig):
|
|
217
347
|
# Convert Pydantic model to dict, excluding None values
|
|
218
348
|
graphql_body = body_type_config.model_dump(exclude_none=True, by_alias=False)
|
|
219
|
-
return body_fields, request_schema, graphql_body
|
|
349
|
+
return body_fields, request_schema, graphql_body, request_body_defaults
|
|
220
350
|
|
|
221
351
|
# Parse standard request body
|
|
222
352
|
for content_type_key, media_type in request_body.content.items():
|
|
@@ -226,11 +356,15 @@ def _extract_request_body_config(
|
|
|
226
356
|
# Resolve all $refs in the schema using jsonref
|
|
227
357
|
request_schema = resolve_schema_refs(schema, spec_dict)
|
|
228
358
|
|
|
229
|
-
# Extract body field names from resolved schema
|
|
359
|
+
# Extract body field names and defaults from resolved schema
|
|
230
360
|
if isinstance(request_schema, dict) and "properties" in request_schema:
|
|
231
361
|
body_fields = list(request_schema["properties"].keys())
|
|
362
|
+
# Extract default values for each property
|
|
363
|
+
for field_name, field_schema in request_schema["properties"].items():
|
|
364
|
+
if isinstance(field_schema, dict) and "default" in field_schema:
|
|
365
|
+
request_body_defaults[field_name] = field_schema["default"]
|
|
232
366
|
|
|
233
|
-
return body_fields, request_schema, graphql_body
|
|
367
|
+
return body_fields, request_schema, graphql_body, request_body_defaults
|
|
234
368
|
|
|
235
369
|
|
|
236
370
|
def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel:
|
|
@@ -315,6 +449,8 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
315
449
|
query_params: list[str] = []
|
|
316
450
|
query_params_schema: dict[str, dict[str, Any]] = {}
|
|
317
451
|
deep_object_params: list[str] = []
|
|
452
|
+
header_params: list[str] = []
|
|
453
|
+
header_params_schema: dict[str, dict[str, Any]] = {}
|
|
318
454
|
|
|
319
455
|
if operation.parameters:
|
|
320
456
|
for param in operation.parameters:
|
|
@@ -336,9 +472,12 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
336
472
|
# Check if this is a deepObject style parameter
|
|
337
473
|
if hasattr(param, "style") and param.style == "deepObject":
|
|
338
474
|
deep_object_params.append(param.name)
|
|
475
|
+
elif param.in_ == "header":
|
|
476
|
+
header_params.append(param.name)
|
|
477
|
+
header_params_schema[param.name] = schema_info
|
|
339
478
|
|
|
340
|
-
# Extract body fields from request schema
|
|
341
|
-
body_fields, request_schema, graphql_body = _extract_request_body_config(operation.request_body, spec_dict)
|
|
479
|
+
# Extract body fields and defaults from request schema
|
|
480
|
+
body_fields, request_schema, graphql_body, request_body_defaults = _extract_request_body_config(operation.request_body, spec_dict)
|
|
342
481
|
|
|
343
482
|
# Extract response schema
|
|
344
483
|
response_schema = None
|
|
@@ -372,6 +511,9 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
372
511
|
deep_object_params=deep_object_params,
|
|
373
512
|
path_params=path_params,
|
|
374
513
|
path_params_schema=path_params_schema,
|
|
514
|
+
header_params=header_params,
|
|
515
|
+
header_params_schema=header_params_schema,
|
|
516
|
+
request_body_defaults=request_body_defaults,
|
|
375
517
|
content_type=content_type,
|
|
376
518
|
request_schema=request_schema,
|
|
377
519
|
response_schema=response_schema,
|
|
@@ -420,6 +562,8 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
420
562
|
if not connector_id:
|
|
421
563
|
raise InvalidOpenAPIError("Missing required x-airbyte-connector-id field")
|
|
422
564
|
|
|
565
|
+
search_field_paths = _extract_search_field_paths(spec)
|
|
566
|
+
|
|
423
567
|
# Create ConnectorModel
|
|
424
568
|
model = ConnectorModel(
|
|
425
569
|
id=connector_id,
|
|
@@ -430,6 +574,7 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
430
574
|
entities=entities,
|
|
431
575
|
openapi_spec=spec,
|
|
432
576
|
retry_config=retry_config,
|
|
577
|
+
search_field_paths=search_field_paths,
|
|
433
578
|
)
|
|
434
579
|
|
|
435
580
|
return model
|
|
@@ -535,6 +680,13 @@ def _parse_oauth2_config(scheme: Any) -> dict[str, str]:
|
|
|
535
680
|
if x_token_extract:
|
|
536
681
|
config["token_extract"] = x_token_extract
|
|
537
682
|
|
|
683
|
+
# Extract additional_headers from x-airbyte-auth-config extension
|
|
684
|
+
x_auth_config = getattr(scheme, "x_airbyte_auth_config", None)
|
|
685
|
+
if x_auth_config:
|
|
686
|
+
additional_headers = getattr(x_auth_config, "additional_headers", None)
|
|
687
|
+
if additional_headers:
|
|
688
|
+
config["additional_headers"] = additional_headers
|
|
689
|
+
|
|
538
690
|
return config
|
|
539
691
|
|
|
540
692
|
|
|
@@ -606,7 +758,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
606
758
|
description="Authentication bearer token",
|
|
607
759
|
format=None,
|
|
608
760
|
pattern=None,
|
|
609
|
-
airbyte_secret=False,
|
|
610
761
|
default=None,
|
|
611
762
|
)
|
|
612
763
|
},
|
|
@@ -626,7 +777,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
626
777
|
description="Authentication username",
|
|
627
778
|
format=None,
|
|
628
779
|
pattern=None,
|
|
629
|
-
airbyte_secret=False,
|
|
630
780
|
default=None,
|
|
631
781
|
),
|
|
632
782
|
"password": AuthConfigFieldSpec(
|
|
@@ -635,7 +785,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
635
785
|
description="Authentication password",
|
|
636
786
|
format=None,
|
|
637
787
|
pattern=None,
|
|
638
|
-
airbyte_secret=False,
|
|
639
788
|
default=None,
|
|
640
789
|
),
|
|
641
790
|
},
|
|
@@ -655,7 +804,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
655
804
|
description="API authentication key",
|
|
656
805
|
format=None,
|
|
657
806
|
pattern=None,
|
|
658
|
-
airbyte_secret=False,
|
|
659
807
|
default=None,
|
|
660
808
|
)
|
|
661
809
|
},
|
|
@@ -680,7 +828,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
680
828
|
description="OAuth2 access token",
|
|
681
829
|
format=None,
|
|
682
830
|
pattern=None,
|
|
683
|
-
airbyte_secret=False,
|
|
684
831
|
default=None,
|
|
685
832
|
),
|
|
686
833
|
"refresh_token": AuthConfigFieldSpec(
|
|
@@ -689,7 +836,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
689
836
|
description="OAuth2 refresh token (optional)",
|
|
690
837
|
format=None,
|
|
691
838
|
pattern=None,
|
|
692
|
-
airbyte_secret=False,
|
|
693
839
|
default=None,
|
|
694
840
|
),
|
|
695
841
|
"client_id": AuthConfigFieldSpec(
|
|
@@ -698,7 +844,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
698
844
|
description="OAuth2 client ID (optional)",
|
|
699
845
|
format=None,
|
|
700
846
|
pattern=None,
|
|
701
|
-
airbyte_secret=False,
|
|
702
847
|
default=None,
|
|
703
848
|
),
|
|
704
849
|
"client_secret": AuthConfigFieldSpec(
|
|
@@ -707,7 +852,6 @@ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
|
|
|
707
852
|
description="OAuth2 client secret (optional)",
|
|
708
853
|
format=None,
|
|
709
854
|
pattern=None,
|
|
710
|
-
airbyte_secret=False,
|
|
711
855
|
default=None,
|
|
712
856
|
),
|
|
713
857
|
},
|
|
@@ -819,6 +963,9 @@ def _parse_single_security_scheme(scheme: Any) -> AuthConfig:
|
|
|
819
963
|
oauth2_config = _parse_oauth2_config(scheme)
|
|
820
964
|
# Use explicit x-airbyte-auth-config if present, otherwise generate default
|
|
821
965
|
auth_config_obj = scheme.x_airbyte_auth_config or _generate_default_auth_config(AuthType.OAUTH2)
|
|
966
|
+
# Validate auth_mapping keys if explicitly provided
|
|
967
|
+
if scheme.x_airbyte_auth_config:
|
|
968
|
+
_validate_auth_mapping_keys(AuthType.OAUTH2, scheme.x_airbyte_auth_config)
|
|
822
969
|
return AuthConfig(
|
|
823
970
|
type=AuthType.OAUTH2,
|
|
824
971
|
config=oauth2_config,
|
|
@@ -829,6 +976,10 @@ def _parse_single_security_scheme(scheme: Any) -> AuthConfig:
|
|
|
829
976
|
# Use explicit x-airbyte-auth-config if present, otherwise generate default
|
|
830
977
|
auth_config_obj = scheme.x_airbyte_auth_config or _generate_default_auth_config(auth_type)
|
|
831
978
|
|
|
979
|
+
# Validate auth_mapping keys if explicitly provided
|
|
980
|
+
if scheme.x_airbyte_auth_config:
|
|
981
|
+
_validate_auth_mapping_keys(auth_type, scheme.x_airbyte_auth_config)
|
|
982
|
+
|
|
832
983
|
return AuthConfig(
|
|
833
984
|
type=auth_type,
|
|
834
985
|
config=auth_config,
|
|
@@ -64,6 +64,7 @@ class _OperationContext:
|
|
|
64
64
|
self.build_path = executor._build_path
|
|
65
65
|
self.extract_query_params = executor._extract_query_params
|
|
66
66
|
self.extract_body = executor._extract_body
|
|
67
|
+
self.extract_header_params = executor._extract_header_params
|
|
67
68
|
self.build_request_body = executor._build_request_body
|
|
68
69
|
self.determine_request_format = executor._determine_request_format
|
|
69
70
|
self.validate_required_body_fields = executor._validate_required_body_fields
|
|
@@ -693,6 +694,40 @@ class LocalExecutor:
|
|
|
693
694
|
"""
|
|
694
695
|
return {key: value for key, value in params.items() if key in allowed_fields and value is not None}
|
|
695
696
|
|
|
697
|
+
def _extract_header_params(self, endpoint: EndpointDefinition, params: dict[str, Any], body: dict[str, Any] | None = None) -> dict[str, str]:
|
|
698
|
+
"""Extract header parameters from params and schema defaults.
|
|
699
|
+
|
|
700
|
+
Also adds Content-Type header when there's a request body (unless already specified
|
|
701
|
+
as a header parameter in the OpenAPI spec).
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
endpoint: Endpoint definition with header_params and header_params_schema
|
|
705
|
+
params: All parameters
|
|
706
|
+
body: Request body (if any) - used to determine if Content-Type should be added
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
Dictionary of header name -> value
|
|
710
|
+
"""
|
|
711
|
+
headers: dict[str, str] = {}
|
|
712
|
+
|
|
713
|
+
for header_name in endpoint.header_params:
|
|
714
|
+
# Check if value is provided in params
|
|
715
|
+
if header_name in params and params[header_name] is not None:
|
|
716
|
+
headers[header_name] = str(params[header_name])
|
|
717
|
+
# Otherwise, use default from schema if available
|
|
718
|
+
elif header_name in endpoint.header_params_schema:
|
|
719
|
+
default_value = endpoint.header_params_schema[header_name].get("default")
|
|
720
|
+
if default_value is not None:
|
|
721
|
+
headers[header_name] = str(default_value)
|
|
722
|
+
|
|
723
|
+
# Add Content-Type header when there's a request body, but only if not already
|
|
724
|
+
# specified as a header parameter (which allows custom content types like
|
|
725
|
+
# application/vnd.spCampaign.v3+json)
|
|
726
|
+
if body is not None and endpoint.content_type and "Content-Type" not in headers:
|
|
727
|
+
headers["Content-Type"] = endpoint.content_type.value
|
|
728
|
+
|
|
729
|
+
return headers
|
|
730
|
+
|
|
696
731
|
def _serialize_deep_object_params(self, params: dict[str, Any], deep_object_param_names: list[str]) -> dict[str, Any]:
|
|
697
732
|
"""Serialize deepObject parameters to bracket notation format.
|
|
698
733
|
|
|
@@ -848,7 +883,15 @@ class LocalExecutor:
|
|
|
848
883
|
param_defaults = {name: schema.get("default") for name, schema in endpoint.query_params_schema.items() if "default" in schema}
|
|
849
884
|
return self._build_graphql_body(endpoint.graphql_body, params, param_defaults)
|
|
850
885
|
elif endpoint.body_fields:
|
|
851
|
-
|
|
886
|
+
# Start with defaults from request body schema
|
|
887
|
+
body = dict(endpoint.request_body_defaults)
|
|
888
|
+
# Override with user-provided params (filtering out None values)
|
|
889
|
+
user_body = self._extract_body(endpoint.body_fields, params)
|
|
890
|
+
body.update(user_body)
|
|
891
|
+
return body if body else None
|
|
892
|
+
elif endpoint.request_body_defaults:
|
|
893
|
+
# If no body_fields but we have defaults, return the defaults
|
|
894
|
+
return dict(endpoint.request_body_defaults)
|
|
852
895
|
return None
|
|
853
896
|
|
|
854
897
|
def _flatten_form_data(self, data: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
|
|
@@ -989,7 +1032,9 @@ class LocalExecutor:
|
|
|
989
1032
|
if "variables" in graphql_config and graphql_config["variables"]:
|
|
990
1033
|
variables = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
|
|
991
1034
|
# Filter out None values (optional fields not provided) - matches REST _extract_body() behavior
|
|
992
|
-
|
|
1035
|
+
# But preserve None for variables explicitly marked as nullable (e.g., to unassign a user)
|
|
1036
|
+
nullable_vars = set(graphql_config.get("x-airbyte-nullable-variables") or [])
|
|
1037
|
+
body["variables"] = {k: v for k, v in variables.items() if v is not None or k in nullable_vars}
|
|
993
1038
|
|
|
994
1039
|
# Add operation name if specified
|
|
995
1040
|
if "operationName" in graphql_config:
|
|
@@ -1484,6 +1529,9 @@ class _StandardOperationHandler:
|
|
|
1484
1529
|
# Determine request format (json/data parameters)
|
|
1485
1530
|
request_kwargs = self.ctx.determine_request_format(endpoint, body)
|
|
1486
1531
|
|
|
1532
|
+
# Extract header parameters from OpenAPI operation (pass body to add Content-Type)
|
|
1533
|
+
header_params = self.ctx.extract_header_params(endpoint, params, body)
|
|
1534
|
+
|
|
1487
1535
|
# Execute async HTTP request
|
|
1488
1536
|
response_data, response_headers = await self.ctx.http_client.request(
|
|
1489
1537
|
method=endpoint.method,
|
|
@@ -1491,6 +1539,7 @@ class _StandardOperationHandler:
|
|
|
1491
1539
|
params=query_params if query_params else None,
|
|
1492
1540
|
json=request_kwargs.get("json"),
|
|
1493
1541
|
data=request_kwargs.get("data"),
|
|
1542
|
+
headers=header_params if header_params else None,
|
|
1494
1543
|
)
|
|
1495
1544
|
|
|
1496
1545
|
# Extract metadata from original response (before record extraction)
|