airbyte-agent-zendesk-support 0.18.18__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.
Files changed (37) hide show
  1. airbyte_agent_zendesk_support/__init__.py +239 -18
  2. airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_strategies.py +2 -5
  3. airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_template.py +1 -1
  4. airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  5. airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/client.py +213 -0
  6. airbyte_agent_zendesk_support/_vendored/connector_sdk/connector_model_loader.py +35 -8
  7. airbyte_agent_zendesk_support/_vendored/connector_sdk/constants.py +1 -1
  8. airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/hosted_executor.py +92 -84
  9. airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/local_executor.py +274 -54
  10. airbyte_agent_zendesk_support/_vendored/connector_sdk/extensions.py +43 -5
  11. airbyte_agent_zendesk_support/_vendored/connector_sdk/http/response.py +2 -0
  12. airbyte_agent_zendesk_support/_vendored/connector_sdk/http_client.py +63 -49
  13. airbyte_agent_zendesk_support/_vendored/connector_sdk/introspection.py +262 -0
  14. airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/logger.py +19 -10
  15. airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/types.py +11 -10
  16. airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/config.py +179 -0
  17. airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/models.py +6 -6
  18. airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/session.py +41 -32
  19. airbyte_agent_zendesk_support/_vendored/connector_sdk/performance/metrics.py +3 -3
  20. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/base.py +22 -18
  21. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/components.py +59 -58
  22. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/connector.py +22 -33
  23. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/extensions.py +131 -10
  24. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/operations.py +32 -32
  25. airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/security.py +44 -34
  26. airbyte_agent_zendesk_support/_vendored/connector_sdk/secrets.py +2 -2
  27. airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/events.py +9 -8
  28. airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/tracker.py +9 -5
  29. airbyte_agent_zendesk_support/_vendored/connector_sdk/types.py +9 -3
  30. airbyte_agent_zendesk_support/_vendored/connector_sdk/validation.py +12 -6
  31. airbyte_agent_zendesk_support/connector.py +1170 -171
  32. airbyte_agent_zendesk_support/connector_model.py +7 -1
  33. airbyte_agent_zendesk_support/models.py +628 -69
  34. airbyte_agent_zendesk_support/types.py +3717 -1
  35. {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/METADATA +55 -31
  36. {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/RECORD +37 -33
  37. {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/WHEEL +0 -0
@@ -80,6 +80,8 @@ class HTTPResponse:
80
80
  HTTPStatusError: For 4xx or 5xx status codes.
81
81
  """
82
82
  if 400 <= self._status_code < 600:
83
+ # NOTE: Import here intentionally to avoid circular import.
84
+ # exceptions.py imports HTTPResponse for type hints.
83
85
  from .exceptions import HTTPStatusError
84
86
 
85
87
  raise HTTPStatusError(
@@ -147,6 +147,9 @@ class HTTPClient:
147
147
  self.base_url = self.base_url.replace(f"{{{var_name}}}", var_value)
148
148
 
149
149
  self.auth_config = auth_config
150
+ assert (
151
+ self.auth_config.type is not None
152
+ ), "auth_config.type cannot be None" # Should never be None when instantiated via the local executor flow
150
153
  self.secrets = secrets
151
154
  self.logger = logger or NullLogger()
152
155
  self.metrics = HTTPMetrics()
@@ -296,12 +299,12 @@ class HTTPClient:
296
299
 
297
300
  # Support both sync and async callbacks
298
301
  callback_result = self.on_token_refresh(callback_data)
299
- if hasattr(callback_result, "__await__"):
302
+ if callback_result is not None and hasattr(callback_result, "__await__"):
300
303
  await callback_result
301
304
  except Exception as callback_error:
302
305
  self.logger.log_error(
303
306
  request_id=None,
304
- error=("Token refresh callback failed during initialization: " f"{callback_error!s}"),
307
+ error=(f"Token refresh callback failed during initialization: {callback_error!s}"),
305
308
  status_code=None,
306
309
  )
307
310
 
@@ -418,10 +421,14 @@ class HTTPClient:
418
421
  headers: dict[str, str] | None = None,
419
422
  *,
420
423
  stream: bool = False,
421
- ):
424
+ ) -> tuple[dict[str, Any], dict[str, str]]:
422
425
  """Execute a single HTTP request attempt (no retries).
423
426
 
424
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).
425
432
  """
426
433
  # Ensure auth credentials are initialized (proactive refresh if needed)
427
434
  await self._ensure_auth_initialized()
@@ -471,8 +478,9 @@ class HTTPClient:
471
478
  request_id=request_id,
472
479
  status_code=status_code,
473
480
  response_body=f"<binary content, {response.headers.get('content-length', 'unknown')} bytes>",
481
+ response_headers=dict(response.headers),
474
482
  )
475
- return response
483
+ return response, dict(response.headers)
476
484
 
477
485
  # Parse response - handle non-JSON responses gracefully
478
486
  content_type = response.headers.get("content-type", "")
@@ -485,7 +493,7 @@ class HTTPClient:
485
493
  elif "application/json" in content_type or not content_type:
486
494
  response_data = await response.json()
487
495
  else:
488
- error_msg = f"Expected JSON response for {method.upper()} {url}, " f"got content-type: {content_type}"
496
+ error_msg = f"Expected JSON response for {method.upper()} {url}, got content-type: {content_type}"
489
497
  raise HTTPClientError(error_msg)
490
498
 
491
499
  except ValueError as e:
@@ -497,8 +505,9 @@ class HTTPClient:
497
505
  request_id=request_id,
498
506
  status_code=status_code,
499
507
  response_body=response_data,
508
+ response_headers=dict(response.headers),
500
509
  )
501
- return response_data
510
+ return response_data, dict(response.headers)
502
511
 
503
512
  except AuthenticationError as e:
504
513
  # Auth error (401, 403) - handle token refresh
@@ -556,6 +565,7 @@ class HTTPClient:
556
565
  current_token = self.secrets.get("access_token")
557
566
  strategy = AuthStrategyFactory.get_strategy(self.auth_config.type)
558
567
 
568
+ # Try to refresh credentials
559
569
  try:
560
570
  result = await strategy.handle_auth_error(
561
571
  status_code=status_code,
@@ -564,53 +574,56 @@ class HTTPClient:
564
574
  config_values=self.config_values,
565
575
  http_client=None, # Let strategy create its own client
566
576
  )
567
-
568
- if result:
569
- # Notify callback if provided (for persistence)
570
- # Include both tokens AND extracted values for full persistence
571
- if self.on_token_refresh is not None:
572
- try:
573
- # Build callback data with both tokens and extracted values
574
- callback_data = dict(result.tokens)
575
- if result.extracted_values:
576
- callback_data.update(result.extracted_values)
577
-
578
- # Support both sync and async callbacks
579
- callback_result = self.on_token_refresh(callback_data)
580
- if hasattr(callback_result, "__await__"):
581
- await callback_result
582
- except Exception as callback_error:
583
- self.logger.log_error(
584
- request_id=request_id,
585
- error=f"Token refresh callback failed: {str(callback_error)}",
586
- status_code=status_code,
587
- )
588
-
589
- # Update secrets with new tokens (in-memory)
590
- self.secrets.update(result.tokens)
591
-
592
- # Update config_values and re-render base_url with extracted values
593
- if result.extracted_values:
594
- self._apply_token_extract(result.extracted_values)
595
-
596
- if self.secrets.get("access_token") != current_token:
597
- # Retry with new token - this will go through full retry logic
598
- return await self.request(
599
- method=method,
600
- path=path,
601
- params=params,
602
- json=json,
603
- data=data,
604
- headers=headers,
605
- )
606
-
607
577
  except Exception as refresh_error:
608
578
  self.logger.log_error(
609
579
  request_id=request_id,
610
580
  error=f"Credential refresh failed: {str(refresh_error)}",
611
581
  status_code=status_code,
612
582
  )
583
+ result = None
584
+
585
+ # If refresh succeeded, update tokens and retry
586
+ if result:
587
+ # Notify callback if provided (for persistence)
588
+ # Include both tokens AND extracted values for full persistence
589
+ if self.on_token_refresh is not None:
590
+ try:
591
+ # Build callback data with both tokens and extracted values
592
+ callback_data = dict(result.tokens)
593
+ if result.extracted_values:
594
+ callback_data.update(result.extracted_values)
613
595
 
596
+ # Support both sync and async callbacks
597
+ callback_result = self.on_token_refresh(callback_data)
598
+ if callback_result is not None and hasattr(callback_result, "__await__"):
599
+ await callback_result
600
+ except Exception as callback_error:
601
+ self.logger.log_error(
602
+ request_id=request_id,
603
+ error=f"Token refresh callback failed: {str(callback_error)}",
604
+ status_code=status_code,
605
+ )
606
+
607
+ # Update secrets with new tokens (in-memory)
608
+ self.secrets.update(result.tokens)
609
+
610
+ # Update config_values and re-render base_url with extracted values
611
+ if result.extracted_values:
612
+ self._apply_token_extract(result.extracted_values)
613
+
614
+ if self.secrets.get("access_token") != current_token:
615
+ # Retry with new token - this will go through full retry logic
616
+ # Any errors from this retry will propagate to the caller
617
+ return await self.request(
618
+ method=method,
619
+ path=path,
620
+ params=params,
621
+ json=json,
622
+ data=data,
623
+ headers=headers,
624
+ )
625
+
626
+ # Refresh failed or token didn't change, log and let original error propagate
614
627
  self.logger.log_error(request_id=request_id, error=str(error), status_code=status_code)
615
628
 
616
629
  async def request(
@@ -624,7 +637,7 @@ class HTTPClient:
624
637
  *,
625
638
  stream: bool = False,
626
639
  _auth_retry_attempted: bool = False,
627
- ):
640
+ ) -> tuple[dict[str, Any], dict[str, str]]:
628
641
  """Make an async HTTP request with optional streaming and automatic retries.
629
642
 
630
643
  Args:
@@ -637,8 +650,9 @@ class HTTPClient:
637
650
  stream: If True, do not eagerly read the body (useful for downloads)
638
651
 
639
652
  Returns:
640
- - If stream=False: Parsed JSON (dict) or empty dict
641
- - 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)
642
656
 
643
657
  Raises:
644
658
  HTTPStatusError: If request fails with 4xx/5xx status after all retries
@@ -0,0 +1,262 @@
1
+ """
2
+ Shared introspection utilities for connector metadata.
3
+
4
+ This module provides utilities for introspecting connector metadata,
5
+ generating descriptions, and formatting parameter signatures. These
6
+ functions are used by both the runtime decorators and the generated
7
+ connector code.
8
+
9
+ The module is designed to work with any object conforming to the
10
+ ConnectorModel and EndpointDefinition interfaces from connector_sdk.types.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Protocol
16
+
17
+ # Constants
18
+ MAX_EXAMPLE_QUESTIONS = 5 # Maximum number of example questions to include in description
19
+
20
+
21
+ class EndpointProtocol(Protocol):
22
+ """Protocol defining the expected interface for endpoint parameters.
23
+
24
+ This allows functions to work with any endpoint-like object
25
+ that has these attributes, including EndpointDefinition and mock objects.
26
+ """
27
+
28
+ path_params: list[str]
29
+ path_params_schema: dict[str, dict[str, Any]]
30
+ query_params: list[str]
31
+ query_params_schema: dict[str, dict[str, Any]]
32
+ body_fields: list[str]
33
+ request_schema: dict[str, Any] | None
34
+
35
+
36
+ class EntityProtocol(Protocol):
37
+ """Protocol defining the expected interface for entity definitions."""
38
+
39
+ name: str
40
+ actions: list[Any]
41
+ endpoints: dict[Any, EndpointProtocol]
42
+
43
+
44
+ class ConnectorModelProtocol(Protocol):
45
+ """Protocol defining the expected interface for connector model parameters.
46
+
47
+ This allows functions to work with any connector-like object
48
+ that has these attributes, including ConnectorModel and mock objects.
49
+ """
50
+
51
+ @property
52
+ def entities(self) -> list[EntityProtocol]: ...
53
+
54
+ @property
55
+ def openapi_spec(self) -> Any: ...
56
+
57
+
58
+ def format_param_signature(endpoint: EndpointProtocol) -> str:
59
+ """Format parameter signature for an endpoint action.
60
+
61
+ Returns a string like: (id*) or (limit?, starting_after?, email?)
62
+ where * = required, ? = optional
63
+
64
+ Args:
65
+ endpoint: Object conforming to EndpointProtocol (e.g., EndpointDefinition)
66
+
67
+ Returns:
68
+ Formatted parameter signature string
69
+ """
70
+ params = []
71
+
72
+ # Defensive: safely access attributes with defaults for malformed endpoints
73
+ path_params = getattr(endpoint, "path_params", []) or []
74
+ query_params = getattr(endpoint, "query_params", []) or []
75
+ query_params_schema = getattr(endpoint, "query_params_schema", {}) or {}
76
+ body_fields = getattr(endpoint, "body_fields", []) or []
77
+ request_schema = getattr(endpoint, "request_schema", None)
78
+
79
+ # Path params (always required)
80
+ for name in path_params:
81
+ params.append(f"{name}*")
82
+
83
+ # Query params
84
+ for name in query_params:
85
+ schema = query_params_schema.get(name, {})
86
+ required = schema.get("required", False)
87
+ params.append(f"{name}{'*' if required else '?'}")
88
+
89
+ # Body fields
90
+ if request_schema:
91
+ required_fields = set(request_schema.get("required", []))
92
+ for name in body_fields:
93
+ params.append(f"{name}{'*' if name in required_fields else '?'}")
94
+
95
+ return f"({', '.join(params)})" if params else "()"
96
+
97
+
98
+ def describe_entities(model: ConnectorModelProtocol) -> list[dict[str, Any]]:
99
+ """Generate entity descriptions from ConnectorModel.
100
+
101
+ Returns a list of entity descriptions with detailed parameter information
102
+ for each action. This is used by generated connectors' describe() method.
103
+
104
+ Args:
105
+ model: Object conforming to ConnectorModelProtocol (e.g., ConnectorModel)
106
+
107
+ Returns:
108
+ List of entity description dicts with keys:
109
+ - entity_name: Name of the entity (e.g., "contacts", "deals")
110
+ - description: Entity description from the first endpoint
111
+ - available_actions: List of actions (e.g., ["list", "get", "create"])
112
+ - parameters: Dict mapping action -> list of parameter dicts
113
+ """
114
+ entities = []
115
+ for entity_def in model.entities:
116
+ description = ""
117
+ parameters: dict[str, list[dict[str, Any]]] = {}
118
+
119
+ endpoints = getattr(entity_def, "endpoints", {}) or {}
120
+ if endpoints:
121
+ for action, endpoint in endpoints.items():
122
+ # Get description from first endpoint that has one
123
+ if not description:
124
+ endpoint_desc = getattr(endpoint, "description", None)
125
+ if endpoint_desc:
126
+ description = endpoint_desc
127
+
128
+ action_params: list[dict[str, Any]] = []
129
+
130
+ # Defensive: safely access endpoint attributes
131
+ path_params = getattr(endpoint, "path_params", []) or []
132
+ path_params_schema = getattr(endpoint, "path_params_schema", {}) or {}
133
+ query_params = getattr(endpoint, "query_params", []) or []
134
+ query_params_schema = getattr(endpoint, "query_params_schema", {}) or {}
135
+ body_fields = getattr(endpoint, "body_fields", []) or []
136
+ request_schema = getattr(endpoint, "request_schema", None)
137
+
138
+ # Path params (always required)
139
+ for param_name in path_params:
140
+ schema = path_params_schema.get(param_name, {})
141
+ action_params.append(
142
+ {
143
+ "name": param_name,
144
+ "in": "path",
145
+ "required": True,
146
+ "type": schema.get("type", "string"),
147
+ "description": schema.get("description", ""),
148
+ }
149
+ )
150
+
151
+ # Query params
152
+ for param_name in query_params:
153
+ schema = query_params_schema.get(param_name, {})
154
+ action_params.append(
155
+ {
156
+ "name": param_name,
157
+ "in": "query",
158
+ "required": schema.get("required", False),
159
+ "type": schema.get("type", "string"),
160
+ "description": schema.get("description", ""),
161
+ }
162
+ )
163
+
164
+ # Body fields
165
+ if request_schema:
166
+ required_fields = request_schema.get("required", [])
167
+ properties = request_schema.get("properties", {})
168
+ for param_name in body_fields:
169
+ prop = properties.get(param_name, {})
170
+ action_params.append(
171
+ {
172
+ "name": param_name,
173
+ "in": "body",
174
+ "required": param_name in required_fields,
175
+ "type": prop.get("type", "string"),
176
+ "description": prop.get("description", ""),
177
+ }
178
+ )
179
+
180
+ if action_params:
181
+ # Action is an enum, use .value to get string
182
+ action_key = action.value if hasattr(action, "value") else str(action)
183
+ parameters[action_key] = action_params
184
+
185
+ actions = getattr(entity_def, "actions", []) or []
186
+ entities.append(
187
+ {
188
+ "entity_name": entity_def.name,
189
+ "description": description,
190
+ "available_actions": [a.value if hasattr(a, "value") else str(a) for a in actions],
191
+ "parameters": parameters,
192
+ }
193
+ )
194
+
195
+ return entities
196
+
197
+
198
+ def generate_tool_description(model: ConnectorModelProtocol) -> str:
199
+ """Generate AI tool description from connector metadata.
200
+
201
+ Produces a detailed description that includes:
202
+ - Per-entity/action parameter signatures with required (*) and optional (?) markers
203
+ - Response structure documentation with pagination hints
204
+ - Example questions if available in the OpenAPI spec
205
+
206
+ This is used by the Connector.describe class method decorator to populate
207
+ function docstrings for AI framework integration.
208
+
209
+ Args:
210
+ model: Object conforming to ConnectorModelProtocol (e.g., ConnectorModel)
211
+
212
+ Returns:
213
+ Formatted description string suitable for AI tool documentation
214
+ """
215
+ lines = []
216
+
217
+ # Entity/action parameter details (including pagination params like limit, starting_after)
218
+ lines.append("ENTITIES AND PARAMETERS:")
219
+ for entity in model.entities:
220
+ lines.append(f" {entity.name}:")
221
+ actions = getattr(entity, "actions", []) or []
222
+ endpoints = getattr(entity, "endpoints", {}) or {}
223
+ for action in actions:
224
+ action_str = action.value if hasattr(action, "value") else str(action)
225
+ endpoint = endpoints.get(action)
226
+ if endpoint:
227
+ param_sig = format_param_signature(endpoint)
228
+ lines.append(f" - {action_str}{param_sig}")
229
+ else:
230
+ lines.append(f" - {action_str}()")
231
+
232
+ # Response structure (brief, includes pagination hint)
233
+ lines.append("")
234
+ lines.append("RESPONSE STRUCTURE:")
235
+ lines.append(" - list/api_search: {data: [...], meta: {has_more: bool}}")
236
+ lines.append(" - get: Returns entity directly (no envelope)")
237
+ lines.append(" To paginate: pass starting_after=<last_id> while has_more is true")
238
+
239
+ # Add example questions if available in openapi_spec
240
+ openapi_spec = getattr(model, "openapi_spec", None)
241
+ if openapi_spec:
242
+ info = getattr(openapi_spec, "info", None)
243
+ if info:
244
+ example_questions = getattr(info, "x_airbyte_example_questions", None)
245
+ if example_questions:
246
+ supported = getattr(example_questions, "supported", None)
247
+ if supported:
248
+ lines.append("")
249
+ lines.append("EXAMPLE QUESTIONS:")
250
+ for q in supported[:MAX_EXAMPLE_QUESTIONS]:
251
+ lines.append(f" - {q}")
252
+
253
+ # Generic parameter description for function signature
254
+ lines.append("")
255
+ lines.append("FUNCTION PARAMETERS:")
256
+ lines.append(" - entity: Entity name (string)")
257
+ lines.append(" - action: Operation to perform (string)")
258
+ lines.append(" - params: Operation parameters (dict) - see entity details above")
259
+ lines.append("")
260
+ lines.append("Parameter markers: * = required, ? = optional")
261
+
262
+ return "\n".join(lines)
@@ -5,7 +5,7 @@ import json
5
5
  import time
6
6
  import uuid
7
7
  from pathlib import Path
8
- from typing import Any, Dict, Optional, Set
8
+ from typing import Any, Dict, Set
9
9
 
10
10
  from .types import LogSession, RequestLog
11
11
 
@@ -31,9 +31,9 @@ class RequestLogger:
31
31
 
32
32
  def __init__(
33
33
  self,
34
- log_file: Optional[str] = None,
35
- connector_name: Optional[str] = None,
36
- max_logs: Optional[int] = 10000,
34
+ log_file: str | None = None,
35
+ connector_name: str | None = None,
36
+ max_logs: int | None = 10000,
37
37
  ):
38
38
  """
39
39
  Initialize the request logger.
@@ -99,9 +99,9 @@ class RequestLogger:
99
99
  method: str,
100
100
  url: str,
101
101
  path: str,
102
- headers: Optional[Dict[str, str]] = None,
103
- params: Optional[Dict[str, Any]] = None,
104
- body: Optional[Any] = None,
102
+ headers: Dict[str, str] | None = None,
103
+ params: Dict[str, Any] | None = None,
104
+ body: Any | None = None,
105
105
  ) -> str:
106
106
  """
107
107
  Log the start of an HTTP request.
@@ -133,7 +133,8 @@ class RequestLogger:
133
133
  self,
134
134
  request_id: str,
135
135
  status_code: int,
136
- response_body: Optional[Any] = None,
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
 
@@ -176,7 +179,7 @@ class RequestLogger:
176
179
  self,
177
180
  request_id: str,
178
181
  error: str,
179
- status_code: Optional[int] = None,
182
+ status_code: int | None = None,
180
183
  ) -> None:
181
184
  """
182
185
  Log an HTTP request error.
@@ -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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import base64
4
4
  from datetime import UTC, datetime
5
- from typing import Any, Dict, List, Optional
5
+ from typing import Any, Dict, List
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
8
8
 
@@ -27,12 +27,13 @@ class RequestLog(BaseModel):
27
27
  url: str
28
28
  path: str
29
29
  headers: Dict[str, str] = Field(default_factory=dict)
30
- params: Optional[Dict[str, Any]] = None
31
- body: Optional[Any] = None
32
- response_status: Optional[int] = None
33
- response_body: Optional[Any] = None
34
- timing_ms: Optional[float] = None
35
- error: Optional[str] = None
30
+ params: Dict[str, Any] | None = None
31
+ body: Any | None = None
32
+ response_status: int | None = None
33
+ response_body: Any | None = None
34
+ response_headers: Dict[str, str] = Field(default_factory=dict)
35
+ timing_ms: float | None = None
36
+ error: str | None = None
36
37
 
37
38
  @field_serializer("timestamp")
38
39
  def serialize_datetime(self, value: datetime) -> str:
@@ -50,9 +51,9 @@ class LogSession(BaseModel):
50
51
 
51
52
  session_id: str
52
53
  started_at: datetime = Field(default_factory=_utc_now)
53
- connector_name: Optional[str] = None
54
+ connector_name: str | None = None
54
55
  logs: List[RequestLog] = Field(default_factory=list)
55
- max_logs: Optional[int] = Field(
56
+ max_logs: int | None = Field(
56
57
  default=10000,
57
58
  description="Maximum number of logs to keep in memory. "
58
59
  "When limit is reached, oldest logs should be flushed before removal. "
@@ -60,7 +61,7 @@ class LogSession(BaseModel):
60
61
  )
61
62
  chunk_logs: List[bytes] = Field(
62
63
  default_factory=list,
63
- description="Captured chunks from streaming responses. " "Each chunk is logged when log_chunk_fetch() is called.",
64
+ description="Captured chunks from streaming responses. Each chunk is logged when log_chunk_fetch() is called.",
64
65
  )
65
66
 
66
67
  @field_validator("chunk_logs", mode="before")