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.
@@ -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()
@@ -474,8 +478,9 @@ class HTTPClient:
474
478
  request_id=request_id,
475
479
  status_code=status_code,
476
480
  response_body=f"<binary content, {response.headers.get('content-length', 'unknown')} bytes>",
481
+ response_headers=dict(response.headers),
477
482
  )
478
- return response
483
+ return response, dict(response.headers)
479
484
 
480
485
  # Parse response - handle non-JSON responses gracefully
481
486
  content_type = response.headers.get("content-type", "")
@@ -500,8 +505,9 @@ class HTTPClient:
500
505
  request_id=request_id,
501
506
  status_code=status_code,
502
507
  response_body=response_data,
508
+ response_headers=dict(response.headers),
503
509
  )
504
- return response_data
510
+ return response_data, dict(response.headers)
505
511
 
506
512
  except AuthenticationError as e:
507
513
  # Auth error (401, 403) - handle token refresh
@@ -631,7 +637,7 @@ class HTTPClient:
631
637
  *,
632
638
  stream: bool = False,
633
639
  _auth_retry_attempted: bool = False,
634
- ):
640
+ ) -> tuple[dict[str, Any], dict[str, str]]:
635
641
  """Make an async HTTP request with optional streaming and automatic retries.
636
642
 
637
643
  Args:
@@ -644,8 +650,9 @@ class HTTPClient:
644
650
  stream: If True, do not eagerly read the body (useful for downloads)
645
651
 
646
652
  Returns:
647
- - If stream=False: Parsed JSON (dict) or empty dict
648
- - If stream=True: Response object suitable for streaming
653
+ Tuple of (response_data, response_headers):
654
+ - If stream=False: (parsed JSON dict or empty dict, response headers dict)
655
+ - If stream=True: (response object suitable for streaming, response headers dict)
649
656
 
650
657
  Raises:
651
658
  HTTPStatusError: If request fails with 4xx/5xx status after all retries
@@ -134,6 +134,7 @@ class RequestLogger:
134
134
  request_id: str,
135
135
  status_code: int,
136
136
  response_body: Any | None = None,
137
+ response_headers: Dict[str, str] | None = None,
137
138
  ) -> None:
138
139
  """
139
140
  Log a successful HTTP response.
@@ -142,6 +143,7 @@ class RequestLogger:
142
143
  request_id: ID returned from log_request
143
144
  status_code: HTTP status code
144
145
  response_body: Response body
146
+ response_headers: Response headers
145
147
  """
146
148
  if request_id not in self._active_requests:
147
149
  return
@@ -166,6 +168,7 @@ class RequestLogger:
166
168
  body=request_data["body"],
167
169
  response_status=status_code,
168
170
  response_body=serializable_body,
171
+ response_headers=response_headers or {},
169
172
  timing_ms=timing_ms,
170
173
  )
171
174
 
@@ -243,7 +246,13 @@ class NullLogger:
243
246
  """No-op log_request."""
244
247
  return ""
245
248
 
246
- def log_response(self, *args, **kwargs) -> None:
249
+ def log_response(
250
+ self,
251
+ request_id: str,
252
+ status_code: int,
253
+ response_body: Any | None = None,
254
+ response_headers: Dict[str, str] | None = None,
255
+ ) -> None:
247
256
  """No-op log_response."""
248
257
  pass
249
258
 
@@ -31,6 +31,7 @@ class RequestLog(BaseModel):
31
31
  body: Any | None = None
32
32
  response_status: int | None = None
33
33
  response_body: Any | None = None
34
+ response_headers: Dict[str, str] = Field(default_factory=dict)
34
35
  timing_ms: float | None = None
35
36
  error: str | None = None
36
37
 
@@ -13,7 +13,7 @@ from uuid import UUID
13
13
  from pydantic import BaseModel, ConfigDict, Field, field_validator
14
14
  from pydantic_core import Url
15
15
 
16
- from .extensions import RetryConfig
16
+ from .extensions import CacheConfig, RetryConfig
17
17
 
18
18
 
19
19
  class ExampleQuestions(BaseModel):
@@ -105,6 +105,7 @@ class Info(BaseModel):
105
105
  - x-airbyte-external-documentation-urls: List of external documentation URLs (Airbyte extension)
106
106
  - x-airbyte-retry-config: Retry configuration for transient errors (Airbyte extension)
107
107
  - x-airbyte-example-questions: Example questions for AI connector README (Airbyte extension)
108
+ - x-airbyte-cache: Cache configuration for field mapping between API and cache schemas (Airbyte extension)
108
109
  """
109
110
 
110
111
  model_config = ConfigDict(populate_by_name=True, extra="forbid")
@@ -122,6 +123,7 @@ class Info(BaseModel):
122
123
  x_airbyte_external_documentation_urls: list[DocUrl] = Field(..., alias="x-airbyte-external-documentation-urls")
123
124
  x_airbyte_retry_config: RetryConfig | None = Field(None, alias="x-airbyte-retry-config")
124
125
  x_airbyte_example_questions: ExampleQuestions | None = Field(None, alias="x-airbyte-example-questions")
126
+ x_airbyte_cache: CacheConfig | None = Field(None, alias="x-airbyte-cache")
125
127
 
126
128
 
127
129
  class ServerVariable(BaseModel):
@@ -150,6 +152,7 @@ class Server(BaseModel):
150
152
  url: str
151
153
  description: str | None = None
152
154
  variables: Dict[str, ServerVariable] = Field(default_factory=dict)
155
+ x_airbyte_replication_user_config_mapping: Dict[str, str] | None = Field(default=None, alias="x-airbyte-replication-user-config-mapping")
153
156
 
154
157
  @field_validator("url")
155
158
  @classmethod
@@ -7,6 +7,7 @@ References:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from collections.abc import Iterator
10
11
  from typing import Any
11
12
 
12
13
  from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -15,7 +16,7 @@ from ..constants import OPENAPI_VERSION_PREFIX
15
16
 
16
17
  from .base import Info, Server
17
18
  from .components import Components
18
- from .operations import PathItem
19
+ from .operations import Operation, PathItem
19
20
  from .security import SecurityRequirement
20
21
 
21
22
 
@@ -33,6 +34,10 @@ class Tag(BaseModel):
33
34
  external_docs: dict[str, Any] | None = Field(None, alias="externalDocs")
34
35
 
35
36
 
37
+ # HTTP methods supported by OpenAPI operations
38
+ HTTP_METHODS = frozenset({"get", "post", "put", "patch", "delete", "options", "head", "trace"})
39
+
40
+
36
41
  class ExternalDocs(BaseModel):
37
42
  """
38
43
  External documentation reference.
@@ -79,7 +84,7 @@ class OpenAPIConnector(BaseModel):
79
84
  raise ValueError(f"OpenAPI version must be {OPENAPI_VERSION_PREFIX}x, got: {v}")
80
85
  return v
81
86
 
82
- def get_entity_operations(self, entity_name: str) -> list[tuple[str, str, Any]]:
87
+ def get_entity_operations(self, entity_name: str) -> list[tuple[str, str, Operation]]:
83
88
  """
84
89
  Get all operations for a specific entity.
85
90
 
@@ -89,22 +94,7 @@ class OpenAPIConnector(BaseModel):
89
94
  Returns:
90
95
  List of tuples: (path, method, operation)
91
96
  """
92
- results = []
93
- for path, path_item in self.paths.items():
94
- for method in [
95
- "get",
96
- "post",
97
- "put",
98
- "patch",
99
- "delete",
100
- "options",
101
- "head",
102
- "trace",
103
- ]:
104
- operation = getattr(path_item, method, None)
105
- if operation and operation.x_airbyte_entity == entity_name:
106
- results.append((path, method, operation))
107
- return results
97
+ return [(path, method, op) for path, method, op in self._iter_operations() if op.x_airbyte_entity == entity_name]
108
98
 
109
99
  def list_entities(self) -> list[str]:
110
100
  """
@@ -113,19 +103,18 @@ class OpenAPIConnector(BaseModel):
113
103
  Returns:
114
104
  Sorted list of unique entity names
