airbyte-agent-zendesk-support 0.18.39__py3-none-any.whl → 0.18.51__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 +239 -18
- airbyte_agent_zendesk_support/_vendored/connector_sdk/connector_model_loader.py +3 -2
- airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/local_executor.py +181 -30
- 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 +4 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/connector.py +22 -33
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/extensions.py +122 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/validation.py +12 -6
- airbyte_agent_zendesk_support/connector.py +1073 -157
- airbyte_agent_zendesk_support/connector_model.py +3 -3
- airbyte_agent_zendesk_support/models.py +628 -69
- airbyte_agent_zendesk_support/types.py +3716 -0
- {airbyte_agent_zendesk_support-0.18.39.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/METADATA +12 -9
- {airbyte_agent_zendesk_support-0.18.39.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/RECORD +17 -17
- {airbyte_agent_zendesk_support-0.18.39.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/WHEEL +0 -0
|
@@ -53,42 +53,52 @@ from .models import (
|
|
|
53
53
|
ZendeskSupportExecuteResult,
|
|
54
54
|
ZendeskSupportExecuteResultWithMeta,
|
|
55
55
|
TicketsListResult,
|
|
56
|
-
TicketsGetResult,
|
|
57
56
|
UsersListResult,
|
|
58
|
-
UsersGetResult,
|
|
59
57
|
OrganizationsListResult,
|
|
60
|
-
OrganizationsGetResult,
|
|
61
58
|
GroupsListResult,
|
|
62
|
-
GroupsGetResult,
|
|
63
59
|
TicketCommentsListResult,
|
|
64
|
-
AttachmentsGetResult,
|
|
65
60
|
TicketAuditsListResult,
|
|
66
61
|
TicketMetricsListResult,
|
|
67
62
|
TicketFieldsListResult,
|
|
68
|
-
TicketFieldsGetResult,
|
|
69
63
|
BrandsListResult,
|
|
70
|
-
BrandsGetResult,
|
|
71
64
|
ViewsListResult,
|
|
72
|
-
ViewsGetResult,
|
|
73
65
|
MacrosListResult,
|
|
74
|
-
MacrosGetResult,
|
|
75
66
|
TriggersListResult,
|
|
76
|
-
TriggersGetResult,
|
|
77
67
|
AutomationsListResult,
|
|
78
|
-
AutomationsGetResult,
|
|
79
68
|
TagsListResult,
|
|
80
69
|
SatisfactionRatingsListResult,
|
|
81
|
-
SatisfactionRatingsGetResult,
|
|
82
70
|
GroupMembershipsListResult,
|
|
83
71
|
OrganizationMembershipsListResult,
|
|
84
72
|
SlaPoliciesListResult,
|
|
85
|
-
SlaPoliciesGetResult,
|
|
86
73
|
TicketFormsListResult,
|
|
87
|
-
TicketFormsGetResult,
|
|
88
74
|
ArticlesListResult,
|
|
89
|
-
ArticlesGetResult,
|
|
90
75
|
ArticleAttachmentsListResult,
|
|
91
|
-
|
|
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
|
|
92
102
|
)
|
|
93
103
|
from .types import (
|
|
94
104
|
TicketsListParams,
|
|
@@ -130,7 +140,218 @@ from .types import (
|
|
|
130
140
|
ArticlesGetParams,
|
|
131
141
|
ArticleAttachmentsListParams,
|
|
132
142
|
ArticleAttachmentsGetParams,
|
|
133
|
-
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
|
|
134
182
|
)
|
|
135
183
|
|
|
136
|
-
__all__ = [
|
|
184
|
+
__all__ = [
|
|
185
|
+
"ZendeskSupportConnector",
|
|
186
|
+
"ZendeskSupportAuthConfig",
|
|
187
|
+
"Ticket",
|
|
188
|
+
"User",
|
|
189
|
+
"Organization",
|
|
190
|
+
"Group",
|
|
191
|
+
"TicketComment",
|
|
192
|
+
"Attachment",
|
|
193
|
+
"TicketAudit",
|
|
194
|
+
"TicketMetric",
|
|
195
|
+
"TicketField",
|
|
196
|
+
"Brand",
|
|
197
|
+
"View",
|
|
198
|
+
"Macro",
|
|
199
|
+
"Trigger",
|
|
200
|
+
"Automation",
|
|
201
|
+
"Tag",
|
|
202
|
+
"SatisfactionRating",
|
|
203
|
+
"GroupMembership",
|
|
204
|
+
"OrganizationMembership",
|
|
205
|
+
"SLAPolicy",
|
|
206
|
+
"TicketForm",
|
|
207
|
+
"Article",
|
|
208
|
+
"ArticleAttachment",
|
|
209
|
+
"TicketsListResultMeta",
|
|
210
|
+
"UsersListResultMeta",
|
|
211
|
+
"OrganizationsListResultMeta",
|
|
212
|
+
"GroupsListResultMeta",
|
|
213
|
+
"TicketCommentsListResultMeta",
|
|
214
|
+
"TicketAuditsListResultMeta",
|
|
215
|
+
"TicketMetricsListResultMeta",
|
|
216
|
+
"TicketFieldsListResultMeta",
|
|
217
|
+
"BrandsListResultMeta",
|
|
218
|
+
"ViewsListResultMeta",
|
|
219
|
+
"MacrosListResultMeta",
|
|
220
|
+
"TriggersListResultMeta",
|
|
221
|
+
"AutomationsListResultMeta",
|
|
222
|
+
"TagsListResultMeta",
|
|
223
|
+
"SatisfactionRatingsListResultMeta",
|
|
224
|
+
"GroupMembershipsListResultMeta",
|
|
225
|
+
"OrganizationMembershipsListResultMeta",
|
|
226
|
+
"SlaPoliciesListResultMeta",
|
|
227
|
+
"TicketFormsListResultMeta",
|
|
228
|
+
"ArticlesListResultMeta",
|
|
229
|
+
"ArticleAttachmentsListResultMeta",
|
|
230
|
+
"ZendeskSupportExecuteResult",
|
|
231
|
+
"ZendeskSupportExecuteResultWithMeta",
|
|
232
|
+
"TicketsListResult",
|
|
233
|
+
"UsersListResult",
|
|
234
|
+
"OrganizationsListResult",
|
|
235
|
+
"GroupsListResult",
|
|
236
|
+
"TicketCommentsListResult",
|
|
237
|
+
"TicketAuditsListResult",
|
|
238
|
+
"TicketMetricsListResult",
|
|
239
|
+
"TicketFieldsListResult",
|
|
240
|
+
"BrandsListResult",
|
|
241
|
+
"ViewsListResult",
|
|
242
|
+
"MacrosListResult",
|
|
243
|
+
"TriggersListResult",
|
|
244
|
+
"AutomationsListResult",
|
|
245
|
+
"TagsListResult",
|
|
246
|
+
"SatisfactionRatingsListResult",
|
|
247
|
+
"GroupMembershipsListResult",
|
|
248
|
+
"OrganizationMembershipsListResult",
|
|
249
|
+
"SlaPoliciesListResult",
|
|
250
|
+
"TicketFormsListResult",
|
|
251
|
+
"ArticlesListResult",
|
|
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",
|
|
279
|
+
"TicketsListParams",
|
|
280
|
+
"TicketsGetParams",
|
|
281
|
+
"UsersListParams",
|
|
282
|
+
"UsersGetParams",
|
|
283
|
+
"OrganizationsListParams",
|
|
284
|
+
"OrganizationsGetParams",
|
|
285
|
+
"GroupsListParams",
|
|
286
|
+
"GroupsGetParams",
|
|
287
|
+
"TicketCommentsListParams",
|
|
288
|
+
"AttachmentsGetParams",
|
|
289
|
+
"AttachmentsDownloadParams",
|
|
290
|
+
"TicketAuditsListParams",
|
|
291
|
+
"TicketAuditsListParams",
|
|
292
|
+
"TicketMetricsListParams",
|
|
293
|
+
"TicketFieldsListParams",
|
|
294
|
+
"TicketFieldsGetParams",
|
|
295
|
+
"BrandsListParams",
|
|
296
|
+
"BrandsGetParams",
|
|
297
|
+
"ViewsListParams",
|
|
298
|
+
"ViewsGetParams",
|
|
299
|
+
"MacrosListParams",
|
|
300
|
+
"MacrosGetParams",
|
|
301
|
+
"TriggersListParams",
|
|
302
|
+
"TriggersGetParams",
|
|
303
|
+
"AutomationsListParams",
|
|
304
|
+
"AutomationsGetParams",
|
|
305
|
+
"TagsListParams",
|
|
306
|
+
"SatisfactionRatingsListParams",
|
|
307
|
+
"SatisfactionRatingsGetParams",
|
|
308
|
+
"GroupMembershipsListParams",
|
|
309
|
+
"OrganizationMembershipsListParams",
|
|
310
|
+
"SlaPoliciesListParams",
|
|
311
|
+
"SlaPoliciesGetParams",
|
|
312
|
+
"TicketFormsListParams",
|
|
313
|
+
"TicketFormsGetParams",
|
|
314
|
+
"ArticlesListParams",
|
|
315
|
+
"ArticlesGetParams",
|
|
316
|
+
"ArticleAttachmentsListParams",
|
|
317
|
+
"ArticleAttachmentsGetParams",
|
|
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",
|
|
357
|
+
]
|
|
@@ -519,13 +519,14 @@ def _parse_oauth2_config(scheme: Any) -> dict[str, str]:
|
|
|
519
519
|
config["refresh_url"] = refresh_url
|
|
520
520
|
|
|
521
521
|
# Extract custom refresh configuration from x-airbyte-token-refresh extension
|
|
522
|
+
# Note: x_token_refresh is a Dict[str, Any], not a Pydantic model, so use .get()
|
|
522
523
|
x_token_refresh = getattr(scheme, "x_token_refresh", None)
|
|
523
524
|
if x_token_refresh:
|
|
524
|
-
auth_style =
|
|
525
|
+
auth_style = x_token_refresh.get("auth_style")
|
|
525
526
|
if auth_style:
|
|
526
527
|
config["auth_style"] = auth_style
|
|
527
528
|
|
|
528
|
-
body_format =
|
|
529
|
+
body_format = x_token_refresh.get("body_format")
|
|
529
530
|
if body_format:
|
|
530
531
|
config["body_format"] = body_format
|
|
531
532
|
|
|
@@ -495,6 +495,14 @@ class LocalExecutor:
|
|
|
495
495
|
print(result.data)
|
|
496
496
|
"""
|
|
497
497
|
try:
|
|
498
|
+
# Check for hosted-only actions before converting to Action enum
|
|
499
|
+
if config.action == "search":
|
|
500
|
+
raise NotImplementedError(
|
|
501
|
+
"search is only available in hosted execution mode. "
|
|
502
|
+
"Initialize the connector with external_user_id, airbyte_client_id, "
|
|
503
|
+
"and airbyte_client_secret to use this feature."
|
|
504
|
+
)
|
|
505
|
+
|
|
498
506
|
# Convert config to internal format
|
|
499
507
|
action = Action(config.action) if isinstance(config.action, str) else config.action
|
|
500
508
|
params = config.params or {}
|
|
@@ -979,7 +987,9 @@ class LocalExecutor:
|
|
|
979
987
|
|
|
980
988
|
# Substitute variables from params
|
|
981
989
|
if "variables" in graphql_config and graphql_config["variables"]:
|
|
982
|
-
|
|
990
|
+
variables = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
|
|
991
|
+
# Filter out None values (optional fields not provided) - matches REST _extract_body() behavior
|
|
992
|
+
body["variables"] = {k: v for k, v in variables.items() if v is not None}
|
|
983
993
|
|
|
984
994
|
# Add operation name if specified
|
|
985
995
|
if "operationName" in graphql_config:
|
|
@@ -1098,35 +1108,93 @@ class LocalExecutor:
|
|
|
1098
1108
|
|
|
1099
1109
|
return interpolate_value(variables)
|
|
1100
1110
|
|
|
1111
|
+
def _wrap_primitives(self, data: Any) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
1112
|
+
"""Wrap primitive values in dict format for consistent response structure.
|
|
1113
|
+
|
|
1114
|
+
Transforms primitive API responses into dict format so downstream code
|
|
1115
|
+
can always expect dict-based data structures.
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
data: Response data (could be primitive, list, dict, or None)
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
- If data is a primitive (str, int, float, bool): {"value": data}
|
|
1122
|
+
- If data is a list: wraps all non-dict elements as {"value": item}
|
|
1123
|
+
- If data is already a dict or list of dicts: unchanged
|
|
1124
|
+
- If data is None: None
|
|
1125
|
+
|
|
1126
|
+
Examples:
|
|
1127
|
+
>>> executor._wrap_primitives(42)
|
|
1128
|
+
{"value": 42}
|
|
1129
|
+
>>> executor._wrap_primitives([1, 2, 3])
|
|
1130
|
+
[{"value": 1}, {"value": 2}, {"value": 3}]
|
|
1131
|
+
>>> executor._wrap_primitives([1, {"id": 2}, 3])
|
|
1132
|
+
[{"value": 1}, {"id": 2}, {"value": 3}]
|
|
1133
|
+
>>> executor._wrap_primitives([[1, 2], 3])
|
|
1134
|
+
[{"value": [1, 2]}, {"value": 3}]
|
|
1135
|
+
>>> executor._wrap_primitives({"id": 1})
|
|
1136
|
+
{"id": 1} # unchanged
|
|
1137
|
+
"""
|
|
1138
|
+
if data is None:
|
|
1139
|
+
return None
|
|
1140
|
+
|
|
1141
|
+
# Handle primitive scalars
|
|
1142
|
+
if isinstance(data, (bool, str, int, float)):
|
|
1143
|
+
return {"value": data}
|
|
1144
|
+
|
|
1145
|
+
# Handle lists - wrap non-dict elements
|
|
1146
|
+
if isinstance(data, list):
|
|
1147
|
+
if not data:
|
|
1148
|
+
return [] # Empty list unchanged
|
|
1149
|
+
|
|
1150
|
+
wrapped = []
|
|
1151
|
+
for item in data:
|
|
1152
|
+
if isinstance(item, dict):
|
|
1153
|
+
wrapped.append(item)
|
|
1154
|
+
else:
|
|
1155
|
+
wrapped.append({"value": item})
|
|
1156
|
+
return wrapped
|
|
1157
|
+
|
|
1158
|
+
# Dict - return unchanged
|
|
1159
|
+
if isinstance(data, dict):
|
|
1160
|
+
return data
|
|
1161
|
+
|
|
1162
|
+
# Unknown type - wrap for safety
|
|
1163
|
+
return {"value": data}
|
|
1164
|
+
|
|
1101
1165
|
def _extract_records(
|
|
1102
1166
|
self,
|
|
1103
|
-
response_data:
|
|
1167
|
+
response_data: Any,
|
|
1104
1168
|
endpoint: EndpointDefinition,
|
|
1105
|
-
) -> dict[str, Any] | list[Any] | None:
|
|
1169
|
+
) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
1106
1170
|
"""Extract records from response using record extractor.
|
|
1107
1171
|
|
|
1108
1172
|
Type inference based on action:
|
|
1109
1173
|
- list, search: Returns array ([] if not found)
|
|
1110
1174
|
- get, create, update, delete: Returns single record (None if not found)
|
|
1111
1175
|
|
|
1176
|
+
Automatically wraps primitive values (int, str, float, bool) in {"value": primitive}
|
|
1177
|
+
format to ensure consistent dict-based responses for downstream code.
|
|
1178
|
+
|
|
1112
1179
|
Args:
|
|
1113
|
-
response_data: Full API response
|
|
1180
|
+
response_data: Full API response (can be dict, list, primitive, or None)
|
|
1114
1181
|
endpoint: Endpoint with optional record extractor and action
|
|
1115
1182
|
|
|
1116
1183
|
Returns:
|
|
1117
1184
|
- Extracted data if extractor configured and path found
|
|
1118
1185
|
- [] or None if path not found (based on action)
|
|
1119
1186
|
- Original response if no extractor configured or on error
|
|
1187
|
+
- Primitives are wrapped as {"value": primitive}
|
|
1120
1188
|
"""
|
|
1121
1189
|
# Check if endpoint has record extractor
|
|
1122
1190
|
extractor = endpoint.record_extractor
|
|
1123
1191
|
if not extractor:
|
|
1124
|
-
return response_data
|
|
1192
|
+
return self._wrap_primitives(response_data)
|
|
1125
1193
|
|
|
1126
1194
|
# Determine if this action returns array or single record
|
|
1127
1195
|
action = endpoint.action
|
|
1128
1196
|
if not action:
|
|
1129
|
-
return response_data
|
|
1197
|
+
return self._wrap_primitives(response_data)
|
|
1130
1198
|
|
|
1131
1199
|
is_array_action = action in (Action.LIST, Action.API_SEARCH)
|
|
1132
1200
|
|
|
@@ -1139,30 +1207,39 @@ class LocalExecutor:
|
|
|
1139
1207
|
# Path not found - return empty based on action
|
|
1140
1208
|
return [] if is_array_action else None
|
|
1141
1209
|
|
|
1142
|
-
# Return extracted data
|
|
1210
|
+
# Return extracted data with primitive wrapping
|
|
1143
1211
|
if is_array_action:
|
|
1144
1212
|
# For array actions, return the array (or list of matches)
|
|
1145
|
-
|
|
1213
|
+
result = matches[0] if len(matches) == 1 else matches
|
|
1146
1214
|
else:
|
|
1147
1215
|
# For single record actions, return first match
|
|
1148
|
-
|
|
1216
|
+
result = matches[0]
|
|
1217
|
+
|
|
1218
|
+
return self._wrap_primitives(result)
|
|
1149
1219
|
|
|
1150
1220
|
except Exception as e:
|
|
1151
1221
|
logging.warning(f"Failed to apply record extractor '{extractor}': {e}. Returning original response.")
|
|
1152
|
-
return response_data
|
|
1222
|
+
return self._wrap_primitives(response_data)
|
|
1153
1223
|
|
|
1154
1224
|
def _extract_metadata(
|
|
1155
1225
|
self,
|
|
1156
1226
|
response_data: dict[str, Any],
|
|
1227
|
+
response_headers: dict[str, str],
|
|
1157
1228
|
endpoint: EndpointDefinition,
|
|
1158
1229
|
) -> dict[str, Any] | None:
|
|
1159
1230
|
"""Extract metadata from response using meta extractor.
|
|
1160
1231
|
|
|
1161
|
-
Each field in meta_extractor dict is independently extracted using JSONPath
|
|
1232
|
+
Each field in meta_extractor dict is independently extracted using JSONPath
|
|
1233
|
+
for body extraction, or special prefixes for header extraction:
|
|
1234
|
+
- @link.{rel}: Extract URL from RFC 5988 Link header by rel type
|
|
1235
|
+
- @header.{name}: Extract raw header value by header name
|
|
1236
|
+
- Otherwise: JSONPath expression for body extraction
|
|
1237
|
+
|
|
1162
1238
|
Missing or invalid paths result in None for that field (no crash).
|
|
1163
1239
|
|
|
1164
1240
|
Args:
|
|
1165
1241
|
response_data: Full API response (before record extraction)
|
|
1242
|
+
response_headers: HTTP response headers
|
|
1166
1243
|
endpoint: Endpoint with optional meta extractor configuration
|
|
1167
1244
|
|
|
1168
1245
|
Returns:
|
|
@@ -1173,11 +1250,15 @@ class LocalExecutor:
|
|
|
1173
1250
|
Example:
|
|
1174
1251
|
meta_extractor = {
|
|
1175
1252
|
"pagination": "$.records",
|
|
1176
|
-
"request_id": "$.requestId"
|
|
1253
|
+
"request_id": "$.requestId",
|
|
1254
|
+
"next_page_url": "@link.next",
|
|
1255
|
+
"rate_limit": "@header.X-RateLimit-Remaining"
|
|
1177
1256
|
}
|
|
1178
1257
|
Returns: {
|
|
1179
1258
|
"pagination": {"cursor": "abc", "total": 100},
|
|
1180
|
-
"request_id": "xyz123"
|
|
1259
|
+
"request_id": "xyz123",
|
|
1260
|
+
"next_page_url": "https://api.example.com/data?cursor=abc",
|
|
1261
|
+
"rate_limit": "99"
|
|
1181
1262
|
}
|
|
1182
1263
|
"""
|
|
1183
1264
|
# Check if endpoint has meta extractor
|
|
@@ -1187,26 +1268,96 @@ class LocalExecutor:
|
|
|
1187
1268
|
extracted_meta: dict[str, Any] = {}
|
|
1188
1269
|
|
|
1189
1270
|
# Extract each field independently
|
|
1190
|
-
for field_name,
|
|
1271
|
+
for field_name, extractor_expr in endpoint.meta_extractor.items():
|
|
1191
1272
|
try:
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
#
|
|
1198
|
-
|
|
1273
|
+
if extractor_expr.startswith("@link."):
|
|
1274
|
+
# RFC 5988 Link header extraction
|
|
1275
|
+
rel = extractor_expr[6:]
|
|
1276
|
+
extracted_meta[field_name] = self._extract_link_url(response_headers, rel)
|
|
1277
|
+
elif extractor_expr.startswith("@header."):
|
|
1278
|
+
# Raw header value extraction (case-insensitive lookup)
|
|
1279
|
+
header_name = extractor_expr[8:]
|
|
1280
|
+
extracted_meta[field_name] = self._get_header_value(response_headers, header_name)
|
|
1199
1281
|
else:
|
|
1200
|
-
#
|
|
1201
|
-
|
|
1282
|
+
# JSONPath body extraction
|
|
1283
|
+
jsonpath_expr = parse_jsonpath(extractor_expr)
|
|
1284
|
+
matches = [match.value for match in jsonpath_expr.find(response_data)]
|
|
1285
|
+
|
|
1286
|
+
if matches:
|
|
1287
|
+
# Return first match (most common case)
|
|
1288
|
+
extracted_meta[field_name] = matches[0]
|
|
1289
|
+
else:
|
|
1290
|
+
# Path not found - set to None
|
|
1291
|
+
extracted_meta[field_name] = None
|
|
1202
1292
|
|
|
1203
1293
|
except Exception as e:
|
|
1204
1294
|
# Log error but continue with other fields
|
|
1205
|
-
logging.warning(f"Failed to apply meta extractor for field '{field_name}' with
|
|
1295
|
+
logging.warning(f"Failed to apply meta extractor for field '{field_name}' with expression '{extractor_expr}': {e}. Setting to None.")
|
|
1206
1296
|
extracted_meta[field_name] = None
|
|
1207
1297
|
|
|
1208
1298
|
return extracted_meta
|
|
1209
1299
|
|
|
1300
|
+
@staticmethod
|
|
1301
|
+
def _extract_link_url(headers: dict[str, str], rel: str) -> str | None:
|
|
1302
|
+
"""Extract URL from RFC 5988 Link header by rel type.
|
|
1303
|
+
|
|
1304
|
+
Parses Link header format: <url>; param1="value1"; rel="next"; param2="value2"
|
|
1305
|
+
|
|
1306
|
+
Supports:
|
|
1307
|
+
- Multiple parameters per link in any order
|
|
1308
|
+
- Both quoted and unquoted rel values
|
|
1309
|
+
- Multiple links separated by commas
|
|
1310
|
+
|
|
1311
|
+
Args:
|
|
1312
|
+
headers: Response headers dict
|
|
1313
|
+
rel: The rel type to extract (e.g., "next", "prev", "first", "last")
|
|
1314
|
+
|
|
1315
|
+
Returns:
|
|
1316
|
+
The URL for the specified rel type, or None if not found
|
|
1317
|
+
"""
|
|
1318
|
+
link_header = headers.get("Link") or headers.get("link", "")
|
|
1319
|
+
if not link_header:
|
|
1320
|
+
return None
|
|
1321
|
+
|
|
1322
|
+
for link_segment in re.split(r",(?=\s*<)", link_header):
|
|
1323
|
+
link_segment = link_segment.strip()
|
|
1324
|
+
|
|
1325
|
+
url_match = re.match(r"<([^>]+)>", link_segment)
|
|
1326
|
+
if not url_match:
|
|
1327
|
+
continue
|
|
1328
|
+
|
|
1329
|
+
url = url_match.group(1)
|
|
1330
|
+
params_str = link_segment[url_match.end() :]
|
|
1331
|
+
|
|
1332
|
+
rel_match = re.search(r';\s*rel="?([^";,]+)"?', params_str, re.IGNORECASE)
|
|
1333
|
+
if rel_match and rel_match.group(1).strip() == rel:
|
|
1334
|
+
return url
|
|
1335
|
+
|
|
1336
|
+
return None
|
|
1337
|
+
|
|
1338
|
+
@staticmethod
|
|
1339
|
+
def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
|
|
1340
|
+
"""Get header value with case-insensitive lookup.
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
headers: Response headers dict
|
|
1344
|
+
header_name: Header name to look up
|
|
1345
|
+
|
|
1346
|
+
Returns:
|
|
1347
|
+
Header value or None if not found
|
|
1348
|
+
"""
|
|
1349
|
+
# Try exact match first
|
|
1350
|
+
if header_name in headers:
|
|
1351
|
+
return headers[header_name]
|
|
1352
|
+
|
|
1353
|
+
# Case-insensitive lookup
|
|
1354
|
+
header_name_lower = header_name.lower()
|
|
1355
|
+
for key, value in headers.items():
|
|
1356
|
+
if key.lower() == header_name_lower:
|
|
1357
|
+
return value
|
|
1358
|
+
|
|
1359
|
+
return None
|
|
1360
|
+
|
|
1210
1361
|
def _validate_required_body_fields(self, endpoint: Any, params: dict[str, Any], action: Action, entity: str) -> None:
|
|
1211
1362
|
"""Validate that required body fields are present for CREATE/UPDATE operations.
|
|
1212
1363
|
|
|
@@ -1334,7 +1485,7 @@ class _StandardOperationHandler:
|
|
|
1334
1485
|
request_kwargs = self.ctx.determine_request_format(endpoint, body)
|
|
1335
1486
|
|
|
1336
1487
|
# Execute async HTTP request
|
|
1337
|
-
|
|
1488
|
+
response_data, response_headers = await self.ctx.http_client.request(
|
|
1338
1489
|
method=endpoint.method,
|
|
1339
1490
|
path=path,
|
|
1340
1491
|
params=query_params if query_params else None,
|
|
@@ -1343,10 +1494,10 @@ class _StandardOperationHandler:
|
|
|
1343
1494
|
)
|
|
1344
1495
|
|
|
1345
1496
|
# Extract metadata from original response (before record extraction)
|
|
1346
|
-
metadata = self.ctx.executor._extract_metadata(
|
|
1497
|
+
metadata = self.ctx.executor._extract_metadata(response_data, response_headers, endpoint)
|
|
1347
1498
|
|
|
1348
1499
|
# Extract records if extractor configured
|
|
1349
|
-
response = self.ctx.extract_records(
|
|
1500
|
+
response = self.ctx.extract_records(response_data, endpoint)
|
|
1350
1501
|
|
|
1351
1502
|
# Assume success with 200 status code if no exception raised
|
|
1352
1503
|
status_code = 200
|
|
@@ -1472,7 +1623,7 @@ class _DownloadOperationHandler:
|
|
|
1472
1623
|
request_format = self.ctx.determine_request_format(operation, request_body)
|
|
1473
1624
|
self.ctx.validate_required_body_fields(operation, params, action, entity)
|
|
1474
1625
|
|
|
1475
|
-
metadata_response = await self.ctx.http_client.request(
|
|
1626
|
+
metadata_response, _ = await self.ctx.http_client.request(
|
|
1476
1627
|
method=operation.method,
|
|
1477
1628
|
path=path,
|
|
1478
1629
|
params=query_params,
|
|
@@ -1487,7 +1638,7 @@ class _DownloadOperationHandler:
|
|
|
1487
1638
|
)
|
|
1488
1639
|
|
|
1489
1640
|
# Step 3: Stream file from extracted URL
|
|
1490
|
-
file_response = await self.ctx.http_client.request(
|
|
1641
|
+
file_response, _ = await self.ctx.http_client.request(
|
|
1491
1642
|
method="GET",
|
|
1492
1643
|
path=file_url,
|
|
1493
1644
|
headers=headers,
|
|
@@ -1495,7 +1646,7 @@ class _DownloadOperationHandler:
|
|
|
1495
1646
|
)
|
|
1496
1647
|
else:
|
|
1497
1648
|
# One-step direct download: stream file directly from endpoint
|
|
1498
|
-
file_response = await self.ctx.http_client.request(
|
|
1649
|
+
file_response, _ = await self.ctx.http_client.request(
|
|
1499
1650
|
method=operation.method,
|
|
1500
1651
|
path=path,
|
|
1501
1652
|
params=query_params,
|