airbyte-agent-mcp 0.1.30__py3-none-any.whl → 0.1.53__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_mcp/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_mcp/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_mcp/_vendored/connector_sdk/connector_model_loader.py +31 -4
- airbyte_agent_mcp/_vendored/connector_sdk/constants.py +1 -1
- airbyte_agent_mcp/_vendored/connector_sdk/executor/hosted_executor.py +93 -84
- airbyte_agent_mcp/_vendored/connector_sdk/executor/local_executor.py +93 -23
- airbyte_agent_mcp/_vendored/connector_sdk/extensions.py +42 -3
- airbyte_agent_mcp/_vendored/connector_sdk/http_client.py +50 -43
- airbyte_agent_mcp/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_mcp/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_mcp/_vendored/connector_sdk/observability/session.py +35 -28
- airbyte_agent_mcp/_vendored/connector_sdk/schema/base.py +2 -1
- airbyte_agent_mcp/_vendored/connector_sdk/schema/components.py +2 -1
- airbyte_agent_mcp/_vendored/connector_sdk/schema/operations.py +1 -1
- airbyte_agent_mcp/_vendored/connector_sdk/schema/security.py +10 -0
- airbyte_agent_mcp/_vendored/connector_sdk/telemetry/events.py +2 -1
- airbyte_agent_mcp/_vendored/connector_sdk/telemetry/tracker.py +3 -0
- airbyte_agent_mcp/_vendored/connector_sdk/types.py +7 -1
- airbyte_agent_mcp/config.py +1 -1
- {airbyte_agent_mcp-0.1.30.dist-info → airbyte_agent_mcp-0.1.53.dist-info}/METADATA +1 -1
- {airbyte_agent_mcp-0.1.30.dist-info → airbyte_agent_mcp-0.1.53.dist-info}/RECORD +22 -18
- {airbyte_agent_mcp-0.1.30.dist-info → airbyte_agent_mcp-0.1.53.dist-info}/WHEEL +0 -0
|
@@ -674,16 +674,16 @@ class LocalExecutor:
|
|
|
674
674
|
return {key: value for key, value in params.items() if key in allowed_params}
|
|
675
675
|
|
|
676
676
|
def _extract_body(self, allowed_fields: list[str], params: dict[str, Any]) -> dict[str, Any]:
|
|
677
|
-
"""Extract body fields from params.
|
|
677
|
+
"""Extract body fields from params, filtering out None values.
|
|
678
678
|
|
|
679
679
|
Args:
|
|
680
680
|
allowed_fields: List of allowed body field names
|
|
681
681
|
params: All parameters
|
|
682
682
|
|
|
683
683
|
Returns:
|
|
684
|
-
Dictionary of body fields
|
|
684
|
+
Dictionary of body fields with None values filtered out
|
|
685
685
|
"""
|
|
686
|
-
return {key: value for key, value in params.items() if key in allowed_fields}
|
|
686
|
+
return {key: value for key, value in params.items() if key in allowed_fields and value is not None}
|
|
687
687
|
|
|
688
688
|
def _serialize_deep_object_params(self, params: dict[str, Any], deep_object_param_names: list[str]) -> dict[str, Any]:
|
|
689
689
|
"""Serialize deepObject parameters to bracket notation format.
|
|
@@ -837,15 +837,65 @@ class LocalExecutor:
|
|
|
837
837
|
Request body dict or None if no body needed
|
|
838
838
|
"""
|
|
839
839
|
if endpoint.graphql_body:
|
|
840
|
-
|
|
840
|
+
# Extract defaults from query_params_schema for GraphQL variable interpolation
|
|
841
|
+
param_defaults = {name: schema.get("default") for name, schema in endpoint.query_params_schema.items() if "default" in schema}
|
|
842
|
+
return self._build_graphql_body(endpoint.graphql_body, params, param_defaults)
|
|
841
843
|
elif endpoint.body_fields:
|
|
842
844
|
return self._extract_body(endpoint.body_fields, params)
|
|
843
845
|
return None
|
|
844
846
|
|
|
847
|
+
def _flatten_form_data(self, data: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
|
|
848
|
+
"""Flatten nested dict/list structures into bracket notation for form encoding.
|
|
849
|
+
|
|
850
|
+
Stripe and similar APIs require nested arrays/objects to be encoded using bracket
|
|
851
|
+
notation when using application/x-www-form-urlencoded content type.
|
|
852
|
+
|
|
853
|
+
Args:
|
|
854
|
+
data: Nested dict with arrays/objects to flatten
|
|
855
|
+
parent_key: Parent key for nested structures (used in recursion)
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
Flattened dict with bracket notation keys
|
|
859
|
+
|
|
860
|
+
Examples:
|
|
861
|
+
>>> _flatten_form_data({"items": [{"price": "p1", "qty": 1}]})
|
|
862
|
+
{"items[0][price]": "p1", "items[0][qty]": 1}
|
|
863
|
+
|
|
864
|
+
>>> _flatten_form_data({"customer": "cus_123", "metadata": {"key": "value"}})
|
|
865
|
+
{"customer": "cus_123", "metadata[key]": "value"}
|
|
866
|
+
"""
|
|
867
|
+
flattened = {}
|
|
868
|
+
|
|
869
|
+
for key, value in data.items():
|
|
870
|
+
new_key = f"{parent_key}[{key}]" if parent_key else key
|
|
871
|
+
|
|
872
|
+
if isinstance(value, dict):
|
|
873
|
+
# Recursively flatten nested dicts
|
|
874
|
+
flattened.update(self._flatten_form_data(value, new_key))
|
|
875
|
+
elif isinstance(value, list):
|
|
876
|
+
# Flatten arrays with indexed bracket notation
|
|
877
|
+
for i, item in enumerate(value):
|
|
878
|
+
indexed_key = f"{new_key}[{i}]"
|
|
879
|
+
if isinstance(item, dict):
|
|
880
|
+
# Nested dict in array - recurse
|
|
881
|
+
flattened.update(self._flatten_form_data(item, indexed_key))
|
|
882
|
+
elif isinstance(item, list):
|
|
883
|
+
# Nested list in array - recurse
|
|
884
|
+
flattened.update(self._flatten_form_data({str(i): item}, new_key))
|
|
885
|
+
else:
|
|
886
|
+
# Primitive value in array
|
|
887
|
+
flattened[indexed_key] = item
|
|
888
|
+
else:
|
|
889
|
+
# Primitive value - add directly
|
|
890
|
+
flattened[new_key] = value
|
|
891
|
+
|
|
892
|
+
return flattened
|
|
893
|
+
|
|
845
894
|
def _determine_request_format(self, endpoint: EndpointDefinition, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
846
895
|
"""Determine json/data parameters for HTTP request.
|
|
847
896
|
|
|
848
897
|
GraphQL always uses JSON, regardless of content_type setting.
|
|
898
|
+
For form-encoded requests, nested structures are flattened into bracket notation.
|
|
849
899
|
|
|
850
900
|
Args:
|
|
851
901
|
endpoint: Endpoint definition
|
|
@@ -862,7 +912,9 @@ class LocalExecutor:
|
|
|
862
912
|
if is_graphql or endpoint.content_type.value == "application/json":
|
|
863
913
|
return {"json": body}
|
|
864
914
|
elif endpoint.content_type.value == "application/x-www-form-urlencoded":
|
|
865
|
-
|
|
915
|
+
# Flatten nested structures for form encoding
|
|
916
|
+
flattened_body = self._flatten_form_data(body)
|
|
917
|
+
return {"data": flattened_body}
|
|
866
918
|
|
|
867
919
|
return {}
|
|
868
920
|
|
|
@@ -903,12 +955,18 @@ class LocalExecutor:
|
|
|
903
955
|
|
|
904
956
|
return query
|
|
905
957
|
|
|
906
|
-
def _build_graphql_body(
|
|
958
|
+
def _build_graphql_body(
|
|
959
|
+
self,
|
|
960
|
+
graphql_config: dict[str, Any],
|
|
961
|
+
params: dict[str, Any],
|
|
962
|
+
param_defaults: dict[str, Any] | None = None,
|
|
963
|
+
) -> dict[str, Any]:
|
|
907
964
|
"""Build GraphQL request body with variable substitution and field selection.
|
|
908
965
|
|
|
909
966
|
Args:
|
|
910
967
|
graphql_config: GraphQL configuration from x-airbyte-body-type extension
|
|
911
968
|
params: Parameters from execute() call
|
|
969
|
+
param_defaults: Default values for params from query_params_schema
|
|
912
970
|
|
|
913
971
|
Returns:
|
|
914
972
|
GraphQL request body: {"query": "...", "variables": {...}}
|
|
@@ -922,7 +980,7 @@ class LocalExecutor:
|
|
|
922
980
|
|
|
923
981
|
# Substitute variables from params
|
|
924
982
|
if "variables" in graphql_config and graphql_config["variables"]:
|
|
925
|
-
body["variables"] = self._interpolate_variables(graphql_config["variables"], params)
|
|
983
|
+
body["variables"] = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
|
|
926
984
|
|
|
927
985
|
# Add operation name if specified
|
|
928
986
|
if "operationName" in graphql_config:
|
|
@@ -981,7 +1039,12 @@ class LocalExecutor:
|
|
|
981
1039
|
fields_str = " ".join(graphql_fields)
|
|
982
1040
|
return query.replace("{{ fields }}", fields_str)
|
|
983
1041
|
|
|
984
|
-
def _interpolate_variables(
|
|
1042
|
+
def _interpolate_variables(
|
|
1043
|
+
self,
|
|
1044
|
+
variables: dict[str, Any],
|
|
1045
|
+
params: dict[str, Any],
|
|
1046
|
+
param_defaults: dict[str, Any] | None = None,
|
|
1047
|
+
) -> dict[str, Any]:
|
|
985
1048
|
"""Recursively interpolate variables using params.
|
|
986
1049
|
|
|
987
1050
|
Preserves types (doesn't stringify everything).
|
|
@@ -990,15 +1053,18 @@ class LocalExecutor:
|
|
|
990
1053
|
- Direct replacement: "{{ owner }}" → params["owner"] (preserves type)
|
|
991
1054
|
- Nested objects: {"input": {"name": "{{ name }}"}}
|
|
992
1055
|
- Arrays: [{"id": "{{ id }}"}]
|
|
993
|
-
-
|
|
1056
|
+
- Default values: "{{ per_page }}" → param_defaults["per_page"] if not in params
|
|
1057
|
+
- Unsubstituted placeholders: "{{ states }}" → None (for optional params without defaults)
|
|
994
1058
|
|
|
995
1059
|
Args:
|
|
996
1060
|
variables: Variables dict with template placeholders
|
|
997
1061
|
params: Parameters to substitute
|
|
1062
|
+
param_defaults: Default values for params from query_params_schema
|
|
998
1063
|
|
|
999
1064
|
Returns:
|
|
1000
1065
|
Interpolated variables dict with types preserved
|
|
1001
1066
|
"""
|
|
1067
|
+
defaults = param_defaults or {}
|
|
1002
1068
|
|
|
1003
1069
|
def interpolate_value(value: Any) -> Any:
|
|
1004
1070
|
if isinstance(value, str):
|
|
@@ -1012,8 +1078,15 @@ class LocalExecutor:
|
|
|
1012
1078
|
value = value.replace(placeholder, str(param_value))
|
|
1013
1079
|
|
|
1014
1080
|
# Check if any unsubstituted placeholders remain
|
|
1015
|
-
# If so, return None (treats as "not provided" for optional params)
|
|
1016
1081
|
if re.search(r"\{\{\s*\w+\s*\}\}", value):
|
|
1082
|
+
# Extract placeholder name and check for default value
|
|
1083
|
+
match = re.search(r"\{\{\s*(\w+)\s*\}\}", value)
|
|
1084
|
+
if match:
|
|
1085
|
+
param_name = match.group(1)
|
|
1086
|
+
if param_name in defaults:
|
|
1087
|
+
# Use default value (preserves type)
|
|
1088
|
+
return defaults[param_name]
|
|
1089
|
+
# No default found - return None (for optional params)
|
|
1017
1090
|
return None
|
|
1018
1091
|
|
|
1019
1092
|
return value
|
|
@@ -1056,7 +1129,7 @@ class LocalExecutor:
|
|
|
1056
1129
|
if not action:
|
|
1057
1130
|
return response_data
|
|
1058
1131
|
|
|
1059
|
-
is_array_action = action in (Action.LIST, Action.
|
|
1132
|
+
is_array_action = action in (Action.LIST, Action.API_SEARCH)
|
|
1060
1133
|
|
|
1061
1134
|
try:
|
|
1062
1135
|
# Parse and apply JSONPath expression
|
|
@@ -1151,21 +1224,18 @@ class LocalExecutor:
|
|
|
1151
1224
|
if action not in (Action.CREATE, Action.UPDATE):
|
|
1152
1225
|
return
|
|
1153
1226
|
|
|
1154
|
-
#
|
|
1155
|
-
|
|
1227
|
+
# Get the request schema to find truly required fields
|
|
1228
|
+
request_schema = endpoint.request_schema
|
|
1229
|
+
if not request_schema:
|
|
1156
1230
|
return
|
|
1157
1231
|
|
|
1158
|
-
#
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
missing_fields = []
|
|
1162
|
-
for field in endpoint.body_fields:
|
|
1163
|
-
if field not in params:
|
|
1164
|
-
missing_fields.append(field)
|
|
1232
|
+
# Only validate fields explicitly marked as required in the schema
|
|
1233
|
+
required_fields = request_schema.get("required", [])
|
|
1234
|
+
missing_fields = [field for field in required_fields if field not in params]
|
|
1165
1235
|
|
|
1166
1236
|
if missing_fields:
|
|
1167
1237
|
raise MissingParameterError(
|
|
1168
|
-
f"Missing required body fields for {entity}.{action.value}: {missing_fields}. Provided parameters: {list(params.keys())}"
|
|
1238
|
+
f"Missing required body fields for {entity}.{action.value}: {missing_fields}. " f"Provided parameters: {list(params.keys())}"
|
|
1169
1239
|
)
|
|
1170
1240
|
|
|
1171
1241
|
async def close(self):
|
|
@@ -1189,7 +1259,7 @@ class LocalExecutor:
|
|
|
1189
1259
|
|
|
1190
1260
|
|
|
1191
1261
|
class _StandardOperationHandler:
|
|
1192
|
-
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE,
|
|
1262
|
+
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE, API_SEARCH, AUTHORIZE)."""
|
|
1193
1263
|
|
|
1194
1264
|
def __init__(self, context: _OperationContext):
|
|
1195
1265
|
self.ctx = context
|
|
@@ -1202,7 +1272,7 @@ class _StandardOperationHandler:
|
|
|
1202
1272
|
Action.CREATE,
|
|
1203
1273
|
Action.UPDATE,
|
|
1204
1274
|
Action.DELETE,
|
|
1205
|
-
Action.
|
|
1275
|
+
Action.API_SEARCH,
|
|
1206
1276
|
Action.AUTHORIZE,
|
|
1207
1277
|
}
|
|
1208
1278
|
|
|
@@ -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",
|
|
@@ -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
|
|
|
@@ -485,7 +488,7 @@ class HTTPClient:
|
|
|
485
488
|
elif "application/json" in content_type or not content_type:
|
|
486
489
|
response_data = await response.json()
|
|
487
490
|
else:
|
|
488
|
-
error_msg = f"Expected JSON response for {method.upper()} {url},
|
|
491
|
+
error_msg = f"Expected JSON response for {method.upper()} {url}, got content-type: {content_type}"
|
|
489
492
|
raise HTTPClientError(error_msg)
|
|
490
493
|
|
|
491
494
|
except ValueError as e:
|
|
@@ -556,6 +559,7 @@ class HTTPClient:
|
|
|
556
559
|
current_token = self.secrets.get("access_token")
|
|
557
560
|
strategy = AuthStrategyFactory.get_strategy(self.auth_config.type)
|
|
558
561
|
|
|
562
|
+
# Try to refresh credentials
|
|
559
563
|
try:
|
|
560
564
|
result = await strategy.handle_auth_error(
|
|
561
565
|
status_code=status_code,
|
|
@@ -564,53 +568,56 @@ class HTTPClient:
|
|
|
564
568
|
config_values=self.config_values,
|
|
565
569
|
http_client=None, # Let strategy create its own client
|
|
566
570
|
)
|
|
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
571
|
except Exception as refresh_error:
|
|
608
572
|
self.logger.log_error(
|
|
609
573
|
request_id=request_id,
|
|
610
574
|
error=f"Credential refresh failed: {str(refresh_error)}",
|
|
611
575
|
status_code=status_code,
|
|
612
576
|
)
|
|
577
|
+
result = None
|
|
578
|
+
|
|
579
|
+
# If refresh succeeded, update tokens and retry
|
|
580
|
+
if result:
|
|
581
|
+
# Notify callback if provided (for persistence)
|
|
582
|
+
# Include both tokens AND extracted values for full persistence
|
|
583
|
+
if self.on_token_refresh is not None:
|
|
584
|
+
try:
|
|
585
|
+
# Build callback data with both tokens and extracted values
|
|
586
|
+
callback_data = dict(result.tokens)
|
|
587
|
+
if result.extracted_values:
|
|
588
|
+
callback_data.update(result.extracted_values)
|
|
589
|
+
|
|
590
|
+
# Support both sync and async callbacks
|
|
591
|
+
callback_result = self.on_token_refresh(callback_data)
|
|
592
|
+
if callback_result is not None and hasattr(callback_result, "__await__"):
|
|
593
|
+
await callback_result
|
|
594
|
+
except Exception as callback_error:
|
|
595
|
+
self.logger.log_error(
|
|
596
|
+
request_id=request_id,
|
|
597
|
+
error=f"Token refresh callback failed: {str(callback_error)}",
|
|
598
|
+
status_code=status_code,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Update secrets with new tokens (in-memory)
|
|
602
|
+
self.secrets.update(result.tokens)
|
|
603
|
+
|
|
604
|
+
# Update config_values and re-render base_url with extracted values
|
|
605
|
+
if result.extracted_values:
|
|
606
|
+
self._apply_token_extract(result.extracted_values)
|
|
613
607
|
|
|
608
|
+
if self.secrets.get("access_token") != current_token:
|
|
609
|
+
# Retry with new token - this will go through full retry logic
|
|
610
|
+
# Any errors from this retry will propagate to the caller
|
|
611
|
+
return await self.request(
|
|
612
|
+
method=method,
|
|
613
|
+
path=path,
|
|
614
|
+
params=params,
|
|
615
|
+
json=json,
|
|
616
|
+
data=data,
|
|
617
|
+
headers=headers,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Refresh failed or token didn't change, log and let original error propagate
|
|
614
621
|
self.logger.log_error(request_id=request_id, error=str(error), status_code=status_code)
|
|
615
622
|
|
|
616
623
|
async def request(
|