airbyte-agent-zendesk-support 0.18.44__py3-none-any.whl → 0.18.49__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.
@@ -72,7 +72,33 @@ from .models import (
72
72
  SlaPoliciesListResult,
73
73
  TicketFormsListResult,
74
74
  ArticlesListResult,
75
- ArticleAttachmentsListResult
75
+ ArticleAttachmentsListResult,
76
+ SearchHit,
77
+ SearchResult,
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
+ SearchParams,
145
+ SortOrder,
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
+ "SearchHit",
254
+ "SearchResult",
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
+ "SearchParams",
320
+ "SortOrder",
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
  ]
@@ -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 {}
@@ -1214,15 +1222,22 @@ class LocalExecutor:
1214
1222
  def _extract_metadata(
1215
1223
  self,
1216
1224
  response_data: dict[str, Any],
1225
+ response_headers: dict[str, str],
1217
1226
  endpoint: EndpointDefinition,
1218
1227
  ) -> dict[str, Any] | None:
1219
1228
  """Extract metadata from response using meta extractor.
1220
1229
 
1221
- Each field in meta_extractor dict is independently extracted using JSONPath.
1230
+ Each field in meta_extractor dict is independently extracted using JSONPath
1231
+ for body extraction, or special prefixes for header extraction:
1232
+ - @link.{rel}: Extract URL from RFC 5988 Link header by rel type
1233
+ - @header.{name}: Extract raw header value by header name
1234
+ - Otherwise: JSONPath expression for body extraction
1235
+
1222
1236
  Missing or invalid paths result in None for that field (no crash).
1223
1237
 
1224
1238
  Args:
1225
1239
  response_data: Full API response (before record extraction)
1240
+ response_headers: HTTP response headers
1226
1241
  endpoint: Endpoint with optional meta extractor configuration
1227
1242
 
1228
1243
  Returns:
@@ -1233,11 +1248,15 @@ class LocalExecutor:
1233
1248
  Example:
1234
1249
  meta_extractor = {
1235
1250
  "pagination": "$.records",
1236
- "request_id": "$.requestId"
1251
+ "request_id": "$.requestId",
1252
+ "next_page_url": "@link.next",
1253
+ "rate_limit": "@header.X-RateLimit-Remaining"
1237
1254
  }
1238
1255
  Returns: {
1239
1256
  "pagination": {"cursor": "abc", "total": 100},
1240
- "request_id": "xyz123"
1257
+ "request_id": "xyz123",
1258
+ "next_page_url": "https://api.example.com/data?cursor=abc",
1259
+ "rate_limit": "99"
1241
1260
  }
1242
1261
  """
1243
1262
  # Check if endpoint has meta extractor
@@ -1247,26 +1266,96 @@ class LocalExecutor:
1247
1266
  extracted_meta: dict[str, Any] = {}
1248
1267
 
1249
1268
  # Extract each field independently
1250
- for field_name, jsonpath_expr_str in endpoint.meta_extractor.items():
1269
+ for field_name, extractor_expr in endpoint.meta_extractor.items():
1251
1270
  try:
1252
- # Parse and apply JSONPath expression
1253
- jsonpath_expr = parse_jsonpath(jsonpath_expr_str)
1254
- matches = [match.value for match in jsonpath_expr.find(response_data)]
1255
-
1256
- if matches:
1257
- # Return first match (most common case)
1258
- extracted_meta[field_name] = matches[0]
1271
+ if extractor_expr.startswith("@link."):
1272
+ # RFC 5988 Link header extraction
1273
+ rel = extractor_expr[6:]
1274
+ extracted_meta[field_name] = self._extract_link_url(response_headers, rel)
1275
+ elif extractor_expr.startswith("@header."):
1276
+ # Raw header value extraction (case-insensitive lookup)
1277
+ header_name = extractor_expr[8:]
1278
+ extracted_meta[field_name] = self._get_header_value(response_headers, header_name)
1259
1279
  else:
1260
- # Path not found - set to None
1261
- extracted_meta[field_name] = None
1280
+ # JSONPath body extraction
1281
+ jsonpath_expr = parse_jsonpath(extractor_expr)
1282
+ matches = [match.value for match in jsonpath_expr.find(response_data)]
1283
+
1284
+ if matches:
1285
+ # Return first match (most common case)
1286
+ extracted_meta[field_name] = matches[0]
1287
+ else:
1288
+ # Path not found - set to None
1289
+ extracted_meta[field_name] = None
1262
1290
 
1263
1291
  except Exception as e:
1264
1292
  # Log error but continue with other fields
1265
- logging.warning(f"Failed to apply meta extractor for field '{field_name}' with path '{jsonpath_expr_str}': {e}. Setting to None.")
1293
+ logging.warning(f"Failed to apply meta extractor for field '{field_name}' with expression '{extractor_expr}': {e}. Setting to None.")
1266
1294
  extracted_meta[field_name] = None
