airbyte-agent-zendesk-support 0.18.18__py3-none-any.whl → 0.18.39__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/_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 +32 -6
- 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 +94 -25
- 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 +50 -43
- airbyte_agent_zendesk_support/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/logger.py +9 -9
- airbyte_agent_zendesk_support/_vendored/connector_sdk/logging/types.py +10 -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 +18 -17
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/components.py +59 -58
- airbyte_agent_zendesk_support/_vendored/connector_sdk/schema/extensions.py +9 -9
- 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/connector.py +98 -15
- airbyte_agent_zendesk_support/connector_model.py +7 -1
- airbyte_agent_zendesk_support/types.py +1 -1
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/METADATA +47 -26
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.dist-info}/RECORD +33 -29
- {airbyte_agent_zendesk_support-0.18.18.dist-info → airbyte_agent_zendesk_support-0.18.39.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(
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -837,15 +836,65 @@ class LocalExecutor:
|
|
|
837
836
|
Request body dict or None if no body needed
|
|
838
837
|
"""
|
|
839
838
|
if endpoint.graphql_body:
|
|
840
|
-
|
|
839
|
+
# Extract defaults from query_params_schema for GraphQL variable interpolation
|
|
840
|
+
param_defaults = {name: schema.get("default") for name, schema in endpoint.query_params_schema.items() if "default" in schema}
|
|
841
|
+
return self._build_graphql_body(endpoint.graphql_body, params, param_defaults)
|
|
841
842
|
elif endpoint.body_fields:
|
|
842
843
|
return self._extract_body(endpoint.body_fields, params)
|
|
843
844
|
return None
|
|
844
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
|
+
|
|
845
893
|
def _determine_request_format(self, endpoint: EndpointDefinition, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
846
894
|
"""Determine json/data parameters for HTTP request.
|
|
847
895
|
|
|
848
896
|
GraphQL always uses JSON, regardless of content_type setting.
|
|
897
|
+
For form-encoded requests, nested structures are flattened into bracket notation.
|
|
849
898
|
|
|
850
899
|
Args:
|
|
851
900
|
endpoint: Endpoint definition
|
|
@@ -862,7 +911,9 @@ class LocalExecutor:
|
|
|
862
911
|
if is_graphql or endpoint.content_type.value == "application/json":
|
|
863
912
|
return {"json": body}
|
|
864
913
|
elif endpoint.content_type.value == "application/x-www-form-urlencoded":
|
|
865
|
-
|
|
914
|
+
# Flatten nested structures for form encoding
|
|
915
|
+
flattened_body = self._flatten_form_data(body)
|
|
916
|
+
return {"data": flattened_body}
|
|
866
917
|
|
|
867
918
|
return {}
|
|
868
919
|
|
|
@@ -903,12 +954,18 @@ class LocalExecutor:
|
|
|
903
954
|
|
|
904
955
|
return query
|
|
905
956
|
|
|
906
|
-
def _build_graphql_body(
|
|
957
|
+
def _build_graphql_body(
|
|
958
|
+
self,
|
|
959
|
+
graphql_config: dict[str, Any],
|
|
960
|
+
params: dict[str, Any],
|
|
961
|
+
param_defaults: dict[str, Any] | None = None,
|
|
962
|
+
) -> dict[str, Any]:
|
|
907
963
|
"""Build GraphQL request body with variable substitution and field selection.
|
|
908
964
|
|
|
909
965
|
Args:
|
|
910
966
|
graphql_config: GraphQL configuration from x-airbyte-body-type extension
|
|
911
967
|
params: Parameters from execute() call
|
|
968
|
+
param_defaults: Default values for params from query_params_schema
|
|
912
969
|
|
|
913
970
|
Returns:
|
|
914
971
|
GraphQL request body: {"query": "...", "variables": {...}}
|
|
@@ -922,7 +979,7 @@ class LocalExecutor:
|
|
|
922
979
|
|
|
923
980
|
# Substitute variables from params
|
|
924
981
|
if "variables" in graphql_config and graphql_config["variables"]:
|
|
925
|
-
body["variables"] = self._interpolate_variables(graphql_config["variables"], params)
|
|
982
|
+
body["variables"] = self._interpolate_variables(graphql_config["variables"], params, param_defaults)
|
|
926
983
|
|
|
927
984
|
# Add operation name if specified
|
|
928
985
|
if "operationName" in graphql_config:
|
|
@@ -981,7 +1038,12 @@ class LocalExecutor:
|
|
|
981
1038
|
fields_str = " ".join(graphql_fields)
|
|
982
1039
|
return query.replace("{{ fields }}", fields_str)
|
|
983
1040
|
|
|
984
|
-
def _interpolate_variables(
|
|
1041
|
+
def _interpolate_variables(
|
|
1042
|
+
self,
|
|
1043
|
+
variables: dict[str, Any],
|
|
1044
|
+
params: dict[str, Any],
|
|
1045
|
+
param_defaults: dict[str, Any] | None = None,
|
|
1046
|
+
) -> dict[str, Any]:
|
|
985
1047
|
"""Recursively interpolate variables using params.
|
|
986
1048
|
|
|
987
1049
|
Preserves types (doesn't stringify everything).
|
|
@@ -990,15 +1052,18 @@ class LocalExecutor:
|
|
|
990
1052
|
- Direct replacement: "{{ owner }}" → params["owner"] (preserves type)
|
|
991
1053
|
- Nested objects: {"input": {"name": "{{ name }}"}}
|
|
992
1054
|
- Arrays: [{"id": "{{ id }}"}]
|
|
993
|
-
-
|
|
1055
|
+
- Default values: "{{ per_page }}" → param_defaults["per_page"] if not in params
|
|
1056
|
+
- Unsubstituted placeholders: "{{ states }}" → None (for optional params without defaults)
|
|
994
1057
|
|
|
995
1058
|
Args:
|
|
996
1059
|
variables: Variables dict with template placeholders
|
|
997
1060
|
params: Parameters to substitute
|
|
1061
|
+
param_defaults: Default values for params from query_params_schema
|
|
998
1062
|
|
|
999
1063
|
Returns:
|
|
1000
1064
|
Interpolated variables dict with types preserved
|
|
1001
1065
|
"""
|
|
1066
|
+
defaults = param_defaults or {}
|
|
1002
1067
|
|
|
1003
1068
|
def interpolate_value(value: Any) -> Any:
|
|
1004
1069
|
if isinstance(value, str):
|
|
@@ -1012,8 +1077,15 @@ class LocalExecutor:
|
|
|
1012
1077
|
value = value.replace(placeholder, str(param_value))
|
|
1013
1078
|
|
|
1014
1079
|
# Check if any unsubstituted placeholders remain
|
|
1015
|
-
# If so, return None (treats as "not provided" for optional params)
|
|
1016
1080
|
if re.search(r"\{\{\s*\w+\s*\}\}", value):
|
|
1081
|
+
# Extract placeholder name and check for default value
|
|
1082
|
+
match = re.search(r"\{\{\s*(\w+)\s*\}\}", value)
|
|
1083
|
+
if match:
|
|
1084
|
+
param_name = match.group(1)
|
|
1085
|
+
if param_name in defaults:
|
|
1086
|
+
# Use default value (preserves type)
|
|
1087
|
+
return defaults[param_name]
|
|
1088
|
+
# No default found - return None (for optional params)
|
|
1017
1089
|
return None
|
|
1018
1090
|
|
|
1019
1091
|
return value
|
|
@@ -1056,7 +1128,7 @@ class LocalExecutor:
|
|
|
1056
1128
|
if not action:
|
|
1057
1129
|
return response_data
|
|
1058
1130
|
|
|
1059
|
-
is_array_action = action in (Action.LIST, Action.
|
|
1131
|
+
is_array_action = action in (Action.LIST, Action.API_SEARCH)
|
|
1060
1132
|
|
|
1061
1133
|
try:
|
|
1062
1134
|
# Parse and apply JSONPath expression
|
|
@@ -1151,17 +1223,14 @@ class LocalExecutor:
|
|
|
1151
1223
|
if action not in (Action.CREATE, Action.UPDATE):
|
|
1152
1224
|
return
|
|
1153
1225
|
|
|
1154
|
-
#
|
|
1155
|
-
|
|
1226
|
+
# Get the request schema to find truly required fields
|
|
1227
|
+
request_schema = endpoint.request_schema
|
|
1228
|
+
if not request_schema:
|
|
1156
1229
|
return
|
|
1157
1230
|
|
|
1158
|
-
#
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
missing_fields = []
|
|
1162
|
-
for field in endpoint.body_fields:
|
|
1163
|
-
if field not in params:
|
|
1164
|
-
missing_fields.append(field)
|
|
1231
|
+
# Only validate fields explicitly marked as required in the schema
|
|
1232
|
+
required_fields = request_schema.get("required", [])
|
|
1233
|
+
missing_fields = [field for field in required_fields if field not in params]
|
|
1165
1234
|
|
|
1166
1235
|
if missing_fields:
|
|
1167
1236
|
raise MissingParameterError(
|
|
@@ -1189,7 +1258,7 @@ class LocalExecutor:
|
|
|
1189
1258
|
|
|
1190
1259
|
|
|
1191
1260
|
class _StandardOperationHandler:
|
|
1192
|
-
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE,
|
|
1261
|
+
"""Handler for standard REST operations (GET, LIST, CREATE, UPDATE, DELETE, API_SEARCH, AUTHORIZE)."""
|
|
1193
1262
|
|
|
1194
1263
|
def __init__(self, context: _OperationContext):
|
|
1195
1264
|
self.ctx = context
|
|
@@ -1202,7 +1271,7 @@ class _StandardOperationHandler:
|
|
|
1202
1271
|
Action.CREATE,
|
|
1203
1272
|
Action.UPDATE,
|
|
1204
1273
|
Action.DELETE,
|
|
1205
|
-
Action.
|
|
1274
|
+
Action.API_SEARCH,
|
|
1206
1275
|
Action.AUTHORIZE,
|
|
1207
1276
|
}
|
|
1208
1277
|
|
|
@@ -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(
|
|
@@ -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(
|