airbyte-agent-zendesk-support 0.18.47__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 +130 -2
- 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 +152 -22
- airbyte_agent_zendesk_support/_vendored/connector_sdk/http_client.py +13 -6
- 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 +920 -1
- airbyte_agent_zendesk_support/connector_model.py +1 -1
- airbyte_agent_zendesk_support/models.py +606 -0
- airbyte_agent_zendesk_support/types.py +3715 -0
- {airbyte_agent_zendesk_support-0.18.47.dist-info → airbyte_agent_zendesk_support-0.18.57.dist-info}/METADATA +35 -22
- {airbyte_agent_zendesk_support-0.18.47.dist-info → airbyte_agent_zendesk_support-0.18.57.dist-info}/RECORD +18 -18
- {airbyte_agent_zendesk_support-0.18.47.dist-info → airbyte_agent_zendesk_support-0.18.57.dist-info}/WHEEL +0 -0
|
@@ -72,7 +72,33 @@ from .models import (
|
|
|
72
72
|
SlaPoliciesListResult,
|
|
73
73
|
TicketFormsListResult,
|
|
74
74
|
ArticlesListResult,
|
|
75
|
-
ArticleAttachmentsListResult
|
|
75
|
+
ArticleAttachmentsListResult,
|
|
76
|
+
AirbyteSearchHit,
|
|
77
|
+
AirbyteSearchResult,
|
|
78
|
+
BrandsSearchData,
|
|
79
|
+
BrandsSearchResult,
|
|
80
|
+
GroupsSearchData,
|
|
81
|
+
GroupsSearchResult,
|
|
82
|
+
OrganizationsSearchData,
|
|
83
|
+
OrganizationsSearchResult,
|
|
84
|
+
SatisfactionRatingsSearchData,
|
|
85
|
+
SatisfactionRatingsSearchResult,
|
|
86
|
+
TagsSearchData,
|
|
87
|
+
TagsSearchResult,
|
|
88
|
+
TicketAuditsSearchData,
|
|
89
|
+
TicketAuditsSearchResult,
|
|
90
|
+
TicketCommentsSearchData,
|
|
91
|
+
TicketCommentsSearchResult,
|
|
92
|
+
TicketFieldsSearchData,
|
|
93
|
+
TicketFieldsSearchResult,
|
|
94
|
+
TicketFormsSearchData,
|
|
95
|
+
TicketFormsSearchResult,
|
|
96
|
+
TicketMetricsSearchData,
|
|
97
|
+
TicketMetricsSearchResult,
|
|
98
|
+
TicketsSearchData,
|
|
99
|
+
TicketsSearchResult,
|
|
100
|
+
UsersSearchData,
|
|
101
|
+
UsersSearchResult
|
|
76
102
|
)
|
|
77
103
|
from .types import (
|
|
78
104
|
TicketsListParams,
|
|
@@ -114,7 +140,45 @@ from .types import (
|
|
|
114
140
|
ArticlesGetParams,
|
|
115
141
|
ArticleAttachmentsListParams,
|
|
116
142
|
ArticleAttachmentsGetParams,
|
|
117
|
-
ArticleAttachmentsDownloadParams
|
|
143
|
+
ArticleAttachmentsDownloadParams,
|
|
144
|
+
AirbyteSearchParams,
|
|
145
|
+
AirbyteSortOrder,
|
|
146
|
+
BrandsSearchFilter,
|
|
147
|
+
BrandsSearchQuery,
|
|
148
|
+
BrandsCondition,
|
|
149
|
+
GroupsSearchFilter,
|
|
150
|
+
GroupsSearchQuery,
|
|
151
|
+
GroupsCondition,
|
|
152
|
+
OrganizationsSearchFilter,
|
|
153
|
+
OrganizationsSearchQuery,
|
|
154
|
+
OrganizationsCondition,
|
|
155
|
+
SatisfactionRatingsSearchFilter,
|
|
156
|
+
SatisfactionRatingsSearchQuery,
|
|
157
|
+
SatisfactionRatingsCondition,
|
|
158
|
+
TagsSearchFilter,
|
|
159
|
+
TagsSearchQuery,
|
|
160
|
+
TagsCondition,
|
|
161
|
+
TicketAuditsSearchFilter,
|
|
162
|
+
TicketAuditsSearchQuery,
|
|
163
|
+
TicketAuditsCondition,
|
|
164
|
+
TicketCommentsSearchFilter,
|
|
165
|
+
TicketCommentsSearchQuery,
|
|
166
|
+
TicketCommentsCondition,
|
|
167
|
+
TicketFieldsSearchFilter,
|
|
168
|
+
TicketFieldsSearchQuery,
|
|
169
|
+
TicketFieldsCondition,
|
|
170
|
+
TicketFormsSearchFilter,
|
|
171
|
+
TicketFormsSearchQuery,
|
|
172
|
+
TicketFormsCondition,
|
|
173
|
+
TicketMetricsSearchFilter,
|
|
174
|
+
TicketMetricsSearchQuery,
|
|
175
|
+
TicketMetricsCondition,
|
|
176
|
+
TicketsSearchFilter,
|
|
177
|
+
TicketsSearchQuery,
|
|
178
|
+
TicketsCondition,
|
|
179
|
+
UsersSearchFilter,
|
|
180
|
+
UsersSearchQuery,
|
|
181
|
+
UsersCondition
|
|
118
182
|
)
|
|
119
183
|
|
|
120
184
|
__all__ = [
|
|
@@ -186,6 +250,32 @@ __all__ = [
|
|
|
186
250
|
"TicketFormsListResult",
|
|
187
251
|
"ArticlesListResult",
|
|
188
252
|
"ArticleAttachmentsListResult",
|
|
253
|
+
"AirbyteSearchHit",
|
|
254
|
+
"AirbyteSearchResult",
|
|
255
|
+
"BrandsSearchData",
|
|
256
|
+
"BrandsSearchResult",
|
|
257
|
+
"GroupsSearchData",
|
|
258
|
+
"GroupsSearchResult",
|
|
259
|
+
"OrganizationsSearchData",
|
|
260
|
+
"OrganizationsSearchResult",
|
|
261
|
+
"SatisfactionRatingsSearchData",
|
|
262
|
+
"SatisfactionRatingsSearchResult",
|
|
263
|
+
"TagsSearchData",
|
|
264
|
+
"TagsSearchResult",
|
|
265
|
+
"TicketAuditsSearchData",
|
|
266
|
+
"TicketAuditsSearchResult",
|
|
267
|
+
"TicketCommentsSearchData",
|
|
268
|
+
"TicketCommentsSearchResult",
|
|
269
|
+
"TicketFieldsSearchData",
|
|
270
|
+
"TicketFieldsSearchResult",
|
|
271
|
+
"TicketFormsSearchData",
|
|
272
|
+
"TicketFormsSearchResult",
|
|
273
|
+
"TicketMetricsSearchData",
|
|
274
|
+
"TicketMetricsSearchResult",
|
|
275
|
+
"TicketsSearchData",
|
|
276
|
+
"TicketsSearchResult",
|
|
277
|
+
"UsersSearchData",
|
|
278
|
+
"UsersSearchResult",
|
|
189
279
|
"TicketsListParams",
|
|
190
280
|
"TicketsGetParams",
|
|
191
281
|
"UsersListParams",
|
|
@@ -226,4 +316,42 @@ __all__ = [
|
|
|
226
316
|
"ArticleAttachmentsListParams",
|
|
227
317
|
"ArticleAttachmentsGetParams",
|
|
228
318
|
"ArticleAttachmentsDownloadParams",
|
|
319
|
+
"AirbyteSearchParams",
|
|
320
|
+
"AirbyteSortOrder",
|
|
321
|
+
"BrandsSearchFilter",
|
|
322
|
+
"BrandsSearchQuery",
|
|
323
|
+
"BrandsCondition",
|
|
324
|
+
"GroupsSearchFilter",
|
|
325
|
+
"GroupsSearchQuery",
|
|
326
|
+
"GroupsCondition",
|
|
327
|
+
"OrganizationsSearchFilter",
|
|
328
|
+
"OrganizationsSearchQuery",
|
|
329
|
+
"OrganizationsCondition",
|
|
330
|
+
"SatisfactionRatingsSearchFilter",
|
|
331
|
+
"SatisfactionRatingsSearchQuery",
|
|
332
|
+
"SatisfactionRatingsCondition",
|
|
333
|
+
"TagsSearchFilter",
|
|
334
|
+
"TagsSearchQuery",
|
|
335
|
+
"TagsCondition",
|
|
336
|
+
"TicketAuditsSearchFilter",
|
|
337
|
+
"TicketAuditsSearchQuery",
|
|
338
|
+
"TicketAuditsCondition",
|
|
339
|
+
"TicketCommentsSearchFilter",
|
|
340
|
+
"TicketCommentsSearchQuery",
|
|
341
|
+
"TicketCommentsCondition",
|
|
342
|
+
"TicketFieldsSearchFilter",
|
|
343
|
+
"TicketFieldsSearchQuery",
|
|
344
|
+
"TicketFieldsCondition",
|
|
345
|
+
"TicketFormsSearchFilter",
|
|
346
|
+
"TicketFormsSearchQuery",
|
|
347
|
+
"TicketFormsCondition",
|
|
348
|
+
"TicketMetricsSearchFilter",
|
|
349
|
+
"TicketMetricsSearchQuery",
|
|
350
|
+
"TicketMetricsCondition",
|
|
351
|
+
"TicketsSearchFilter",
|
|
352
|
+
"TicketsSearchQuery",
|
|
353
|
+
"TicketsCondition",
|
|
354
|
+
"UsersSearchFilter",
|
|
355
|
+
"UsersSearchQuery",
|
|
356
|
+
"UsersCondition",
|
|
229
357
|
]
|
|
@@ -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:
|
|
@@ -1222,15 +1267,22 @@ class LocalExecutor:
|
|
|
1222
1267
|
def _extract_metadata(
|
|
1223
1268
|
self,
|
|
1224
1269
|
response_data: dict[str, Any],
|
|
1270
|
+
response_headers: dict[str, str],
|
|
1225
1271
|
endpoint: EndpointDefinition,
|
|
1226
1272
|
) -> dict[str, Any] | None:
|
|
1227
1273
|
"""Extract metadata from response using meta extractor.
|
|
1228
1274
|
|
|
1229
|
-
Each field in meta_extractor dict is independently extracted using JSONPath
|
|
1275
|
+
Each field in meta_extractor dict is independently extracted using JSONPath
|
|
1276
|
+
for body extraction, or special prefixes for header extraction:
|
|
1277
|
+
- @link.{rel}: Extract URL from RFC 5988 Link header by rel type
|
|
1278
|
+
- @header.{name}: Extract raw header value by header name
|
|
1279
|
+
- Otherwise: JSONPath expression for body extraction
|
|
1280
|
+
|
|
1230
1281
|
Missing or invalid paths result in None for that field (no crash).
|
|
1231
1282
|
|
|
1232
1283
|
Args:
|
|
1233
1284
|
response_data: Full API response (before record extraction)
|
|
1285
|
+
response_headers: HTTP response headers
|
|
1234
1286
|
endpoint: Endpoint with optional meta extractor configuration
|
|
1235
1287
|
|
|
1236
1288
|
Returns:
|
|
@@ -1241,11 +1293,15 @@ class LocalExecutor:
|
|
|
1241
1293
|
Example:
|
|
1242
1294
|
meta_extractor = {
|
|
1243
1295
|
"pagination": "$.records",
|
|
1244
|
-
"request_id": "$.requestId"
|
|
1296
|
+
"request_id": "$.requestId",
|
|
1297
|
+
"next_page_url": "@link.next",
|
|
1298
|
+
"rate_limit": "@header.X-RateLimit-Remaining"
|
|
1245
1299
|
}
|
|
1246
1300
|
Returns: {
|
|
1247
1301
|
"pagination": {"cursor": "abc", "total": 100},
|
|
1248
|
-
"request_id": "xyz123"
|
|
1302
|
+
"request_id": "xyz123",
|
|
1303
|
+
"next_page_url": "https://api.example.com/data?cursor=abc",
|
|
1304
|
+
"rate_limit": "99"
|
|
1249
1305
|
}
|
|
1250
1306
|
"""
|
|
1251
1307
|
# Check if endpoint has meta extractor
|
|
@@ -1255,26 +1311,96 @@ class LocalExecutor:
|
|
|
1255
1311
|
extracted_meta: dict[str, Any] = {}
|
|
1256
1312
|
|
|
1257
1313
|
# Extract each field independently
|
|
1258
|
-
for field_name,
|
|
1314
|
+
for field_name, extractor_expr in endpoint.meta_extractor.items():
|
|
1259
1315
|
try:
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
#
|
|
1266
|
-
|
|
1316
|
+
if extractor_expr.startswith("@link."):
|
|
1317
|
+
# RFC 5988 Link header extraction
|
|
1318
|
+
rel = extractor_expr[6:]
|
|
1319
|
+
extracted_meta[field_name] = self._extract_link_url(response_headers, rel)
|
|
1320
|
+
elif extractor_expr.startswith("@header."):
|
|
1321
|
+
# Raw header value extraction (case-insensitive lookup)
|
|
1322
|
+
header_name = extractor_expr[8:]
|
|
1323
|
+
extracted_meta[field_name] = self._get_header_value(response_headers, header_name)
|
|
1267
1324
|
else:
|
|
1268
|
-
#
|
|
1269
|
-
|
|
1325
|
+
# JSONPath body extraction
|
|
1326
|
+
jsonpath_expr = parse_jsonpath(extractor_expr)
|
|
1327
|
+
matches = [match.value for match in jsonpath_expr.find(response_data)]
|
|
1328
|
+
|
|
1329
|
+
if matches:
|
|
1330
|
+
# Return first match (most common case)
|
|
1331
|
+
extracted_meta[field_name] = matches[0]
|
|
1332
|
+
else:
|
|
1333
|
+
# Path not found - set to None
|
|
1334
|
+
extracted_meta[field_name] = None
|
|
1270
1335
|
|
|
1271
1336
|
except Exception as e:
|
|
1272
1337
|
# Log error but continue with other fields
|
|
1273
|
-
logging.warning(f"Failed to apply meta extractor for field '{field_name}' with
|
|
1338
|
+
logging.warning(f"Failed to apply meta extractor for field '{field_name}' with expression '{extractor_expr}': {e}. Setting to None.")
|
|
1274
1339
|
extracted_meta[field_name] = None
|
|
1275
1340
|
|
|
1276
1341
|
return extracted_meta
|
|
1277
1342
|
|
|
1343
|
+
@staticmethod
|
|
1344
|
+
def _extract_link_url(headers: dict[str, str], rel: str) -> str | None:
|
|
1345
|
+
"""Extract URL from RFC 5988 Link header by rel type.
|
|
1346
|
+
|
|
1347
|
+
Parses Link header format: <url>; param1="value1"; rel="next"; param2="value2"
|
|
1348
|
+
|
|
1349
|
+
Supports:
|
|
1350
|
+
- Multiple parameters per link in any order
|
|
1351
|
+
- Both quoted and unquoted rel values
|
|
1352
|
+
- Multiple links separated by commas
|
|
1353
|
+
|
|
1354
|
+
Args:
|
|
1355
|
+
headers: Response headers dict
|
|
1356
|
+
rel: The rel type to extract (e.g., "next", "prev", "first", "last")
|
|
1357
|
+
|
|
1358
|
+
Returns:
|
|
1359
|
+
The URL for the specified rel type, or None if not found
|
|
1360
|
+
"""
|
|
1361
|
+
link_header = headers.get("Link") or headers.get("link", "")
|
|
1362
|
+
if not link_header:
|
|
1363
|
+
return None
|
|
1364
|
+
|
|
1365
|
+
for link_segment in re.split(r",(?=\s*<)", link_header):
|
|
1366
|
+
link_segment = link_segment.strip()
|
|
1367
|
+
|
|
1368
|
+
url_match = re.match(r"<([^>]+)>", link_segment)
|
|
1369
|
+
if not url_match:
|
|
1370
|
+
continue
|
|
1371
|
+
|
|
1372
|
+
url = url_match.group(1)
|
|
1373
|
+
params_str = link_segment[url_match.end() :]
|
|
1374
|
+
|
|
1375
|
+
rel_match = re.search(r';\s*rel="?([^";,]+)"?', params_str, re.IGNORECASE)
|
|
1376
|
+
if rel_match and rel_match.group(1).strip() == rel:
|
|
1377
|
+
return url
|
|
1378
|
+
|
|
1379
|
+
return None
|
|
1380
|
+
|
|
1381
|
+
@staticmethod
|
|
1382
|
+
def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
|
|
1383
|
+
"""Get header value with case-insensitive lookup.
|
|
1384
|
+
|
|
1385
|
+
Args:
|
|
1386
|
+
headers: Response headers dict
|
|
1387
|
+
header_name: Header name to look up
|
|
1388
|
+
|
|
1389
|
+
Returns:
|
|
1390
|
+
Header value or None if not found
|
|
1391
|
+
"""
|
|
1392
|
+
# Try exact match first
|
|
1393
|
+
if header_name in headers:
|
|
1394
|
+
return headers[header_name]
|
|
1395
|
+
|
|
1396
|
+
# Case-insensitive lookup
|
|
1397
|
+
header_name_lower = header_name.lower()
|
|
1398
|
+
for key, value in headers.items():
|
|
1399
|
+
if key.lower() == header_name_lower:
|
|
1400
|
+
return value
|
|
1401
|
+
|
|
1402
|
+
return None
|
|
1403
|
+
|
|
1278
1404
|
def _validate_required_body_fields(self, endpoint: Any, params: dict[str, Any], action: Action, entity: str) -> None:
|
|
1279
1405
|
"""Validate that required body fields are present for CREATE/UPDATE operations.
|
|
1280
1406
|
|
|
@@ -1401,20 +1527,24 @@ class _StandardOperationHandler:
|
|
|
1401
1527
|
# Determine request format (json/data parameters)
|
|
1402
1528
|
request_kwargs = self.ctx.determine_request_format(endpoint, body)
|
|
1403
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
|
+
|
|
1404
1533
|
# Execute async HTTP request
|
|
1405
|
-
|
|
1534
|
+
response_data, response_headers = await self.ctx.http_client.request(
|
|
1406
1535
|
method=endpoint.method,
|
|
1407
1536
|
path=path,
|
|
1408
1537
|
params=query_params if query_params else None,
|
|
1409
1538
|
json=request_kwargs.get("json"),
|
|
1410
1539
|
data=request_kwargs.get("data"),
|
|
1540
|
+
headers=header_params if header_params else None,
|
|
1411
1541
|
)
|
|
1412
1542
|
|
|
1413
1543
|
# Extract metadata from original response (before record extraction)
|
|
1414
|
-
metadata = self.ctx.executor._extract_metadata(
|
|
1544
|
+
metadata = self.ctx.executor._extract_metadata(response_data, response_headers, endpoint)
|
|
1415
1545
|
|
|
1416
1546
|
# Extract records if extractor configured
|
|
1417
|
-
response = self.ctx.extract_records(
|
|
1547
|
+
response = self.ctx.extract_records(response_data, endpoint)
|
|
1418
1548
|
|
|
1419
1549
|
# Assume success with 200 status code if no exception raised
|
|
1420
1550
|
status_code = 200
|
|
@@ -1540,7 +1670,7 @@ class _DownloadOperationHandler:
|
|
|
1540
1670
|
request_format = self.ctx.determine_request_format(operation, request_body)
|
|
1541
1671
|
self.ctx.validate_required_body_fields(operation, params, action, entity)
|
|
1542
1672
|
|
|
1543
|
-
metadata_response = await self.ctx.http_client.request(
|
|
1673
|
+
metadata_response, _ = await self.ctx.http_client.request(
|
|
1544
1674
|
method=operation.method,
|
|
1545
1675
|
path=path,
|
|
1546
1676
|
params=query_params,
|
|
@@ -1555,7 +1685,7 @@ class _DownloadOperationHandler:
|
|
|
1555
1685
|
)
|
|
1556
1686
|
|
|
1557
1687
|
# Step 3: Stream file from extracted URL
|
|
1558
|
-
file_response = await self.ctx.http_client.request(
|
|
1688
|
+
file_response, _ = await self.ctx.http_client.request(
|
|
1559
1689
|
method="GET",
|
|
1560
1690
|
path=file_url,
|
|
1561
1691
|
headers=headers,
|
|
@@ -1563,7 +1693,7 @@ class _DownloadOperationHandler:
|
|
|
1563
1693
|
)
|
|
1564
1694
|
else:
|
|
1565
1695
|
# One-step direct download: stream file directly from endpoint
|
|
1566
|
-
file_response = await self.ctx.http_client.request(
|
|
1696
|
+
file_response, _ = await self.ctx.http_client.request(
|
|
1567
1697
|
method=operation.method,
|
|
1568
1698
|
path=path,
|
|
1569
1699
|
params=query_params,
|