115
105
  """
116
- entities = set()
117
- for path_item in self.paths.values():
118
- for method in [
119
- "get",
120
- "post",
121
- "put",
122
- "patch",
123
- "delete",
124
- "options",
125
- "head",
126
- "trace",
127
- ]:
128
- operation = getattr(path_item, method, None)
129
- if operation and operation.x_airbyte_entity:
130
- entities.add(operation.x_airbyte_entity)
106
+ entities = {op.x_airbyte_entity for _, _, op in self._iter_operations() if op.x_airbyte_entity}
131
107
  return sorted(entities)
108
+
109
+ def _iter_operations(self) -> Iterator[tuple[str, str, Operation]]:
110
+ """
111
+ Iterate over all operations in the spec.
112
+
113
+ Yields:
114
+ Tuples of (path, method, operation) for each defined operation
115
+ """
116
+ for path, path_item in self.paths.items():
117
+ for method in HTTP_METHODS:
118
+ operation = getattr(path_item, method, None)
119
+ if operation:
120
+ yield path, method, operation
@@ -14,7 +14,7 @@ are implemented.
14
14
 
15
15
  from typing import Literal
16
16
 
17
- from pydantic import BaseModel, ConfigDict
17
+ from pydantic import BaseModel, ConfigDict, Field
18
18
 
19
19
 
20
20
  class PaginationConfig(BaseModel):
@@ -107,3 +107,124 @@ class RetryConfig(BaseModel):
107
107
  # Header-based delay extraction
108
108
  retry_after_header: str = "Retry-After"
109
109
  retry_after_format: Literal["seconds", "milliseconds", "unix_timestamp"] = "seconds"
110
+
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
+
136
+ class CacheFieldConfig(BaseModel):
137
+ """
138
+ Field configuration for cache mapping.
139
+
140
+ Defines a single field in a cache entity, with optional name aliasing
141
+ to map between user-facing field names and cache storage names.
142
+
143
+ For object-type fields, supports nested properties to define the internal structure
144
+ of complex nested schemas.
145
+
146
+ Used in x-airbyte-cache extension for api_search operations.
147
+ """
148
+
149
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
150
+
151
+ name: str
152
+ x_airbyte_name: str | None = Field(default=None, alias="x-airbyte-name")
153
+ type: str | list[str]
154
+ description: str
155
+ properties: dict[str, CacheFieldProperty] | None = None
156
+
157
+ @property
158
+ def cache_name(self) -> str:
159
+ """Return cache name, falling back to name if alias not specified."""
160
+ return self.x_airbyte_name or self.name
161
+
162
+
163
+ class CacheEntityConfig(BaseModel):
164
+ """
165
+ Entity configuration for cache mapping.
166
+
167
+ Defines a cache-enabled entity with its fields and optional name aliasing
168
+ to map between user-facing entity names and cache storage names.
169
+
170
+ Used in x-airbyte-cache extension for api_search operations.
171
+ """
172
+
173
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
174
+
175
+ entity: str
176
+ x_airbyte_name: str | None = Field(default=None, alias="x-airbyte-name")
177
+ fields: list[CacheFieldConfig]
178
+
179
+ @property
180
+ def cache_name(self) -> str:
181
+ """Return cache entity name, falling back to entity if alias not specified."""
182
+ return self.x_airbyte_name or self.entity
183
+
184
+
185
+ class CacheConfig(BaseModel):
186
+ """
187
+ Cache configuration extension (x-airbyte-cache).
188
+
189
+ Defines cache-enabled entities and their field mappings for api_search operations.
190
+ Supports optional name aliasing via x-airbyte-name for both entities and fields,
191
+ enabling bidirectional mapping between user-facing names and cache storage names.
192
+
193
+ This extension is added to the Info model and provides field-level mapping for
194
+ search operations that use cached data.
195
+
196
+ Example YAML usage:
197
+ info:
198
+ title: Stripe API
199
+ x-airbyte-cache:
200
+ entities:
201
+ - entity: customers
202
+ stream: customers
203
+ fields:
204
+ - name: email
205
+ type: ["null", "string"]
206
+ description: "Customer email address"
207
+ - name: customer_name
208
+ x-airbyte-name: name
209
+ type: ["null", "string"]
210
+ description: "Customer full name"
211
+ """
212
+
213
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
214
+
215
+ entities: list[CacheEntityConfig]
216
+
217
+ def get_entity_mapping(self, user_entity: str) -> CacheEntityConfig | None:
218
+ """
219
+ Get entity config by user-facing name.
220
+
221
+ Args:
222
+ user_entity: User-facing entity name to look up
223
+
224
+ Returns:
225
+ CacheEntityConfig if found, None otherwise
226
+ """
227
+ for entity in self.entities:
228
+ if entity.entity == user_entity:
229
+ return entity
230
+ return None
@@ -486,30 +486,36 @@ def validate_meta_extractor_fields(
486
486
  response_body = spec.captured_response.body
487
487
 
488
488
  # Validate each meta extractor field
489
- for field_name, jsonpath_expr in endpoint.meta_extractor.items():
489
+ for field_name, extractor_expr in endpoint.meta_extractor.items():
490
+ # Skip header-based extractors - they extract from headers, not response body
491
+ # @link.next extracts from RFC 5988 Link header
492
+ # @header.X-Name extracts raw header value
493
+ if extractor_expr.startswith("@link.") or extractor_expr.startswith("@header."):
494
+ continue
495
+
490
496
  # Check 1: Does the JSONPath find data in the actual response?
491
497
  try:
492
- parsed_expr = parse_jsonpath(jsonpath_expr)
498
+ parsed_expr = parse_jsonpath(extractor_expr)
493
499
  matches = [match.value for match in parsed_expr.find(response_body)]
494
500
 
495
501
  if not matches:
496
502
  warnings.append(
497
503
  f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' "
498
- f"with JSONPath '{jsonpath_expr}' found no matches in cassette response"
504
+ f"with JSONPath '{extractor_expr}' found no matches in cassette response"
499
505
  )
500
506
  except Exception as e:
501
507
  warnings.append(
502
- f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' has invalid JSONPath '{jsonpath_expr}': {str(e)}"
508
+ f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' has invalid JSONPath '{extractor_expr}': {str(e)}"
503
509
  )
504
510
 
505
511
  # Check 2: Is this field path declared in the response schema?
506
512
  if endpoint.response_schema:
507
- field_in_schema = _check_field_in_schema(jsonpath_expr, endpoint.response_schema)
513
+ field_in_schema = _check_field_in_schema(extractor_expr, endpoint.response_schema)
508
514
 
509
515
  if not field_in_schema:
510
516
  warnings.append(
511
517
  f"{entity_name}.{action}: x-airbyte-meta-extractor field '{field_name}' "
512
- f"extracts from '{jsonpath_expr}' but this path is not declared in response schema"
518
+ f"extracts from '{extractor_expr}' but this path is not declared in response schema"
513
519
  )
514
520
 
515
521
  except Exception as e: