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.
@@ -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
- ArticleAttachmentsGetResult
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__ = ["ZendeskSupportConnector", "ZendeskSupportAuthConfig", "Ticket", "User", "Organization", "Group", "TicketComment", "Attachment", "TicketAudit", "TicketMetric", "TicketField", "Brand", "View", "Macro", "Trigger", "Automation", "Tag", "SatisfactionRating", "GroupMembership", "OrganizationMembership", "SLAPolicy", "TicketForm", "Article", "ArticleAttachment", "TicketsListResultMeta", "UsersListResultMeta", "OrganizationsListResultMeta", "GroupsListResultMeta", "TicketCommentsListResultMeta", "TicketAuditsListResultMeta", "TicketMetricsListResultMeta", "TicketFieldsListResultMeta", "BrandsListResultMeta", "ViewsListResultMeta", "MacrosListResultMeta", "TriggersListResultMeta", "AutomationsListResultMeta", "TagsListResultMeta", "SatisfactionRatingsListResultMeta", "GroupMembershipsListResultMeta", "OrganizationMembershipsListResultMeta", "SlaPoliciesListResultMeta", "TicketFormsListResultMeta", "ArticlesListResultMeta", "ArticleAttachmentsListResultMeta", "ZendeskSupportExecuteResult", "ZendeskSupportExecuteResultWithMeta", "TicketsListResult", "TicketsGetResult", "UsersListResult", "UsersGetResult", "OrganizationsListResult", "OrganizationsGetResult", "GroupsListResult", "GroupsGetResult", "TicketCommentsListResult", "AttachmentsGetResult", "TicketAuditsListResult", "TicketMetricsListResult", "TicketFieldsListResult", "TicketFieldsGetResult", "BrandsListResult", "BrandsGetResult", "ViewsListResult", "ViewsGetResult", "MacrosListResult", "MacrosGetResult", "TriggersListResult", "TriggersGetResult", "AutomationsListResult", "AutomationsGetResult", "TagsListResult", "SatisfactionRatingsListResult", "SatisfactionRatingsGetResult", "GroupMembershipsListResult", "OrganizationMembershipsListResult", "SlaPoliciesListResult", "SlaPoliciesGetResult", "TicketFormsListResult", "TicketFormsGetResult", "ArticlesListResult", "ArticlesGetResult", "ArticleAttachmentsListResult", "ArticleAttachmentsGetResult", "TicketsListParams", "TicketsGetParams", "UsersListParams", "UsersGetParams", "OrganizationsListParams", "OrganizationsGetParams", "GroupsListParams", "GroupsGetParams", "TicketCommentsListParams", "AttachmentsGetParams", "AttachmentsDownloadParams", "TicketAuditsListParams", "TicketAuditsListParams", "TicketMetricsListParams", "TicketFieldsListParams", "TicketFieldsGetParams", "BrandsListParams", "BrandsGetParams", "ViewsListParams", "ViewsGetParams", "MacrosListParams", "MacrosGetParams", "TriggersListParams", "TriggersGetParams", "AutomationsListParams", "AutomationsGetParams", "TagsListParams", "SatisfactionRatingsListParams", "SatisfactionRatingsGetParams", "GroupMembershipsListParams", "OrganizationMembershipsListParams", "SlaPoliciesListParams", "SlaPoliciesGetParams", "TicketFormsListParams", "TicketFormsGetParams", "ArticlesListParams", "ArticlesGetParams", "ArticleAttachmentsListParams", "ArticleAttachmentsGetParams", "ArticleAttachmentsDownloadParams"]
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 = getattr(x_token_refresh, "auth_style", None)
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 = getattr(x_token_refresh, "body_format", None)
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
- body["variables"] = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
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: dict[str, Any],
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
- return matches[0] if len(matches) == 1 else matches
1213
+ result = matches[0] if len(matches) == 1 else matches
1146
1214
  else:
1147
1215
  # For single record actions, return first match
1148
- return matches[0]
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, jsonpath_expr_str in endpoint.meta_extractor.items():
1271
+ for field_name, extractor_expr in endpoint.meta_extractor.items():
1191
1272
  try:
1192
- # Parse and apply JSONPath expression
1193
- jsonpath_expr = parse_jsonpath(jsonpath_expr_str)
1194
- matches = [match.value for match in jsonpath_expr.find(response_data)]
1195
-
1196
- if matches:
1197
- # Return first match (most common case)
1198
- extracted_meta[field_name] = matches[0]
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
- # Path not found - set to None
1201
- extracted_meta[field_name] = None
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 path '{jsonpath_expr_str}': {e}. Setting to None.")
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
- response = await self.ctx.http_client.request(
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(response, endpoint)
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(response, endpoint)
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,