1267
1295
 
1268
1296
  return extracted_meta
1269
1297
 
1298
+ @staticmethod
1299
+ def _extract_link_url(headers: dict[str, str], rel: str) -> str | None:
1300
+ """Extract URL from RFC 5988 Link header by rel type.
1301
+
1302
+ Parses Link header format: <url>; param1="value1"; rel="next"; param2="value2"
1303
+
1304
+ Supports:
1305
+ - Multiple parameters per link in any order
1306
+ - Both quoted and unquoted rel values
1307
+ - Multiple links separated by commas
1308
+
1309
+ Args:
1310
+ headers: Response headers dict
1311
+ rel: The rel type to extract (e.g., "next", "prev", "first", "last")
1312
+
1313
+ Returns:
1314
+ The URL for the specified rel type, or None if not found
1315
+ """
1316
+ link_header = headers.get("Link") or headers.get("link", "")
1317
+ if not link_header:
1318
+ return None
1319
+
1320
+ for link_segment in re.split(r",(?=\s*<)", link_header):
1321
+ link_segment = link_segment.strip()
1322
+
1323
+ url_match = re.match(r"<([^>]+)>", link_segment)
1324
+ if not url_match:
1325
+ continue
1326
+
1327
+ url = url_match.group(1)
1328
+ params_str = link_segment[url_match.end() :]
1329
+
1330
+ rel_match = re.search(r';\s*rel="?([^";,]+)"?', params_str, re.IGNORECASE)
1331
+ if rel_match and rel_match.group(1).strip() == rel:
1332
+ return url
1333
+
1334
+ return None
1335
+
1336
+ @staticmethod
1337
+ def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
1338
+ """Get header value with case-insensitive lookup.
1339
+
1340
+ Args:
1341
+ headers: Response headers dict
1342
+ header_name: Header name to look up
1343
+
1344
+ Returns:
1345
+ Header value or None if not found
1346
+ """
1347
+ # Try exact match first
1348
+ if header_name in headers:
1349
+ return headers[header_name]
1350
+
1351
+ # Case-insensitive lookup
1352
+ header_name_lower = header_name.lower()
1353
+ for key, value in headers.items():
1354
+ if key.lower() == header_name_lower:
1355
+ return value
1356
+
1357
+ return None
1358
+
1270
1359
  def _validate_required_body_fields(self, endpoint: Any, params: dict[str, Any], action: Action, entity: str) -> None:
1271
1360
  """Validate that required body fields are present for CREATE/UPDATE operations.
1272
1361
 
@@ -1394,7 +1483,7 @@ class _StandardOperationHandler:
1394
1483
  request_kwargs = self.ctx.determine_request_format(endpoint, body)
1395
1484
 
1396
1485
  # Execute async HTTP request
1397
- response = await self.ctx.http_client.request(
1486
+ response_data, response_headers = await self.ctx.http_client.request(
1398
1487
  method=endpoint.method,
1399
1488
  path=path,
1400
1489
  params=query_params if query_params else None,
@@ -1403,10 +1492,10 @@ class _StandardOperationHandler:
1403
1492
  )
1404
1493
 
1405
1494
  # Extract metadata from original response (before record extraction)
1406
- metadata = self.ctx.executor._extract_metadata(response, endpoint)
1495
+ metadata = self.ctx.executor._extract_metadata(response_data, response_headers, endpoint)
1407
1496
 
1408
1497
  # Extract records if extractor configured
1409
- response = self.ctx.extract_records(response, endpoint)
1498
+ response = self.ctx.extract_records(response_data, endpoint)
1410
1499
 
1411
1500
  # Assume success with 200 status code if no exception raised
1412
1501
  status_code = 200
@@ -1532,7 +1621,7 @@ class _DownloadOperationHandler:
1532
1621
  request_format = self.ctx.determine_request_format(operation, request_body)
1533
1622
  self.ctx.validate_required_body_fields(operation, params, action, entity)
1534
1623
 
1535
- metadata_response = await self.ctx.http_client.request(
1624
+ metadata_response, _ = await self.ctx.http_client.request(
1536
1625
  method=operation.method,
1537
1626
  path=path,
1538
1627
  params=query_params,
@@ -1547,7 +1636,7 @@ class _DownloadOperationHandler:
1547
1636
  )
1548
1637
 
1549
1638
  # Step 3: Stream file from extracted URL
1550
- file_response = await self.ctx.http_client.request(
1639
+ file_response, _ = await self.ctx.http_client.request(
1551
1640
  method="GET",
1552
1641
  path=file_url,
1553
1642
  headers=headers,
@@ -1555,7 +1644,7 @@ class _DownloadOperationHandler:
1555
1644
  )
1556
1645
  else:
1557
1646
  # One-step direct download: stream file directly from endpoint
1558
- file_response = await self.ctx.http_client.request(
1647
+ file_response, _ = await self.ctx.http_client.request(
1559
1648
  method=operation.method,
1560
1649
  path=path,
1561
1650
  params=query_params,
@@ -421,10 +421,14 @@ class HTTPClient:
421
421
  headers: dict[str, str] | None = None,
422
422
  *,
423
423
  stream: bool = False,
424
- ):
424
+ ) -> tuple[dict[str, Any], dict[str, str]]:
425
425
  """Execute a single HTTP request attempt (no retries).
426
426
 
427
427
  This is the core request logic, separated from retry handling.
428
+
429
+ Returns:
430
+ Tuple of (response_data, response_headers) for non-streaming requests.
431
+ For streaming requests, returns (response_object, response_headers).
428
432
  """
429
433
  # Ensure auth credentials are initialized (proactive refresh if needed)
430
434
  await self._ensure_auth_initialized()
@@ -475,7 +479,7 @@ class HTTPClient:
475
479
  status_code=status_code,
476
480
  response_body=f"<binary content, {response.headers.get('content-length', 'unknown')} bytes>",
477
481
  )
478
- return response
482
+ return response, dict(response.headers)
479
483
 
480
484
  # Parse response - handle non-JSON responses gracefully
481
485
  content_type = response.headers.get("content-type", "")
@@ -501,7 +505,7 @@ class HTTPClient:
501
505
  status_code=status_code,
502
506
  response_body=response_data,
503
507
  )
504
- return response_data
508
+ return response_data, dict(response.headers)
505
509
 
506
510
  except AuthenticationError as e:
507
511
  # Auth error (401, 403) - handle token refresh
@@ -631,7 +635,7 @@ class HTTPClient:
631
635
  *,
632
636
  stream: bool = False,
633
637
  _auth_retry_attempted: bool = False,
634
- ):
638
+ ) -> tuple[dict[str, Any], dict[str, str]]:
635
639
  """Make an async HTTP request with optional streaming and automatic retries.
636
640
 
637
641
  Args:
@@ -644,8 +648,9 @@ class HTTPClient:
644
648
  stream: If True, do not eagerly read the body (useful for downloads)
645
649
 
646
650
  Returns:
647
- - If stream=False: Parsed JSON (dict) or empty dict
648
- - If stream=True: Response object suitable for streaming
651
+ Tuple of (response_data, response_headers):
652
+ - If stream=False: (parsed JSON dict or empty dict, response headers dict)
653
+ - If stream=True: (response object suitable for streaming, response headers dict)
649
654
 
650
655
  Raises:
651
656
  HTTPStatusError: If request fails with 4xx/5xx status after all retries
@@ -109,6 +109,30 @@ class RetryConfig(BaseModel):
109
109
  retry_after_format: Literal["seconds", "milliseconds", "unix_timestamp"] = "seconds"
110
110
 
111
111
 
112
+ class CacheFieldProperty(BaseModel):
113
+ """
114
+ Nested property definition for object-type cache fields.
115
+
116
+ Supports recursive nesting to represent complex nested schemas in cache field definitions.
117
+ Used when a cache field has type 'object' and needs to define its internal structure.
118
+
119
+ Example YAML usage:
120
+ - name: collaboration
121
+ type: ['null', 'object']
122
+ description: "Collaboration data"
123
+ properties:
124
+ brief:
125
+ type: ['null', 'string']
126
+ comments:
127
+ type: ['null', 'array']
128
+ """
129
+
130
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
131
+
132
+ type: str | list[str]
133
+ properties: dict[str, "CacheFieldProperty"] | None = None
134
+
135
+
112
136
  class CacheFieldConfig(BaseModel):
113
137
  """
114
138
  Field configuration for cache mapping.
@@ -116,6 +140,9 @@ class CacheFieldConfig(BaseModel):
116
140
  Defines a single field in a cache entity, with optional name aliasing
117
141
  to map between user-facing field names and cache storage names.
118
142
 
143
+ For object-type fields, supports nested properties to define the internal structure
144
+ of complex nested schemas.
145
+
119
146
  Used in x-airbyte-cache extension for api_search operations.
120
147
  """
121
148
 
@@ -125,6 +152,7 @@ class CacheFieldConfig(BaseModel):
125
152
  x_airbyte_name: str | None = Field(default=None, alias="x-airbyte-name")
126
153
  type: str | list[str]
127
154
  description: str
155
+ properties: dict[str, CacheFieldProperty] | None = None
128
156
 
129
157
  @property
130
158
  def cache_name(self) -> str: