airbyte-agent-asana 0.19.27__py3-none-any.whl → 0.19.41__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.
Potentially problematic release.
This version of airbyte-agent-asana might be problematic. Click here for more details.
- airbyte_agent_asana/__init__.py +6 -6
- airbyte_agent_asana/_vendored/connector_sdk/auth_strategies.py +2 -5
- airbyte_agent_asana/_vendored/connector_sdk/auth_template.py +1 -1
- airbyte_agent_asana/_vendored/connector_sdk/cloud_utils/client.py +26 -26
- airbyte_agent_asana/_vendored/connector_sdk/connector_model_loader.py +11 -4
- airbyte_agent_asana/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_asana/_vendored/connector_sdk/executor/hosted_executor.py +10 -11
- airbyte_agent_asana/_vendored/connector_sdk/executor/local_executor.py +94 -25
- airbyte_agent_asana/_vendored/connector_sdk/extensions.py +43 -5
- airbyte_agent_asana/_vendored/connector_sdk/http/response.py +2 -0
- airbyte_agent_asana/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_asana/_vendored/connector_sdk/logging/logger.py +9 -9
- airbyte_agent_asana/_vendored/connector_sdk/logging/types.py +10 -10
- airbyte_agent_asana/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_asana/_vendored/connector_sdk/observability/models.py +6 -6
- airbyte_agent_asana/_vendored/connector_sdk/observability/session.py +41 -32
- airbyte_agent_asana/_vendored/connector_sdk/performance/metrics.py +3 -3
- airbyte_agent_asana/_vendored/connector_sdk/schema/base.py +17 -17
- airbyte_agent_asana/_vendored/connector_sdk/schema/components.py +59 -58
- airbyte_agent_asana/_vendored/connector_sdk/schema/extensions.py +9 -9
- airbyte_agent_asana/_vendored/connector_sdk/schema/operations.py +32 -32
- airbyte_agent_asana/_vendored/connector_sdk/schema/security.py +44 -34
- airbyte_agent_asana/_vendored/connector_sdk/secrets.py +2 -2
- airbyte_agent_asana/_vendored/connector_sdk/telemetry/events.py +9 -8
- airbyte_agent_asana/_vendored/connector_sdk/telemetry/tracker.py +9 -5
- airbyte_agent_asana/_vendored/connector_sdk/types.py +7 -3
- airbyte_agent_asana/connector.py +88 -3
- airbyte_agent_asana/connector_model.py +6 -0
- airbyte_agent_asana/models.py +29 -29
- airbyte_agent_asana/types.py +1 -1
- {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/METADATA +11 -11
- {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/RECORD +33 -31
- {airbyte_agent_asana-0.19.27.dist-info → airbyte_agent_asana-0.19.41.dist-info}/WHEEL +0 -0
|
@@ -159,6 +159,38 @@ Example:
|
|
|
159
159
|
```
|
|
160
160
|
"""
|
|
161
161
|
|
|
162
|
+
AIRBYTE_STREAM_NAME = "x-airbyte-stream-name"
|
|
163
|
+
"""
|
|
164
|
+
Extension: x-airbyte-stream-name
|
|
165
|
+
Location: Schema object (in components.schemas)
|
|
166
|
+
Type: string
|
|
167
|
+
Required: No
|
|
168
|
+
|
|
169
|
+
Description:
|
|
170
|
+
Specifies the Airbyte stream name for cache lookup purposes. This maps the entity
|
|
171
|
+
to the corresponding Airbyte stream, enabling cache-based data retrieval. When
|
|
172
|
+
specified, the EntityDefinition.stream_name field will be populated with this value.
|
|
173
|
+
|
|
174
|
+
This extension is placed on Schema objects alongside x-airbyte-entity-name, following
|
|
175
|
+
the same pattern. The stream name is an entity-level property (not operation-level)
|
|
176
|
+
since an entity maps to exactly one Airbyte stream.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
```yaml
|
|
180
|
+
components:
|
|
181
|
+
schemas:
|
|
182
|
+
Customer:
|
|
183
|
+
type: object
|
|
184
|
+
x-airbyte-entity-name: customers
|
|
185
|
+
x-airbyte-stream-name: customers
|
|
186
|
+
properties:
|
|
187
|
+
id:
|
|
188
|
+
type: string
|
|
189
|
+
name:
|
|
190
|
+
type: string
|
|
191
|
+
```
|
|
192
|
+
"""
|
|
193
|
+
|
|
162
194
|
AIRBYTE_TOKEN_PATH = "x-airbyte-token-path"
|
|
163
195
|
"""
|
|
164
196
|
Extension: x-airbyte-token-path
|
|
@@ -495,8 +527,8 @@ class ActionType(str, Enum):
|
|
|
495
527
|
DELETE = "delete"
|
|
496
528
|
"""Delete a record"""
|
|
497
529
|
|
|
498
|
-
|
|
499
|
-
"""Search for records matching specific query criteria"""
|
|
530
|
+
API_SEARCH = "api_search"
|
|
531
|
+
"""Search for records matching specific query criteria via API"""
|
|
500
532
|
|
|
501
533
|
DOWNLOAD = "download"
|
|
502
534
|
"""Download file content from a URL specified in the metadata response"""
|
|
@@ -514,7 +546,7 @@ class BodyType(str, Enum):
|
|
|
514
546
|
|
|
515
547
|
|
|
516
548
|
# Type alias for use in Pydantic models
|
|
517
|
-
ActionTypeLiteral = Literal["get", "list", "create", "update", "delete", "
|
|
549
|
+
ActionTypeLiteral = Literal["get", "list", "create", "update", "delete", "api_search", "download"]
|
|
518
550
|
|
|
519
551
|
|
|
520
552
|
# =============================================================================
|
|
@@ -548,6 +580,7 @@ def get_all_extension_names() -> list[str]:
|
|
|
548
580
|
AIRBYTE_ENTITY,
|
|
549
581
|
AIRBYTE_ACTION,
|
|
550
582
|
AIRBYTE_ENTITY_NAME,
|
|
583
|
+
AIRBYTE_STREAM_NAME,
|
|
551
584
|
AIRBYTE_TOKEN_PATH,
|
|
552
585
|
AIRBYTE_BODY_TYPE,
|
|
553
586
|
AIRBYTE_PATH_OVERRIDE,
|
|
@@ -594,6 +627,12 @@ EXTENSION_REGISTRY = {
|
|
|
594
627
|
"required": False,
|
|
595
628
|
"description": "Links schema to an entity/stream",
|
|
596
629
|
},
|
|
630
|
+
AIRBYTE_STREAM_NAME: {
|
|
631
|
+
"location": "schema",
|
|
632
|
+
"type": "string",
|
|
633
|
+
"required": False,
|
|
634
|
+
"description": "Maps entity to Airbyte stream for cache lookup",
|
|
635
|
+
},
|
|
597
636
|
AIRBYTE_TOKEN_PATH: {
|
|
598
637
|
"location": "securityScheme",
|
|
599
638
|
"type": "string",
|
|
@@ -627,8 +666,7 @@ EXTENSION_REGISTRY = {
|
|
|
627
666
|
"type": "dict[str, str]",
|
|
628
667
|
"required": False,
|
|
629
668
|
"description": (
|
|
630
|
-
"Dictionary mapping field names to JSONPath expressions for extracting metadata "
|
|
631
|
-
"(pagination, request IDs, etc.) from response envelopes"
|
|
669
|
+
"Dictionary mapping field names to JSONPath expressions for extracting metadata (pagination, request IDs, etc.) from response envelopes"
|
|
632
670
|
),
|
|
633
671
|
},
|
|
634
672
|
AIRBYTE_FILE_URL: {
|
|
@@ -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(
|
|
@@ -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,7 @@ 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
137
|
) -> None:
|
|
138
138
|
"""
|
|
139
139
|
Log a successful HTTP response.
|
|
@@ -176,7 +176,7 @@ class RequestLogger:
|
|
|
176
176
|
self,
|
|
177
177
|
request_id: str,
|
|
178
178
|
error: str,
|
|
179
|
-
status_code:
|
|
179
|
+
status_code: int | None = None,
|
|
180
180
|
) -> None:
|
|
181
181
|
"""
|
|
182
182
|
Log an HTTP request error.
|
|
@@ -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,12 @@ 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
|
-
timing_ms:
|
|
35
|
-
error:
|
|
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
|
+
timing_ms: float | None = None
|
|
35
|
+
error: str | None = None
|
|
36
36
|
|
|
37
37
|
@field_serializer("timestamp")
|
|
38
38
|
def serialize_datetime(self, value: datetime) -> str:
|
|
@@ -50,9 +50,9 @@ class LogSession(BaseModel):
|
|
|
50
50
|
|
|
51
51
|
session_id: str
|
|
52
52
|
started_at: datetime = Field(default_factory=_utc_now)
|
|
53
|
-
connector_name:
|
|
53
|
+
connector_name: str | None = None
|
|
54
54
|
logs: List[RequestLog] = Field(default_factory=list)
|
|
55
|
-
max_logs:
|
|
55
|
+
max_logs: int | None = Field(
|
|
56
56
|
default=10000,
|
|
57
57
|
description="Maximum number of logs to keep in memory. "
|
|
58
58
|
"When limit is reached, oldest logs should be flushed before removal. "
|
|
@@ -60,7 +60,7 @@ class LogSession(BaseModel):
|
|
|
60
60
|
)
|
|
61
61
|
chunk_logs: List[bytes] = Field(
|
|
62
62
|
default_factory=list,
|
|
63
|
-
description="Captured chunks from streaming responses.
|
|
63
|
+
description="Captured chunks from streaming responses. Each chunk is logged when log_chunk_fetch() is called.",
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
@field_validator("chunk_logs", mode="before")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Unified configuration for connector-sdk."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# New config location
|
|
16
|
+
CONFIG_DIR = Path.home() / ".airbyte" / "connector-sdk"
|
|
17
|
+
CONFIG_PATH = CONFIG_DIR / "config.yaml"
|
|
18
|
+
|
|
19
|
+
# Legacy file locations (for migration)
|
|
20
|
+
LEGACY_USER_ID_PATH = Path.home() / ".airbyte" / "ai_sdk_user_id"
|
|
21
|
+
LEGACY_INTERNAL_MARKER_PATH = Path.home() / ".airbyte" / "internal_user"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SDKConfig:
|
|
26
|
+
"""Connector SDK configuration."""
|
|
27
|
+
|
|
28
|
+
user_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
29
|
+
is_internal_user: bool = False
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
|
32
|
+
"""Convert to dictionary for YAML serialization."""
|
|
33
|
+
return {
|
|
34
|
+
"user_id": self.user_id,
|
|
35
|
+
"is_internal_user": self.is_internal_user,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _delete_legacy_files() -> None:
|
|
40
|
+
"""
|
|
41
|
+
Delete legacy config files after successful migration.
|
|
42
|
+
|
|
43
|
+
Removes:
|
|
44
|
+
- ~/.airbyte/ai_sdk_user_id
|
|
45
|
+
- ~/.airbyte/internal_user
|
|
46
|
+
"""
|
|
47
|
+
for legacy_path in [LEGACY_USER_ID_PATH, LEGACY_INTERNAL_MARKER_PATH]:
|
|
48
|
+
try:
|
|
49
|
+
if legacy_path.exists():
|
|
50
|
+
legacy_path.unlink()
|
|
51
|
+
logger.debug(f"Deleted legacy config file: {legacy_path}")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.debug(f"Could not delete legacy file {legacy_path}: {e}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _migrate_legacy_config() -> SDKConfig | None:
|
|
57
|
+
"""
|
|
58
|
+
Migrate from legacy file-based config to new YAML format.
|
|
59
|
+
|
|
60
|
+
Reads from:
|
|
61
|
+
- ~/.airbyte/ai_sdk_user_id (user_id)
|
|
62
|
+
- ~/.airbyte/internal_user (is_internal_user marker)
|
|
63
|
+
|
|
64
|
+
Returns SDKConfig if migration was successful, None otherwise.
|
|
65
|
+
"""
|
|
66
|
+
user_id = None
|
|
67
|
+
is_internal = False
|
|
68
|
+
|
|
69
|
+
# Try to read legacy user_id
|
|
70
|
+
try:
|
|
71
|
+
if LEGACY_USER_ID_PATH.exists():
|
|
72
|
+
user_id = LEGACY_USER_ID_PATH.read_text().strip()
|
|
73
|
+
if not user_id:
|
|
74
|
+
user_id = None
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Check legacy internal_user marker
|
|
79
|
+
try:
|
|
80
|
+
is_internal = LEGACY_INTERNAL_MARKER_PATH.exists()
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
if user_id or is_internal:
|
|
85
|
+
return SDKConfig(
|
|
86
|
+
user_id=user_id or str(uuid.uuid4()),
|
|
87
|
+
is_internal_user=is_internal,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_config() -> SDKConfig:
|
|
94
|
+
"""
|
|
95
|
+
Load SDK configuration from config file.
|
|
96
|
+
|
|
97
|
+
Checks (in order):
|
|
98
|
+
1. New config file at ~/.airbyte/connector-sdk/config.yaml
|
|
99
|
+
2. Legacy files at ~/.airbyte/ai_sdk_user_id and ~/.airbyte/internal_user
|
|
100
|
+
3. Creates new config with generated user_id if nothing exists
|
|
101
|
+
|
|
102
|
+
Environment variable AIRBYTE_INTERNAL_USER can override is_internal_user.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
SDKConfig with user_id and is_internal_user
|
|
106
|
+
"""
|
|
107
|
+
config = None
|
|
108
|
+
|
|
109
|
+
# Try to load from new config file
|
|
110
|
+
try:
|
|
111
|
+
if CONFIG_PATH.exists():
|
|
112
|
+
content = CONFIG_PATH.read_text()
|
|
113
|
+
data = yaml.safe_load(content) or {}
|
|
114
|
+
config = SDKConfig(
|
|
115
|
+
user_id=data.get("user_id", str(uuid.uuid4())),
|
|
116
|
+
is_internal_user=data.get("is_internal_user", False),
|
|
117
|
+
)
|
|
118
|
+
# Always clean up legacy files if they exist (even if new config exists)
|
|
119
|
+
_delete_legacy_files()
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.debug(f"Could not load config from {CONFIG_PATH}: {e}")
|
|
122
|
+
|
|
123
|
+
# Try to migrate from legacy files if new config doesn't exist
|
|
124
|
+
if config is None:
|
|
125
|
+
config = _migrate_legacy_config()
|
|
126
|
+
if config:
|
|
127
|
+
# Save migrated config to new location
|
|
128
|
+
try:
|
|
129
|
+
save_config(config)
|
|
130
|
+
logger.debug("Migrated legacy config to new location")
|
|
131
|
+
# Delete legacy files after successful migration
|
|
132
|
+
_delete_legacy_files()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.debug(f"Could not save migrated config: {e}")
|
|
135
|
+
|
|
136
|
+
# Create new config if nothing exists
|
|
137
|
+
if config is None:
|
|
138
|
+
config = SDKConfig()
|
|
139
|
+
try:
|
|
140
|
+
save_config(config)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.debug(f"Could not save new config: {e}")
|
|
143
|
+
|
|
144
|
+
# Environment variable override for is_internal_user
|
|
145
|
+
env_value = os.getenv("AIRBYTE_INTERNAL_USER", "").lower()
|
|
146
|
+
if env_value in ("true", "1", "yes"):
|
|
147
|
+
config.is_internal_user = True
|
|
148
|
+
elif env_value:
|
|
149
|
+
# Any other non-empty value (including "false", "0", "no") defaults to False
|
|
150
|
+
config.is_internal_user = False
|
|
151
|
+
|
|
152
|
+
return config
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def save_config(config: SDKConfig) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Save SDK configuration to config file.
|
|
158
|
+
|
|
159
|
+
Creates the config directory if it doesn't exist.
|
|
160
|
+
Uses atomic writes to prevent corruption from concurrent access.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config: SDKConfig to save
|
|
164
|
+
"""
|
|
165
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
# Use atomic write: write to temp file then rename (atomic on POSIX)
|
|
168
|
+
fd, temp_path = tempfile.mkstemp(dir=CONFIG_DIR, suffix=".tmp")
|
|
169
|
+
try:
|
|
170
|
+
with os.fdopen(fd, "w") as f:
|
|
171
|
+
yaml.dump(config.to_dict(), f, default_flow_style=False)
|
|
172
|
+
os.rename(temp_path, CONFIG_PATH)
|
|
173
|
+
except Exception:
|
|
174
|
+
# Clean up temp file on failure
|
|
175
|
+
try:
|
|
176
|
+
os.unlink(temp_path)
|
|
177
|
+
except OSError:
|
|
178
|
+
pass
|
|
179
|
+
raise
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import Any, Dict
|
|
5
|
+
from typing import Any, Dict
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
@@ -12,8 +12,8 @@ class OperationMetadata:
|
|
|
12
12
|
entity: str
|
|
13
13
|
action: str
|
|
14
14
|
timestamp: datetime
|
|
15
|
-
timing_ms:
|
|
16
|
-
status_code:
|
|
17
|
-
error_type:
|
|
18
|
-
error_message:
|
|
19
|
-
params:
|
|
15
|
+
timing_ms: float | None = None
|
|
16
|
+
status_code: int | None = None
|
|
17
|
+
error_type: str | None = None
|
|
18
|
+
error_message: str | None = None
|
|
19
|
+
params: Dict[str, Any] | None = None
|