airbyte-agent-hubspot 0.15.28__py3-none-any.whl → 0.15.43__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_hubspot/__init__.py +101 -26
- airbyte_agent_hubspot/_vendored/connector_sdk/auth_strategies.py +2 -5
- airbyte_agent_hubspot/_vendored/connector_sdk/auth_template.py +1 -1
- airbyte_agent_hubspot/_vendored/connector_sdk/cloud_utils/client.py +26 -26
- airbyte_agent_hubspot/_vendored/connector_sdk/connector_model_loader.py +11 -4
- airbyte_agent_hubspot/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_hubspot/_vendored/connector_sdk/executor/hosted_executor.py +10 -11
- airbyte_agent_hubspot/_vendored/connector_sdk/executor/local_executor.py +126 -17
- airbyte_agent_hubspot/_vendored/connector_sdk/extensions.py +43 -5
- airbyte_agent_hubspot/_vendored/connector_sdk/http/response.py +2 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/logging/logger.py +9 -9
- airbyte_agent_hubspot/_vendored/connector_sdk/logging/types.py +10 -10
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/models.py +6 -6
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/session.py +41 -32
- airbyte_agent_hubspot/_vendored/connector_sdk/performance/metrics.py +3 -3
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/base.py +20 -18
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/components.py +59 -58
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/connector.py +22 -33
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/extensions.py +103 -10
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/operations.py +32 -32
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/security.py +44 -34
- airbyte_agent_hubspot/_vendored/connector_sdk/secrets.py +2 -2
- airbyte_agent_hubspot/_vendored/connector_sdk/telemetry/events.py +9 -8
- airbyte_agent_hubspot/_vendored/connector_sdk/telemetry/tracker.py +9 -5
- airbyte_agent_hubspot/_vendored/connector_sdk/types.py +7 -3
- airbyte_agent_hubspot/connector.py +182 -87
- airbyte_agent_hubspot/connector_model.py +17 -12
- airbyte_agent_hubspot/models.py +28 -28
- airbyte_agent_hubspot/types.py +45 -45
- {airbyte_agent_hubspot-0.15.28.dist-info → airbyte_agent_hubspot-0.15.43.dist-info}/METADATA +16 -17
- {airbyte_agent_hubspot-0.15.28.dist-info → airbyte_agent_hubspot-0.15.43.dist-info}/RECORD +34 -32
- {airbyte_agent_hubspot-0.15.28.dist-info → airbyte_agent_hubspot-0.15.43.dist-info}/WHEEL +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import inspect
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
@@ -11,6 +12,7 @@ from collections.abc import AsyncIterator
|
|
|
11
12
|
from typing import Any, Protocol
|
|
12
13
|
from urllib.parse import quote
|
|
13
14
|
|
|
15
|
+
from jinja2 import Environment, StrictUndefined
|
|
14
16
|
from jsonpath_ng import parse as parse_jsonpath
|
|
15
17
|
from opentelemetry import trace
|
|
16
18
|
|
|
@@ -506,8 +508,6 @@ class LocalExecutor:
|
|
|
506
508
|
result = handler.execute_operation(config.entity, action, params)
|
|
507
509
|
|
|
508
510
|
# Check if it's an async generator (download) or awaitable (standard)
|
|
509
|
-
import inspect
|
|
510
|
-
|
|
511
511
|
if inspect.isasyncgen(result):
|
|
512
512
|
# Download operation: return generator directly
|
|
513
513
|
return ExecutionResult(
|
|
@@ -814,7 +814,6 @@ class LocalExecutor:
|
|
|
814
814
|
>>> _substitute_file_field_params("attachments[{attachment_index}].url", {"attachment_index": 0})
|
|
815
815
|
"attachments[0].url"
|
|
816
816
|
"""
|
|
817
|
-
from jinja2 import Environment, StrictUndefined
|
|
818
817
|
|
|
819
818
|
# Use custom delimiters to match OpenAPI path parameter syntax {var}
|
|
820
819
|
# StrictUndefined raises clear error if a template variable is missing
|
|
@@ -844,10 +843,58 @@ class LocalExecutor:
|
|
|
844
843
|
return self._extract_body(endpoint.body_fields, params)
|
|
845
844
|
return None
|
|
846
845
|
|
|
846
|
+
def _flatten_form_data(self, data: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
|
|
847
|
+
"""Flatten nested dict/list structures into bracket notation for form encoding.
|
|
848
|
+
|
|
849
|
+
Stripe and similar APIs require nested arrays/objects to be encoded using bracket
|
|
850
|
+
notation when using application/x-www-form-urlencoded content type.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
data: Nested dict with arrays/objects to flatten
|
|
854
|
+
parent_key: Parent key for nested structures (used in recursion)
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
Flattened dict with bracket notation keys
|
|
858
|
+
|
|
859
|
+
Examples:
|
|
860
|
+
>>> _flatten_form_data({"items": [{"price": "p1", "qty": 1}]})
|
|
861
|
+
{"items[0][price]": "p1", "items[0][qty]": 1}
|
|
862
|
+
|
|
863
|
+
>>> _flatten_form_data({"customer": "cus_123", "metadata": {"key": "value"}})
|
|
864
|
+
{"customer": "cus_123", "metadata[key]": "value"}
|
|
865
|
+
"""
|
|
866
|
+
flattened = {}
|
|
867
|
+
|
|
868
|
+
for key, value in data.items():
|
|
869
|
+
new_key = f"{parent_key}[{key}]" if parent_key else key
|
|
870
|
+
|
|
871
|
+
if isinstance(value, dict):
|
|
872
|
+
# Recursively flatten nested dicts
|
|
873
|
+
flattened.update(self._flatten_form_data(value, new_key))
|
|
874
|
+
elif isinstance(value, list):
|
|
875
|
+
# Flatten arrays with indexed bracket notation
|
|
876
|
+
for i, item in enumerate(value):
|
|
877
|
+
indexed_key = f"{new_key}[{i}]"
|
|
878
|
+
if isinstance(item, dict):
|
|
879
|
+
# Nested dict in array - recurse
|
|
880
|
+
flattened.update(self._flatten_form_data(item, indexed_key))
|
|
881
|
+
elif isinstance(item, list):
|
|
882
|
+
# Nested list in array - recurse
|
|
883
|
+
flattened.update(self._flatten_form_data({str(i): item}, new_key))
|
|
884
|
+
else:
|
|
885
|
+
# Primitive value in array
|
|
886
|
+
flattened[indexed_key] = item
|
|
887
|
+
else:
|
|
888
|
+
# Primitive value - add directly
|
|
889
|
+
flattened[new_key] = value
|
|
890
|
+
|
|
891
|
+
return flattened
|
|
892
|
+
|
|
847
893
|
def _determine_request_format(self, endpoint: EndpointDefinition, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
848
894
|
"""Determine json/data parameters for HTTP request.
|
|
849
895
|
|
|
850
896
|
GraphQL always uses JSON, regardless of content_type setting.
|
|
897
|
+
For form-encoded requests, nested structures are flattened into bracket notation.
|
|
851
898
|
|
|
852
899
|
Args:
|
|
853
900
|
endpoint: Endpoint definition
|
|
@@ -864,7 +911,9 @@ class LocalExecutor:
|
|
|
864
911
|
if is_graphql or endpoint.content_type.value == "application/json":
|
|
865
912
|
return {"json": body}
|
|
866
913
|
elif endpoint.content_type.value == "application/x-www-form-urlencoded":
|
|
867
|
-
|
|
914
|
+
# Flatten nested structures for form encoding
|
|
915
|
+
flattened_body = self._flatten_form_data(body)
|
|
916
|
+
return {"data": flattened_body}
|
|
868
917
|
|
|
869
918
|
return {}
|
|
870
919
|
|
|
@@ -1049,37 +1098,95 @@ class LocalExecutor:
|
|
|
1049
1098
|
|
|
1050
1099
|
return interpolate_value(variables)
|
|
1051
1100
|
|
|
1101
|
+
def _wrap_primitives(self, data: Any) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
1102
|
+
"""Wrap primitive values in dict format for consistent response structure.
|
|
1103
|
+
|
|
1104
|
+
Transforms primitive API responses into dict format so downstream code
|
|
1105
|
+
can always expect dict-based data structures.
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
data: Response data (could be primitive, list, dict, or None)
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
- If data is a primitive (str, int, float, bool): {"value": data}
|
|
1112
|
+
- If data is a list: wraps all non-dict elements as {"value": item}
|
|
1113
|
+
- If data is already a dict or list of dicts: unchanged
|
|
1114
|
+
- If data is None: None
|
|
1115
|
+
|
|
1116
|
+
Examples:
|
|
1117
|
+
>>> executor._wrap_primitives(42)
|
|
1118
|
+
{"value": 42}
|
|
1119
|
+
>>> executor._wrap_primitives([1, 2, 3])
|
|
1120
|
+
[{"value": 1}, {"value": 2}, {"value": 3}]
|
|
1121
|
+
>>> executor._wrap_primitives([1, {"id": 2}, 3])
|
|
1122
|
+
[{"value": 1}, {"id": 2}, {"value": 3}]
|
|
1123
|
+
>>> executor._wrap_primitives([[1, 2], 3])
|
|
1124
|
+
[{"value": [1, 2]}, {"value": 3}]
|
|
1125
|
+
>>> executor._wrap_primitives({"id": 1})
|
|
1126
|
+
{"id": 1} # unchanged
|
|
1127
|
+
"""
|
|
1128
|
+
if data is None:
|
|
1129
|
+
return None
|
|
1130
|
+
|
|
1131
|
+
# Handle primitive scalars
|
|
1132
|
+
if isinstance(data, (bool, str, int, float)):
|
|
1133
|
+
return {"value": data}
|
|
1134
|
+
|
|
1135
|
+
# Handle lists - wrap non-dict elements
|
|
1136
|
+
if isinstance(data, list):
|
|
1137
|
+
if not data:
|
|
1138
|
+
return [] # Empty list unchanged
|
|
1139
|
+
|
|
1140
|
+
wrapped = []
|
|
1141
|
+
for item in data:
|
|
1142
|
+
if isinstance(item, dict):
|
|
1143
|
+
wrapped.append(item)
|
|
1144
|
+
else:
|
|
1145
|
+
wrapped.append({"value": item})
|
|
1146
|
+
return wrapped
|
|
1147
|
+
|
|
1148
|
+
# Dict - return unchanged
|
|
1149
|
+
if isinstance(data, dict):
|
|
1150
|
+
return data
|
|
1151
|
+
|
|
1152
|
+
# Unknown type - wrap for safety
|
|
1153
|
+
return {"value": data}
|
|
1154
|
+
|
|
1052
1155
|
def _extract_records(
|
|
1053
1156
|
self,
|
|
1054
|
-
response_data:
|
|
1157
|
+
response_data: Any,
|
|
1055
1158
|
endpoint: EndpointDefinition,
|
|
1056
|
-
) -> dict[str, Any] | list[Any] | None:
|
|
1159
|
+
) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
1057
1160
|
"""Extract records from response using record extractor.
|
|
1058
1161
|
|
|
1059
1162
|
Type inference based on action:
|
|
1060
1163
|
- list, search: Returns array ([] if not found)
|
|
1061
1164
|
- get, create, update, delete: Returns single record (None if not found)
|
|
1062
1165
|
|
|
1166
|
+
Automatically wraps primitive values (int, str, float, bool) in {"value": primitive}
|
|
1167
|
+
format to ensure consistent dict-based responses for downstream code.
|
|
1168
|
+
|
|
1063
1169
|
Args:
|
|
1064
|
-
response_data: Full API response
|
|
1170
|
+
response_data: Full API response (can be dict, list, primitive, or None)
|
|
1065
1171
|
endpoint: Endpoint with optional record extractor and action
|
|
1066
1172
|
|
|
1067
1173
|
Returns:
|
|
1068
1174
|
- Extracted data if extractor configured and path found
|
|
1069
1175
|
- [] or None if path not found (based on action)
|
|
1070
1176
|
- Original response if no extractor configured or on error
|
|
1177
|
+
- Primitives are wrapped as {"value": primitive}
|
|
1071
1178
|
"""
|
|
1072
1179
|
# Check if endpoint has record extractor
|
|
1073
1180
|
extractor = endpoint.record_extractor
|
|
1074
1181
|
if not extractor:
|
|
1075
|
-
return response_data
|
|
1182
|
+
return self._wrap_primitives(response_data)
|
|
1076
1183
|
|
|
1077
1184
|
# Determine if this action returns array or single record
|
|
1078
1185
|
action = endpoint.action
|
|
1079
1186
|
if not action:
|
|
1080
|
-
return response_data
|
|
1187
|
+
return self._wrap_primitives(response_data)
|
|
1081
1188
|
|
|
1082
|
-
is_array_action = action in (Action.LIST, Action.
|
|
1189
|
+
is_array_action = action in (Action.LIST, Action.API_SEARCH)
|
|
1083
1190
|
|
|
1084
1191
|
try:
|
|
1085
1192
|
# Parse and apply JSONPath expression
|
|
@@ -1090,17 +1197,19 @@ class LocalExecutor:
|
|
|
1090
1197
|
# Path not found - return empty based on action
|
|
1091
1198
|
return [] if is_array_action else None
|
|
1092
1199
|
|
|
1093
|
-
# Return extracted data
|
|
1200
|
+
# Return extracted data with primitive wrapping
|
|
1094
1201
|
if is_array_action:
|
|
1095
1202
|
# For array actions, return the array (or list of matches)
|
|
1096
|
-
|
|
1203
|
+
result = matches[0] if len(matches) == 1 else matches
|
|
1097
1204
|
else:
|
|
1098
1205
|
# For single record actions, return first match
|
|
1099
|
-
|
|
1206
|
+
result = matches[0]
|
|
1207
|
+
|
|
1208
|
+
return self._wrap_primitives(result)
|
|
1100
1209
|
|
|
1101
1210
|
except Exception as e:
|
|
1102
1211
|
logging.warning(f"Failed to apply record extractor '{extractor}': {e}. Returning original response.")
|
|
1103
|
-
return response_data
|
|
1212
|
+
return self._wrap_primitives(response_data)
|
|
1104
1213
|
|
|
1105
1214
|
def _extract_metadata(
|
|
1106
1215
|
self,
|
|
@@ -1185,7 +1294,7 @@ class LocalExecutor:
|
|
|
1185
1294
|
|
|
1186
1295
|
if missing_fields:
|
|
1187
1296
|
raise MissingParameterError(
|
|
1188
|
-
f"Missing required body fields for {entity}.{action.value}: {missing_fields}.
|
|
1297
|
+
f"Missing required body fields for {entity}.{action.value}: {missing_fields}. Provided parameters: {list(params.keys())}"
|
|
1189
1298
|
)
|
|
1190
1299
|
|
|
1191
1300
|
async def close(self):
|
|
@@ -1209,7 +1318,7 @@ class LocalExecutor:
|
|
|
1209
1318
|
|
|
1210
1319
|
|
|
1211
1320
|
class _StandardOperationHandler:
|
|
1212
|
-
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE,
|
|
1321
|
+
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE, API_SEARCH, AUTHORIZE)."""
|
|
1213
1322
|
|
|
1214
1323
|
def __init__(self, context: _OperationContext):
|
|
1215
1324
|
self.ctx = context
|
|
@@ -1222,7 +1331,7 @@ class _StandardOperationHandler:
|
|
|
1222
1331
|
Action.CREATE,
|
|
1223
1332
|
Action.UPDATE,
|
|
1224
1333
|
Action.DELETE,
|
|
1225
|
-
Action.
|
|
1334
|
+
Action.API_SEARCH,
|
|
1226
1335
|
Action.AUTHORIZE,
|
|
1227
1336
|
}
|
|
1228
1337
|
|
|
@@ -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.
|