airbyte-agent-zendesk-support 0.18.49__py3-none-any.whl → 0.18.57__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airbyte_agent_zendesk_support/__init__.py +8 -8
- airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_strategies.py +55 -4
- airbyte_agent_zendesk_support/_vendored/connector_sdk/connector_model_loader.py +29 -8
- airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/local_executor.py +51 -2
- airbyte_agent_zendesk_support/_vendored/connector_sdk/http_client.py +2 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/logger.py +10 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/types.py +1 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/base.py +7 -2
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/security.py +14 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/types.py +9 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/validation.py +12 -6
- airbyte_agent_zendesk_support/connector.py +28 -28
- airbyte_agent_zendesk_support/connector_model.py +1 -1
- airbyte_agent_zendesk_support/models.py +16 -16
- airbyte_agent_zendesk_support/types.py +235 -235
- {airbyte_agent_zendesk_support-0.18.49.dist-info → airbyte_agent_zendesk_support-0.18.57.dist-info}/METADATA +35 -22
- {airbyte_agent_zendesk_support-0.18.49.dist-info → airbyte_agent_zendesk_support-0.18.57.dist-info}/RECORD +18 -18
- {airbyte_agent_zendesk_support-0.18.49.dist-info → airbyte_agent_zendesk_support-0.18.57.dist-info}/WHEEL +0 -0
|
@@ -73,8 +73,8 @@ from .models import (
|
|
|
73
73
|
TicketFormsListResult,
|
|
74
74
|
ArticlesListResult,
|
|
75
75
|
ArticleAttachmentsListResult,
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
AirbyteSearchHit,
|
|
77
|
+
AirbyteSearchResult,
|
|
78
78
|
BrandsSearchData,
|
|
79
79
|
BrandsSearchResult,
|
|
80
80
|
GroupsSearchData,
|
|
@@ -141,8 +141,8 @@ from .types import (
|
|
|
141
141
|
ArticleAttachmentsListParams,
|
|
142
142
|
ArticleAttachmentsGetParams,
|
|
143
143
|
ArticleAttachmentsDownloadParams,
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
AirbyteSearchParams,
|
|
145
|
+
AirbyteSortOrder,
|
|
146
146
|
BrandsSearchFilter,
|
|
147
147
|
BrandsSearchQuery,
|
|
148
148
|
BrandsCondition,
|
|
@@ -250,8 +250,8 @@ __all__ = [
|
|
|
250
250
|
"TicketFormsListResult",
|
|
251
251
|
"ArticlesListResult",
|
|
252
252
|
"ArticleAttachmentsListResult",
|
|
253
|
-
"
|
|
254
|
-
"
|
|
253
|
+
"AirbyteSearchHit",
|
|
254
|
+
"AirbyteSearchResult",
|
|
255
255
|
"BrandsSearchData",
|
|
256
256
|
"BrandsSearchResult",
|
|
257
257
|
"GroupsSearchData",
|
|
@@ -316,8 +316,8 @@ __all__ = [
|
|
|
316
316
|
"ArticleAttachmentsListParams",
|
|
317
317
|
"ArticleAttachmentsGetParams",
|
|
318
318
|
"ArticleAttachmentsDownloadParams",
|
|
319
|
-
"
|
|
320
|
-
"
|
|
319
|
+
"AirbyteSearchParams",
|
|
320
|
+
"AirbyteSortOrder",
|
|
321
321
|
"BrandsSearchFilter",
|
|
322
322
|
"BrandsSearchQuery",
|
|
323
323
|
"BrandsCondition",
|
|
@@ -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]
|
|
@@ -188,7 +188,7 @@ def parse_openapi_spec(raw_config: dict) -> OpenAPIConnector:
|
|
|
188
188
|
|
|
189
189
|
def _extract_request_body_config(
|
|
190
190
|
request_body: RequestBody | None, spec_dict: dict[str, Any]
|
|
191
|
-
) -> tuple[list[str], dict[str, Any] | None, dict[str, Any] | None]:
|
|
191
|
+
) -> tuple[list[str], dict[str, Any] | None, dict[str, Any] | None, dict[str, Any]]:
|
|
192
192
|
"""Extract request body configuration (GraphQL or standard).
|
|
193
193
|
|
|
194
194
|
Args:
|
|
@@ -196,17 +196,19 @@ def _extract_request_body_config(
|
|
|
196
196
|
spec_dict: Full OpenAPI spec dict for $ref resolution
|
|
197
197
|
|
|
198
198
|
Returns:
|
|
199
|
-
Tuple of (body_fields, request_schema, graphql_body)
|
|
199
|
+
Tuple of (body_fields, request_schema, graphql_body, request_body_defaults)
|
|
200
200
|
- body_fields: List of field names for standard JSON/form bodies
|
|
201
201
|
- request_schema: Resolved request schema dict (for standard bodies)
|
|
202
202
|
- graphql_body: GraphQL body configuration dict (for GraphQL bodies)
|
|
203
|
+
- request_body_defaults: Default values for request body fields
|
|
203
204
|
"""
|
|
204
205
|
body_fields: list[str] = []
|
|
205
206
|
request_schema: dict[str, Any] | None = None
|
|
206
207
|
graphql_body: dict[str, Any] | None = None
|
|
208
|
+
request_body_defaults: dict[str, Any] = {}
|
|
207
209
|
|
|
208
210
|
if not request_body:
|
|
209
|
-
return body_fields, request_schema, graphql_body
|
|
211
|
+
return body_fields, request_schema, graphql_body, request_body_defaults
|
|
210
212
|
|
|
211
213
|
# Check for GraphQL extension and extract GraphQL body configuration
|
|
212
214
|
if request_body.x_airbyte_body_type:
|
|
@@ -216,7 +218,7 @@ def _extract_request_body_config(
|
|
|
216
218
|
if isinstance(body_type_config, GraphQLBodyConfig):
|
|
217
219
|
# Convert Pydantic model to dict, excluding None values
|
|
218
220
|
graphql_body = body_type_config.model_dump(exclude_none=True, by_alias=False)
|
|
219
|
-
return body_fields, request_schema, graphql_body
|
|
221
|
+
return body_fields, request_schema, graphql_body, request_body_defaults
|
|
220
222
|
|
|
221
223
|
# Parse standard request body
|
|
222
224
|
for content_type_key, media_type in request_body.content.items():
|
|
@@ -226,11 +228,15 @@ def _extract_request_body_config(
|
|
|
226
228
|
# Resolve all $refs in the schema using jsonref
|
|
227
229
|
request_schema = resolve_schema_refs(schema, spec_dict)
|
|
228
230
|
|
|
229
|
-
# Extract body field names from resolved schema
|
|
231
|
+
# Extract body field names and defaults from resolved schema
|
|
230
232
|
if isinstance(request_schema, dict) and "properties" in request_schema:
|
|
231
233
|
body_fields = list(request_schema["properties"].keys())
|
|
234
|
+
# Extract default values for each property
|
|
235
|
+
for field_name, field_schema in request_schema["properties"].items():
|
|
236
|
+
if isinstance(field_schema, dict) and "default" in field_schema:
|
|
237
|
+
request_body_defaults[field_name] = field_schema["default"]
|
|
232
238
|
|
|
233
|
-
return body_fields, request_schema, graphql_body
|
|
239
|
+
return body_fields, request_schema, graphql_body, request_body_defaults
|
|
234
240
|
|
|
235
241
|
|
|
236
242
|
def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel:
|
|
@@ -315,6 +321,8 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
315
321
|
query_params: list[str] = []
|
|
316
322
|
query_params_schema: dict[str, dict[str, Any]] = {}
|
|
317
323
|
deep_object_params: list[str] = []
|
|
324
|
+
header_params: list[str] = []
|
|
325
|
+
header_params_schema: dict[str, dict[str, Any]] = {}
|
|
318
326
|
|
|
319
327
|
if operation.parameters:
|
|
320
328
|
for param in operation.parameters:
|
|
@@ -336,9 +344,12 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
336
344
|
# Check if this is a deepObject style parameter
|
|
337
345
|
if hasattr(param, "style") and param.style == "deepObject":
|
|
338
346
|
deep_object_params.append(param.name)
|
|
347
|
+
elif param.in_ == "header":
|
|
348
|
+
header_params.append(param.name)
|
|
349
|
+
header_params_schema[param.name] = schema_info
|
|
339
350
|
|
|
340
|
-
# Extract body fields from request schema
|
|
341
|
-
body_fields, request_schema, graphql_body = _extract_request_body_config(operation.request_body, spec_dict)
|
|
351
|
+
# Extract body fields and defaults from request schema
|
|
352
|
+
body_fields, request_schema, graphql_body, request_body_defaults = _extract_request_body_config(operation.request_body, spec_dict)
|
|
342
353
|
|
|
343
354
|
# Extract response schema
|
|
344
355
|
response_schema = None
|
|
@@ -372,6 +383,9 @@ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel
|
|
|
372
383
|
deep_object_params=deep_object_params,
|
|
373
384
|
path_params=path_params,
|
|
374
385
|
path_params_schema=path_params_schema,
|
|
386
|
+
header_params=header_params,
|
|
387
|
+
header_params_schema=header_params_schema,
|
|
388
|
+
request_body_defaults=request_body_defaults,
|
|
375
389
|
content_type=content_type,
|
|
376
390
|
request_schema=request_schema,
|
|
377
391
|
response_schema=response_schema,
|
|
@@ -535,6 +549,13 @@ def _parse_oauth2_config(scheme: Any) -> dict[str, str]:
|
|
|
535
549
|
if x_token_extract:
|
|
536
550
|
config["token_extract"] = x_token_extract
|
|
537
551
|
|
|
552
|
+
# Extract additional_headers from x-airbyte-auth-config extension
|
|
553
|
+
x_auth_config = getattr(scheme, "x_airbyte_auth_config", None)
|
|
554
|
+
if x_auth_config:
|
|
555
|
+
additional_headers = getattr(x_auth_config, "additional_headers", None)
|
|
556
|
+
if additional_headers:
|
|
557
|
+
config["additional_headers"] = additional_headers
|
|
558
|
+
|
|
538
559
|
return config
|
|
539
560
|
|
|
540
561
|
|
|
@@ -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]:
|
|
@@ -987,7 +1030,9 @@ class LocalExecutor:
|
|
|
987
1030
|
|
|
988
1031
|
# Substitute variables from params
|
|
989
1032
|
if "variables" in graphql_config and graphql_config["variables"]:
|
|
990
|
-
|
|
1033
|
+
variables = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
|
|
1034
|
+
# Filter out None values (optional fields not provided) - matches REST _extract_body() behavior
|
|
1035
|
+
body["variables"] = {k: v for k, v in variables.items() if v is not None}
|
|
991
1036
|
|
|
992
1037
|
# Add operation name if specified
|
|
993
1038
|
if "operationName" in graphql_config:
|
|
@@ -1482,6 +1527,9 @@ class _StandardOperationHandler:
|
|
|
1482
1527
|
# Determine request format (json/data parameters)
|
|
1483
1528
|
request_kwargs = self.ctx.determine_request_format(endpoint, body)
|
|
1484
1529
|
|
|
1530
|
+
# Extract header parameters from OpenAPI operation (pass body to add Content-Type)
|
|
1531
|
+
header_params = self.ctx.extract_header_params(endpoint, params, body)
|
|
1532
|
+
|
|
1485
1533
|
# Execute async HTTP request
|
|
1486
1534
|
response_data, response_headers = await self.ctx.http_client.request(
|
|
1487
1535
|
method=endpoint.method,
|
|
@@ -1489,6 +1537,7 @@ class _StandardOperationHandler:
|
|
|
1489
1537
|
params=query_params if query_params else None,
|
|
1490
1538
|
json=request_kwargs.get("json"),
|
|
1491
1539
|
data=request_kwargs.get("data"),
|
|
1540
|
+
headers=header_params if header_params else None,
|
|
1492
1541
|
)
|
|
1493
1542
|
|
|
1494
1543
|
# Extract metadata from original response (before record extraction)
|
|
@@ -478,6 +478,7 @@ class HTTPClient:
|
|
|
478
478
|
request_id=request_id,
|
|
479
479
|
status_code=status_code,
|
|
480
480
|
response_body=f"<binary content, {response.headers.get('content-length', 'unknown')} bytes>",
|
|
481
|
+
response_headers=dict(response.headers),
|
|
481
482
|
)
|
|
482
483
|
return response, dict(response.headers)
|
|
483
484
|
|
|
@@ -504,6 +505,7 @@ class HTTPClient:
|
|
|
504
505
|
request_id=request_id,
|
|
505
506
|
status_code=status_code,
|
|
506
507
|
response_body=response_data,
|
|
508
|
+
response_headers=dict(response.headers),
|
|
507
509
|
)
|
|
508
510
|
return response_data, dict(response.headers)
|
|
509
511
|
|
|
@@ -134,6 +134,7 @@ class RequestLogger:
|
|
|
134
134
|
request_id: str,
|
|
135
135
|
status_code: int,
|
|
136
136
|
response_body: Any | None = None,
|
|
137
|
+
response_headers: Dict[str, str] | None = None,
|
|
137
138
|
) -> None:
|
|
138
139
|
"""
|
|
139
140
|
Log a successful HTTP response.
|
|
@@ -142,6 +143,7 @@ class RequestLogger:
|
|
|
142
143
|
request_id: ID returned from log_request
|
|
143
144
|
status_code: HTTP status code
|
|
144
145
|
response_body: Response body
|
|
146
|
+
response_headers: Response headers
|
|
145
147
|
"""
|
|
146
148
|
if request_id not in self._active_requests:
|
|
147
149
|
return
|
|
@@ -166,6 +168,7 @@ class RequestLogger:
|
|
|
166
168
|
body=request_data["body"],
|
|
167
169
|
response_status=status_code,
|
|
168
170
|
response_body=serializable_body,
|
|
171
|
+
response_headers=response_headers or {},
|
|
169
172
|
timing_ms=timing_ms,
|
|
170
173
|
)
|
|
171
174
|
|
|
@@ -243,7 +246,13 @@ class NullLogger:
|
|
|
243
246
|
"""No-op log_request."""
|
|
244
247
|
return ""
|
|
245
248
|
|
|
246
|
-
def log_response(
|
|
249
|
+
def log_response(
|
|
250
|
+
self,
|
|
251
|
+
request_id: str,
|
|
252
|
+
status_code: int,
|
|
253
|
+
response_body: Any | None = None,
|
|
254
|
+
response_headers: Dict[str, str] | None = None,
|
|
255
|
+
) -> None:
|
|
247
256
|
"""No-op log_response."""
|
|
248
257
|
pass
|
|
249
258
|
|
|
@@ -7,7 +7,7 @@ References:
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from enum import StrEnum
|
|
10
|
-
from typing import Dict
|
|
10
|
+
from typing import Any, Dict
|
|
11
11
|
from uuid import UUID
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
@@ -152,7 +152,12 @@ class Server(BaseModel):
|
|
|
152
152
|
url: str
|
|
153
153
|
description: str | None = None
|
|
154
154
|
variables: Dict[str, ServerVariable] = Field(default_factory=dict)
|
|
155
|
-
|
|
155
|
+
x_airbyte_replication_environment_mapping: Dict[str, str] | None = Field(default=None, alias="x-airbyte-replication-environment-mapping")
|
|
156
|
+
x_airbyte_replication_environment_constants: Dict[str, Any] | None = Field(
|
|
157
|
+
default=None,
|
|
158
|
+
alias="x-airbyte-replication-environment-constants",
|
|
159
|
+
description="Constant values to always inject at environment config paths (e.g., 'region': 'us-east-1')",
|
|
160
|
+
)
|
|
156
161
|
|
|
157
162
|
@field_validator("url")
|
|
158
163
|
@classmethod
|
|
@@ -109,6 +109,20 @@ class AirbyteAuthConfig(BaseModel):
|
|
|
109
109
|
description="Mapping from source config paths (e.g., 'credentials.api_key') to auth config keys for direct connectors",
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
+
# Additional headers to inject alongside OAuth2 Bearer token
|
|
113
|
+
additional_headers: Dict[str, str] | None = Field(
|
|
114
|
+
None,
|
|
115
|
+
description=(
|
|
116
|
+
"Extra headers to inject with auth. Values support Jinja2 {{ variable }} template syntax "
|
|
117
|
+
"to reference secrets. Example: {'Amazon-Advertising-API-ClientId': '{{ client_id }}'}"
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Replication connector auth constants
|
|
122
|
+
replication_auth_key_constants: Dict[str, Any] | None = Field(
|
|
123
|
+
None,
|
|
124
|
+
description="Constant values to always inject at source config paths (e.g., 'credentials.auth_type': 'OAuth2.0')",
|
|
125
|
+
)
|
|
112
126
|
# Multiple options (oneOf)
|
|
113
127
|
one_of: List[AuthConfigOption] | None = Field(None, alias="oneOf")
|
|
114
128
|
|
|
@@ -180,6 +180,15 @@ class EndpointDefinition(BaseModel):
|
|
|
180
180
|
default_factory=dict,
|
|
181
181
|
description="Schema for path params including defaults: {name: {type, default, required}}",
|
|
182
182
|
)
|
|
183
|
+
header_params: list[str] = Field(default_factory=list) # Header parameters from OpenAPI
|
|
184
|
+
header_params_schema: dict[str, dict[str, Any]] = Field(
|
|
185
|
+
default_factory=dict,
|
|
186
|
+
description="Schema for header params including defaults: {name: {type, default, required}}",
|
|
187
|
+
)
|
|
188
|
+
request_body_defaults: dict[str, Any] = Field(
|
|
189
|
+
default_factory=dict,
|
|
190
|
+
description="Default values for request body fields from OpenAPI schema",
|
|
191
|
+
)
|
|
183
192
|
content_type: ContentType = ContentType.JSON
|
|
184
193
|
request_schema: dict[str, Any] | None = None
|
|
185
194
|
response_schema: dict[str, Any] | None = None
|
|
@@ -486,30 +486,36 @@ def validate_meta_extractor_fields(
|
|
|
486
486
|
response_body = spec.captured_response.body
|
|
487
487
|
|
|
488
488
|
# Validate each meta extractor field
|
|
489
|
-
for field_name,
|
|
489
|
+
for field_name, extractor_expr in endpoint.meta_extractor.items():
|
|
490
|
+
# Skip header-based extractors - they extract from headers, not response body
|
|
491
|
+
# @link.next extracts from RFC 5988 Link header
|
|
492
|
+
# @header.X-Name extracts raw header value
|
|
493
|
+
if extractor_expr.startswith("@link.") or extractor_expr.startswith("@header."):
|
|
494
|
+
continue
|
|
495
|
+
|
|
490
496
|
# Check 1: Does the JSONPath find data in the actual response?
|
|
491
497
|
try:
|
|
492
|
-
parsed_expr = parse_jsonpath(
|
|
498
|
+
parsed_expr = parse_jsonpath(extractor_expr)
|
|
493
499
|
matches = [match.value for match in parsed_expr.find(response_body)]
|
|
494
500
|
|
|
495
501
|
if not matches:
|
|
496
502
|
warnings.append(
|
|
497
503
|
f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' "
|
|
498
|
-
f"with JSONPath '{
|
|
504
|
+
f"with JSONPath '{extractor_expr}' found no matches in cassette response"
|
|
499
505
|
)
|
|
500
506
|
except Exception as e:
|
|
501
507
|
warnings.append(
|
|
502
|
-
f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' has invalid JSONPath '{
|
|
508
|
+
f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' has invalid JSONPath '{extractor_expr}': {str(e)}"
|
|
503
509
|
)
|
|
504
510
|
|
|
505
511
|
# Check 2: Is this field path declared in the response schema?
|
|
506
512
|
if endpoint.response_schema:
|
|
507
|
-
field_in_schema = _check_field_in_schema(
|
|
513
|
+
field_in_schema = _check_field_in_schema(extractor_expr, endpoint.response_schema)
|
|
508
514
|
|
|
509
515
|
if not field_in_schema:
|
|
510
516
|
warnings.append(
|
|
511
517
|
f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' "
|
|
512
|
-
f"extracts from '{
|
|
518
|
+
f"extracts from '{extractor_expr}' but this path is not declared in response schema"
|
|
513
519
|
)
|
|
514
520
|
|
|
515
521
|
except Exception as e:
|