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.
@@ -73,8 +73,8 @@ from .models import (
73
73
  TicketFormsListResult,
74
74
  ArticlesListResult,
75
75
  ArticleAttachmentsListResult,
76
- SearchHit,
77
- SearchResult,
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
- SearchParams,
145
- SortOrder,
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
- "SearchHit",
254
- "SearchResult",
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
- "SearchParams",
320
- "SortOrder",
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 into headers.
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 authentication injected
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 into headers
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
- return self._extract_body(endpoint.body_fields, params)
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
- body["variables"] = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
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(self, *args, **kwargs) -> None:
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
 
@@ -31,6 +31,7 @@ class RequestLog(BaseModel):
31
31
  body: Any | None = None
32
32
  response_status: int | None = None
33
33
  response_body: Any | None = None
34
+ response_headers: Dict[str, str] = Field(default_factory=dict)
34
35
  timing_ms: float | None = None
35
36
  error: str | None = None
36
37
 
@@ -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
- x_airbyte_replication_user_config_mapping: Dict[str, str] | None = Field(default=None, alias="x-airbyte-replication-user-config-mapping")
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, jsonpath_expr in endpoint.meta_extractor.items():
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(jsonpath_expr)
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 '{jsonpath_expr}' found no matches in cassette response"
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 '{jsonpath_expr}': {str(e)}"
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(jsonpath_expr, endpoint.response_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 '{jsonpath_expr}' but this path is not declared in response schema"
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: