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.
- airbyte_agent_zendesk_support/__init__.py +239 -18
- airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_strategies.py +2 -5
- airbyte_agent_zendesk_support/_vendored/connector_sdk/auth_template.py +1 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/connector_model_loader.py +35 -8
- airbyte_agent_zendesk_support/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/hosted_executor.py +92 -84
- airbyte_agent_zendesk_support/_vendored/connector_sdk/executor/local_executor.py +274 -54
- airbyte_agent_zendesk_support/_vendored/connector_sdk/extensions.py +43 -5
- airbyte_agent_zendesk_support/_vendored/connector_sdk/http/response.py +2 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/http_client.py +63 -49
- airbyte_agent_zendesk_support/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/logger.py +19 -10
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/types.py +11 -10
- airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/models.py +6 -6
- airbyte_agent_zendesk_support/_vendored/connector_sdk/observability/session.py +41 -32
- airbyte_agent_zendesk_support/_vendored/connector_sdk/performance/metrics.py +3 -3
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/base.py +22 -18
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/components.py +59 -58
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/connector.py +22 -33
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/extensions.py +131 -10
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/operations.py +32 -32
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/security.py +44 -34
- airbyte_agent_zendesk_support/_vendored/connector_sdk/secrets.py +2 -2
- airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/events.py +9 -8
- airbyte_agent_zendesk_support/_vendored/connector_sdk/telemetry/tracker.py +9 -5
- airbyte_agent_zendesk_support/_vendored/connector_sdk/types.py +9 -3
- airbyte_agent_zendesk_support/_vendored/connector_sdk/validation.py +12 -6
- airbyte_agent_zendesk_support/connector.py +1170 -171
- airbyte_agent_zendesk_support/connector_model.py +7 -1
- airbyte_agent_zendesk_support/models.py +628 -69
- airbyte_agent_zendesk_support/types.py +3717 -1
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/METADATA +55 -31
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.51.dist-info}/RECORD +37 -33
- {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:
|
|
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},
|
|
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
|
-
|
|
641
|
-
- If stream=
|
|
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,
|
|
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:
|
|
35
|
-
connector_name:
|
|
36
|
-
max_logs:
|
|
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:
|
|
103
|
-
params:
|
|
104
|
-
body:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
31
|
-
body:
|
|
32
|
-
response_status:
|
|
33
|
-
response_body:
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
54
|
+
connector_name: str | None = None
|
|
54
55
|
logs: List[RequestLog] = Field(default_factory=list)
|
|
55
|
-
max_logs:
|
|
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.
|
|
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")
|