soprano-sdk 0.2.0__py3-none-any.whl → 0.2.2__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.
- soprano_sdk/authenticators/mfa.py +10 -9
- soprano_sdk/core/engine.py +26 -11
- soprano_sdk/nodes/collect_input.py +5 -1
- {soprano_sdk-0.2.0.dist-info → soprano_sdk-0.2.2.dist-info}/METADATA +1 -1
- {soprano_sdk-0.2.0.dist-info → soprano_sdk-0.2.2.dist-info}/RECORD +7 -8
- soprano_sdk/nodes/call_api.py +0 -651
- {soprano_sdk-0.2.0.dist-info → soprano_sdk-0.2.2.dist-info}/WHEEL +0 -0
- {soprano_sdk-0.2.0.dist-info → soprano_sdk-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -70,10 +70,7 @@ def mfa_validate_user_input(**state: dict):
|
|
|
70
70
|
if error:
|
|
71
71
|
if _mfa['retry_count'] == 1:
|
|
72
72
|
_mfa['status'] = 'ERRORED'
|
|
73
|
-
|
|
74
|
-
f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
|
|
75
|
-
)
|
|
76
|
-
return False
|
|
73
|
+
return False, f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
|
|
77
74
|
|
|
78
75
|
if response and 'token' in response:
|
|
79
76
|
token = response['token']
|
|
@@ -90,10 +87,10 @@ def mfa_validate_user_input(**state: dict):
|
|
|
90
87
|
)
|
|
91
88
|
if authorize.status_code == 204:
|
|
92
89
|
_mfa['status'] = 'COMPLETED'
|
|
93
|
-
return True
|
|
90
|
+
return True, None
|
|
94
91
|
else:
|
|
95
92
|
_mfa['status'] = 'FAILED'
|
|
96
|
-
return False
|
|
93
|
+
return False, f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
|
|
97
94
|
|
|
98
95
|
|
|
99
96
|
class MFANodeConfig:
|
|
@@ -103,7 +100,7 @@ class MFANodeConfig:
|
|
|
103
100
|
return dict(
|
|
104
101
|
id=f"{source_node}_mfa_start",
|
|
105
102
|
action="call_function",
|
|
106
|
-
function="
|
|
103
|
+
function="soprano_sdk.authenticators.mfa.enforce_mfa_if_required",
|
|
107
104
|
output=f"{source_node}_mfa_start",
|
|
108
105
|
mfa=mfa,
|
|
109
106
|
transitions=[
|
|
@@ -127,7 +124,7 @@ class MFANodeConfig:
|
|
|
127
124
|
description="Collect Input for MFA value",
|
|
128
125
|
field=input_field_name,
|
|
129
126
|
max_attempts=3,
|
|
130
|
-
validator="
|
|
127
|
+
validator="soprano_sdk.authenticators.mfa.mfa_validate_user_input",
|
|
131
128
|
agent=dict(
|
|
132
129
|
name="MFA Input Data Collector",
|
|
133
130
|
model=model_name,
|
|
@@ -140,8 +137,12 @@ class MFANodeConfig:
|
|
|
140
137
|
- Extract ONLY the OTP code value
|
|
141
138
|
- Output in the exact format shown below
|
|
142
139
|
|
|
140
|
+
Examples:
|
|
141
|
+
* User says: "1234" → `MFA_CAPTURED:1223`
|
|
142
|
+
* User says: "2345e" → `MFA_CAPTURED:1223e
|
|
143
|
+
|
|
143
144
|
**Output Format:**
|
|
144
|
-
MFA_CAPTURED
|
|
145
|
+
MFA_CAPTURED:<input_field_name>
|
|
145
146
|
|
|
146
147
|
"""),
|
|
147
148
|
transitions=[
|
soprano_sdk/core/engine.py
CHANGED
|
@@ -34,7 +34,7 @@ class WorkflowEngine:
|
|
|
34
34
|
self.workflow_name = self.config['name']
|
|
35
35
|
self.workflow_description = self.config['description']
|
|
36
36
|
self.workflow_version = self.config['version']
|
|
37
|
-
self.
|
|
37
|
+
self.mfa_validator_steps: set[str] = set()
|
|
38
38
|
self.steps: list = self.load_steps()
|
|
39
39
|
self.step_map = {step['id']: step for step in self.steps}
|
|
40
40
|
self.data_fields = self.load_data()
|
|
@@ -201,7 +201,8 @@ class WorkflowEngine:
|
|
|
201
201
|
|
|
202
202
|
def load_steps(self):
|
|
203
203
|
prepared_steps: list = []
|
|
204
|
-
|
|
204
|
+
mfa_redirects: Dict[str, str] = {}
|
|
205
|
+
|
|
205
206
|
for step in self.config['steps']:
|
|
206
207
|
step_id = step['id']
|
|
207
208
|
|
|
@@ -215,32 +216,46 @@ class WorkflowEngine:
|
|
|
215
216
|
next_node=mfa_data_collector['id'],
|
|
216
217
|
mfa=mfa_config
|
|
217
218
|
)
|
|
219
|
+
|
|
218
220
|
prepared_steps.append(mfa_start)
|
|
219
221
|
prepared_steps.append(mfa_data_collector)
|
|
220
|
-
self.
|
|
222
|
+
self.mfa_validator_steps.add(mfa_data_collector['id'])
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
for transition in previous_step.get('transitions'):
|
|
224
|
-
if transition.get('next') == step_id:
|
|
225
|
-
transition['next'] = mfa_start['id']
|
|
224
|
+
mfa_redirects[step_id] = mfa_start['id']
|
|
226
225
|
|
|
227
226
|
del step['mfa']
|
|
228
227
|
|
|
229
228
|
prepared_steps.append(step)
|
|
230
|
-
|
|
229
|
+
|
|
230
|
+
for step in prepared_steps:
|
|
231
|
+
if step['id'] in self.mfa_validator_steps: # MFA Validator
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
elif 'mfa' in step: # MFA Start
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
elif step.get('transitions'):
|
|
238
|
+
for transition in step.get('transitions'):
|
|
239
|
+
next_step = transition.get('next')
|
|
240
|
+
if next_step in mfa_redirects:
|
|
241
|
+
transition['next'] = mfa_redirects[next_step]
|
|
242
|
+
|
|
243
|
+
elif step.get('next') in mfa_redirects:
|
|
244
|
+
step['next'] = mfa_redirects[step['next']]
|
|
245
|
+
|
|
231
246
|
return prepared_steps
|
|
232
247
|
|
|
233
248
|
def load_data(self):
|
|
234
249
|
data: list = self.config['data']
|
|
235
|
-
for
|
|
250
|
+
for step_id in self.mfa_validator_steps:
|
|
251
|
+
step_details = self.step_map[step_id]
|
|
236
252
|
data.append(
|
|
237
253
|
dict(
|
|
238
|
-
name=f'{
|
|
254
|
+
name=f'{step_details['field']}',
|
|
239
255
|
type='text',
|
|
240
256
|
description='Input Recieved from the user during MFA'
|
|
241
257
|
)
|
|
242
258
|
)
|
|
243
|
-
|
|
244
259
|
return data
|
|
245
260
|
|
|
246
261
|
|
|
@@ -298,7 +298,11 @@ class CollectInputStrategy(ActionStrategy):
|
|
|
298
298
|
instructions = self._render_template_string(instructions, state)
|
|
299
299
|
|
|
300
300
|
if collector_nodes:
|
|
301
|
-
|
|
301
|
+
collector_nodes_for_intent_change = {
|
|
302
|
+
node_id: node_desc for node_id, node_desc in collector_nodes.items()
|
|
303
|
+
if node_id not in self.engine_context.mfa_validator_steps
|
|
304
|
+
}
|
|
305
|
+
instructions = _wrap_instructions_with_intent_detection(instructions, collector_nodes_for_intent_change, self.is_structured_output)
|
|
302
306
|
return instructions
|
|
303
307
|
|
|
304
308
|
def _load_agent_tools(self, state: Dict[str, Any]) -> List:
|
|
@@ -6,17 +6,16 @@ soprano_sdk/agents/adaptor.py,sha256=Cm02YKFclrESu-Qq4CTknCgU7KaA7Z_2FspnQDkEVfU
|
|
|
6
6
|
soprano_sdk/agents/factory.py,sha256=Aucfz4rZVKCXMAQtbGAqp1JR8aYwa66mokRmKkKGhYA,6699
|
|
7
7
|
soprano_sdk/agents/structured_output.py,sha256=7DSVzfMPsZAqBwI3v6XL15qG5Gh4jJ-qddcVPaa3gdc,3326
|
|
8
8
|
soprano_sdk/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
soprano_sdk/authenticators/mfa.py,sha256=
|
|
9
|
+
soprano_sdk/authenticators/mfa.py,sha256=st71m2R2Ag8l4dEbvARbIXV6kdqACBO9i5DaPK0rlWs,5429
|
|
10
10
|
soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
soprano_sdk/core/constants.py,sha256=-Ij4bbSDsyxQ_NZ5NiusJeepWhoe13WGuzN3LlnXqWY,2017
|
|
12
|
-
soprano_sdk/core/engine.py,sha256=
|
|
12
|
+
soprano_sdk/core/engine.py,sha256=BNwmydKlqm64ZIj7VVVqSbn9ebKvOmUvHSr5XZBdQo4,10473
|
|
13
13
|
soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
|
|
14
14
|
soprano_sdk/core/state.py,sha256=ENf81OOe4C2IEYGWUcaBwJ3O1P6VRg4DVirOXFXjioA,2899
|
|
15
15
|
soprano_sdk/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
soprano_sdk/nodes/base.py,sha256=idFyOGGPnjsASYnrOF_NIh7eFcSuJqw61EoVN_WCTaU,2360
|
|
17
|
-
soprano_sdk/nodes/call_api.py,sha256=9ApkuKMU8en5pQ7SK0yB8eF6KXY_bYV2Q3DkthibizU,22655
|
|
18
17
|
soprano_sdk/nodes/call_function.py,sha256=Quys2Rf2JitV1fnTpnEIpIoCCOQDzhWBriRXO59UqqI,5469
|
|
19
|
-
soprano_sdk/nodes/collect_input.py,sha256=
|
|
18
|
+
soprano_sdk/nodes/collect_input.py,sha256=2zhUXYrWlMJ8KH3qo39hZdPiYmc0_moNvvuP3tl3RGY,23381
|
|
20
19
|
soprano_sdk/nodes/factory.py,sha256=l-Gysfgnao-o2dphhnbjjxcH3ojZanZNYN3CBH9dDbA,1624
|
|
21
20
|
soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
21
|
soprano_sdk/routing/router.py,sha256=SrNciTIXXdC9bAbbO5bX7PN9mlRbITjr4RZdNm4jEVA,3450
|
|
@@ -29,7 +28,7 @@ soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,
|
|
|
29
28
|
soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
|
|
30
29
|
soprano_sdk/validation/schema.py,sha256=R6bm4Pzgx8p08o77FN9FFSKTLTKOCf7WuThrR6rV2FI,14578
|
|
31
30
|
soprano_sdk/validation/validator.py,sha256=mUTweaQyDaPqHWQDlsE9AFwnB3_MQAEOBrj1hwKQK68,7888
|
|
32
|
-
soprano_sdk-0.2.
|
|
33
|
-
soprano_sdk-0.2.
|
|
34
|
-
soprano_sdk-0.2.
|
|
35
|
-
soprano_sdk-0.2.
|
|
31
|
+
soprano_sdk-0.2.2.dist-info/METADATA,sha256=lH2Z9tMo_U7riXwJ_Ucgmp7I0foTtlfsU4MWKBscQPU,11297
|
|
32
|
+
soprano_sdk-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
+
soprano_sdk-0.2.2.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
|
|
34
|
+
soprano_sdk-0.2.2.dist-info/RECORD,,
|
soprano_sdk/nodes/call_api.py
DELETED
|
@@ -1,651 +0,0 @@
|
|
|
1
|
-
# # api_response_handler.py
|
|
2
|
-
# import re
|
|
3
|
-
# import os
|
|
4
|
-
# import json
|
|
5
|
-
# import time
|
|
6
|
-
# from typing import Any, Dict, List, Optional, Union, Callable
|
|
7
|
-
# from dataclasses import dataclass
|
|
8
|
-
# from enum import Enum
|
|
9
|
-
# import requests
|
|
10
|
-
# from requests.adapters import HTTPAdapter
|
|
11
|
-
# from urllib3.util.retry import Retry
|
|
12
|
-
# import importlib
|
|
13
|
-
# import operator
|
|
14
|
-
|
|
15
|
-
# class ErrorSeverity(Enum):
|
|
16
|
-
# """Error severity levels"""
|
|
17
|
-
|
|
18
|
-
# INFO = "info"
|
|
19
|
-
# WARNING = "warning"
|
|
20
|
-
# ERROR = "error"
|
|
21
|
-
# CRITICAL = "critical"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# @dataclass
|
|
25
|
-
# class APIError:
|
|
26
|
-
# """Structured API error"""
|
|
27
|
-
|
|
28
|
-
# step_id: str
|
|
29
|
-
# status_code: int
|
|
30
|
-
# error_code: Optional[str] = None
|
|
31
|
-
# message: str = ""
|
|
32
|
-
# severity: ErrorSeverity = ErrorSeverity.ERROR
|
|
33
|
-
# response_data: Optional[Dict] = None
|
|
34
|
-
# retry_after: Optional[int] = None
|
|
35
|
-
|
|
36
|
-
# def to_dict(self) -> Dict[str, Any]:
|
|
37
|
-
# return {
|
|
38
|
-
# "error": True,
|
|
39
|
-
# "step_id": self.step_id,
|
|
40
|
-
# "status_code": self.status_code,
|
|
41
|
-
# "error_code": self.error_code,
|
|
42
|
-
# "message": self.message,
|
|
43
|
-
# "severity": self.severity.value,
|
|
44
|
-
# "response_data": self.response_data,
|
|
45
|
-
# "retry_after": self.retry_after,
|
|
46
|
-
# }
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# class ValueResolver:
|
|
50
|
-
# """Resolves references and variables in values"""
|
|
51
|
-
|
|
52
|
-
# @staticmethod
|
|
53
|
-
# def resolve(value: Any, state: Dict[str, Any]) -> Any:
|
|
54
|
-
# """Resolve a value with state context"""
|
|
55
|
-
# if not isinstance(value, str):
|
|
56
|
-
# return value
|
|
57
|
-
|
|
58
|
-
# # Handle ${env.VAR_NAME}
|
|
59
|
-
# value = ValueResolver._resolve_env_vars(value)
|
|
60
|
-
|
|
61
|
-
# # Handle ${timestamp}
|
|
62
|
-
# if "${timestamp}" in value:
|
|
63
|
-
# value = value.replace("${timestamp}", str(int(time.time())))
|
|
64
|
-
|
|
65
|
-
# # Handle {state.variable} or {variable.nested.path}
|
|
66
|
-
# value = ValueResolver._resolve_state_references(value, state)
|
|
67
|
-
|
|
68
|
-
# return value
|
|
69
|
-
|
|
70
|
-
# @staticmethod
|
|
71
|
-
# def _resolve_env_vars(value: str) -> str:
|
|
72
|
-
# """Resolve environment variables"""
|
|
73
|
-
# env_pattern = r"\$\{env\.([^}]+)\}"
|
|
74
|
-
# matches = re.findall(env_pattern, value)
|
|
75
|
-
# for match in matches:
|
|
76
|
-
# env_value = os.environ.get(match, "")
|
|
77
|
-
# value = value.replace(f"${{env.{match}}}", env_value)
|
|
78
|
-
# return value
|
|
79
|
-
|
|
80
|
-
# @staticmethod
|
|
81
|
-
# def _resolve_state_references(value: str, state: Dict[str, Any]) -> str:
|
|
82
|
-
# """Resolve state references like {username} or {user.profile.name}"""
|
|
83
|
-
# pattern = r"\{([^}]+)\}"
|
|
84
|
-
# matches = re.findall(pattern, value)
|
|
85
|
-
|
|
86
|
-
# for match in matches:
|
|
87
|
-
# resolved = ValueResolver._extract_nested_value(state, match)
|
|
88
|
-
# if resolved is not None:
|
|
89
|
-
# value = value.replace(f"{{{match}}}", str(resolved))
|
|
90
|
-
|
|
91
|
-
# return value
|
|
92
|
-
|
|
93
|
-
# @staticmethod
|
|
94
|
-
# def _extract_nested_value(data: Any, path: str) -> Any:
|
|
95
|
-
# """Extract value from nested structure using dot notation"""
|
|
96
|
-
# parts = path.split(".")
|
|
97
|
-
# result = data
|
|
98
|
-
|
|
99
|
-
# for part in parts:
|
|
100
|
-
# if isinstance(result, dict):
|
|
101
|
-
# result = result.get(part)
|
|
102
|
-
# elif isinstance(result, list) and part.isdigit():
|
|
103
|
-
# try:
|
|
104
|
-
# result = result[int(part)]
|
|
105
|
-
# except (IndexError, ValueError):
|
|
106
|
-
# return None
|
|
107
|
-
# else:
|
|
108
|
-
# return None
|
|
109
|
-
|
|
110
|
-
# if result is None:
|
|
111
|
-
# return None
|
|
112
|
-
|
|
113
|
-
# return result
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# class ConditionEvaluator:
|
|
117
|
-
# OPERATORS = {
|
|
118
|
-
# "==": operator.eq,
|
|
119
|
-
# "!=": operator.ne,
|
|
120
|
-
# ">": operator.gt,
|
|
121
|
-
# "<": operator.lt,
|
|
122
|
-
# ">=": operator.ge,
|
|
123
|
-
# "<=": operator.le,
|
|
124
|
-
# "contains": lambda a, b: b in str(a) if a is not None else False,
|
|
125
|
-
# "not_contains": lambda a, b: b not in str(a) if a is not None else True,
|
|
126
|
-
# "in": lambda a, b: a in b if isinstance(b, (list, tuple)) else False,
|
|
127
|
-
# "not_in": lambda a, b: a not in b if isinstance(b, (list, tuple)) else True,
|
|
128
|
-
# "starts_with": (
|
|
129
|
-
# lambda a, b: str(a).startswith(str(b))
|
|
130
|
-
# if a is not None else False
|
|
131
|
-
# ),
|
|
132
|
-
# "ends_with": lambda a, b: str(a).endswith(str(b)) if a is not None else False,
|
|
133
|
-
# "is_null": lambda a, b: a is None,
|
|
134
|
-
# "is_not_null": lambda a, b: a is not None,
|
|
135
|
-
# "is_empty": lambda a, b: not a if isinstance(a, (list, dict, str)) else False,
|
|
136
|
-
# "is_not_empty": (
|
|
137
|
-
# lambda a, b: bool(a)
|
|
138
|
-
# if isinstance(a, (list, dict, str)) else False
|
|
139
|
-
# ),
|
|
140
|
-
# }
|
|
141
|
-
|
|
142
|
-
# @staticmethod
|
|
143
|
-
# def evaluate(
|
|
144
|
-
# condition: Dict[str, Any], response: Dict[str, Any], state: Dict[str, Any]) -> bool:
|
|
145
|
-
|
|
146
|
-
# if "status_code" in condition:
|
|
147
|
-
# expected = condition["status_code"]
|
|
148
|
-
# actual = response.get("status")
|
|
149
|
-
|
|
150
|
-
# if isinstance(expected, list):
|
|
151
|
-
# if actual not in expected:
|
|
152
|
-
# return False
|
|
153
|
-
# elif actual != expected:
|
|
154
|
-
# return False
|
|
155
|
-
|
|
156
|
-
# if "field" in condition:
|
|
157
|
-
# field_path = condition["field"]
|
|
158
|
-
# operator = condition.get("operator", "==")
|
|
159
|
-
# expected_value = condition.get("value")
|
|
160
|
-
|
|
161
|
-
# actual_value = ValueResolver._extract_nested_value(response, field_path)
|
|
162
|
-
|
|
163
|
-
# if isinstance(expected_value, str):
|
|
164
|
-
# expected_value = ValueResolver.resolve(expected_value, state)
|
|
165
|
-
|
|
166
|
-
# if operator not in ConditionEvaluator.OPERATORS:
|
|
167
|
-
# raise ValueError(f"Unknown operator: {operator}")
|
|
168
|
-
|
|
169
|
-
# return ConditionEvaluator.OPERATORS[operator](actual_value, expected_value)
|
|
170
|
-
|
|
171
|
-
# if "and" in condition:
|
|
172
|
-
# return all(
|
|
173
|
-
# ConditionEvaluator.evaluate(sub_cond, response, state)
|
|
174
|
-
# for sub_cond in condition["and"]
|
|
175
|
-
# )
|
|
176
|
-
|
|
177
|
-
# if "or" in condition:
|
|
178
|
-
# return any(
|
|
179
|
-
# ConditionEvaluator.evaluate(sub_cond, response, state)
|
|
180
|
-
# for sub_cond in condition["or"]
|
|
181
|
-
# )
|
|
182
|
-
|
|
183
|
-
# return True
|
|
184
|
-
|
|
185
|
-
# @staticmethod
|
|
186
|
-
# def evaluate_all(
|
|
187
|
-
# conditions: List[Dict[str, Any]],
|
|
188
|
-
# response: Dict[str, Any],
|
|
189
|
-
# state: Dict[str, Any],
|
|
190
|
-
# ) -> Optional[Dict[str, Any]]:
|
|
191
|
-
# """Find first matching condition from a list"""
|
|
192
|
-
# for condition in conditions:
|
|
193
|
-
# # Skip default conditions for now
|
|
194
|
-
# if condition.get("default"):
|
|
195
|
-
# continue
|
|
196
|
-
|
|
197
|
-
# if ConditionEvaluator.evaluate(condition, response, state):
|
|
198
|
-
# return condition
|
|
199
|
-
|
|
200
|
-
# # Return default if no match
|
|
201
|
-
# for condition in conditions:
|
|
202
|
-
# if condition.get("default"):
|
|
203
|
-
# return condition
|
|
204
|
-
|
|
205
|
-
# return None
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# class ResponseMapper:
|
|
209
|
-
# """Maps response data to state variables"""
|
|
210
|
-
|
|
211
|
-
# @staticmethod
|
|
212
|
-
# def apply_mappings(
|
|
213
|
-
# response: Dict[str, Any], mappings: List[Dict[str, Any]], state: Dict[str, Any]
|
|
214
|
-
# ) -> None:
|
|
215
|
-
# """Apply response mappings to state"""
|
|
216
|
-
# for mapping in mappings:
|
|
217
|
-
# from_path = mapping.get("from", "")
|
|
218
|
-
# to_field = mapping.get("to", "")
|
|
219
|
-
# transform = mapping.get("transform") # Optional transformation function
|
|
220
|
-
# default = mapping.get("default") # Default value if extraction fails
|
|
221
|
-
|
|
222
|
-
# # Extract value
|
|
223
|
-
# value = ValueResolver._extract_nested_value(response, from_path)
|
|
224
|
-
|
|
225
|
-
# # Apply default if needed
|
|
226
|
-
# if value is None and default is not None:
|
|
227
|
-
# value = default
|
|
228
|
-
|
|
229
|
-
# # Apply transformation if specified
|
|
230
|
-
# if value is not None and transform:
|
|
231
|
-
# value = ResponseMapper._apply_transform(value, transform, state)
|
|
232
|
-
|
|
233
|
-
# # Store in state
|
|
234
|
-
# if to_field:
|
|
235
|
-
# state[to_field] = value
|
|
236
|
-
|
|
237
|
-
# @staticmethod
|
|
238
|
-
# def _apply_transform(
|
|
239
|
-
# value: Any, transform: Union[str, Dict], state: Dict[str, Any]
|
|
240
|
-
# ) -> Any:
|
|
241
|
-
# """Apply transformation to a value"""
|
|
242
|
-
# if isinstance(transform, str):
|
|
243
|
-
# # Simple transformations
|
|
244
|
-
# if transform == "lowercase":
|
|
245
|
-
# return str(value).lower()
|
|
246
|
-
# elif transform == "uppercase":
|
|
247
|
-
# return str(value).upper()
|
|
248
|
-
# elif transform == "trim":
|
|
249
|
-
# return str(value).strip()
|
|
250
|
-
# elif transform == "int":
|
|
251
|
-
# return int(value)
|
|
252
|
-
# elif transform == "float":
|
|
253
|
-
# return float(value)
|
|
254
|
-
# elif transform == "bool":
|
|
255
|
-
# return bool(value)
|
|
256
|
-
# elif transform == "json":
|
|
257
|
-
# return json.loads(value) if isinstance(value, str) else value
|
|
258
|
-
|
|
259
|
-
# elif isinstance(transform, dict):
|
|
260
|
-
# # Function-based transformation
|
|
261
|
-
# if "function" in transform:
|
|
262
|
-
# func_path = transform["function"]
|
|
263
|
-
# func_args = transform.get("args", {})
|
|
264
|
-
|
|
265
|
-
# # Resolve arguments
|
|
266
|
-
# resolved_args = {
|
|
267
|
-
# k: ValueResolver.resolve(v, state) for k, v in func_args.items()
|
|
268
|
-
# }
|
|
269
|
-
# resolved_args["value"] = value
|
|
270
|
-
|
|
271
|
-
# # Execute transformation function
|
|
272
|
-
# return FunctionExecutor.execute_transform(func_path, resolved_args)
|
|
273
|
-
|
|
274
|
-
# return value
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
# class ErrorHandler:
|
|
278
|
-
# """Handles API errors with configurable responses"""
|
|
279
|
-
|
|
280
|
-
# @staticmethod
|
|
281
|
-
# def handle_error(
|
|
282
|
-
# step_id: str,
|
|
283
|
-
# response: Dict[str, Any],
|
|
284
|
-
# error_config: Dict[str, Any],
|
|
285
|
-
# state: Dict[str, Any],
|
|
286
|
-
# ) -> APIError:
|
|
287
|
-
# """Handle an error based on configuration"""
|
|
288
|
-
# status_code = response.get("status", 0)
|
|
289
|
-
|
|
290
|
-
# message = error_config.get("message", "An error occurred")
|
|
291
|
-
# message = ValueResolver.resolve(message, {**state, "response": response})
|
|
292
|
-
|
|
293
|
-
# error_code_path = error_config.get("error_code_field", "error_code")
|
|
294
|
-
# error_code = ValueResolver._extract_nested_value(
|
|
295
|
-
# response, f"data.{error_code_path}"
|
|
296
|
-
# )
|
|
297
|
-
|
|
298
|
-
# severity_str = error_config.get("severity", "error")
|
|
299
|
-
# severity = ErrorSeverity(severity_str)
|
|
300
|
-
|
|
301
|
-
# retry_after = error_config.get("retry_after")
|
|
302
|
-
# if retry_after:
|
|
303
|
-
# retry_after = ValueResolver.resolve(retry_after, state)
|
|
304
|
-
|
|
305
|
-
# return APIError(
|
|
306
|
-
# step_id=step_id,
|
|
307
|
-
# status_code=status_code,
|
|
308
|
-
# error_code=error_code,
|
|
309
|
-
# message=message,
|
|
310
|
-
# severity=severity,
|
|
311
|
-
# response_data=response.get("data"),
|
|
312
|
-
# retry_after=retry_after,
|
|
313
|
-
# )
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# class PostProcessor:
|
|
317
|
-
# """Handles post-processing of API responses"""
|
|
318
|
-
|
|
319
|
-
# @staticmethod
|
|
320
|
-
# def process(
|
|
321
|
-
# response: Dict[str, Any], post_config: Dict[str, Any], state: Dict[str, Any]
|
|
322
|
-
# ) -> Any:
|
|
323
|
-
# """Execute post-processing on response"""
|
|
324
|
-
# function_path = post_config.get("function")
|
|
325
|
-
# inputs = post_config.get("inputs", {})
|
|
326
|
-
# output_field = post_config.get("output")
|
|
327
|
-
|
|
328
|
-
# resolved_inputs = {"response": response}
|
|
329
|
-
# for key, value in inputs.items():
|
|
330
|
-
# resolved_inputs[key] = ValueResolver.resolve(value, state)
|
|
331
|
-
|
|
332
|
-
# result = FunctionExecutor.execute(function_path, resolved_inputs)
|
|
333
|
-
|
|
334
|
-
# if output_field:
|
|
335
|
-
# state[output_field] = result
|
|
336
|
-
|
|
337
|
-
# return result
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
# class FunctionExecutor:
|
|
341
|
-
# """Executes functions from module paths"""
|
|
342
|
-
|
|
343
|
-
# @staticmethod
|
|
344
|
-
# def execute(function_path: str, inputs: Dict[str, Any]) -> Any:
|
|
345
|
-
# """Execute a function given its module path"""
|
|
346
|
-
# try:
|
|
347
|
-
# # Parse module and function name
|
|
348
|
-
# parts = function_path.rsplit(".", 1)
|
|
349
|
-
# if len(parts) != 2:
|
|
350
|
-
# raise ValueError(f"Invalid function path: {function_path}")
|
|
351
|
-
|
|
352
|
-
# module_name, function_name = parts
|
|
353
|
-
|
|
354
|
-
# module = importlib.import_module(module_name)
|
|
355
|
-
# function = getattr(module, function_name)
|
|
356
|
-
|
|
357
|
-
# return function(**inputs)
|
|
358
|
-
|
|
359
|
-
# except Exception as e:
|
|
360
|
-
# raise Exception(f"Failed to execute function {function_path}: {str(e)}")
|
|
361
|
-
|
|
362
|
-
# @staticmethod
|
|
363
|
-
# def execute_transform(function_path: str, inputs: Dict[str, Any]) -> Any:
|
|
364
|
-
# """Execute a transformation function"""
|
|
365
|
-
# return FunctionExecutor.execute(function_path, inputs)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
# class APIClient:
|
|
369
|
-
# """Handles API requests with comprehensive error handling"""
|
|
370
|
-
|
|
371
|
-
# def __init__(self, config: Dict[str, Any]):
|
|
372
|
-
# self.name = config.get("name")
|
|
373
|
-
# self.base_url = ValueResolver._resolve_env_vars(config.get("base_url", ""))
|
|
374
|
-
# self.headers = self._resolve_headers(config.get("headers", {}))
|
|
375
|
-
# self.timeout = config.get("timeout", 30)
|
|
376
|
-
# self.retry_config = config.get("retry", {})
|
|
377
|
-
|
|
378
|
-
# self.session = self._create_session()
|
|
379
|
-
|
|
380
|
-
# def _resolve_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
|
|
381
|
-
# """Resolve all header values"""
|
|
382
|
-
# return {k: ValueResolver._resolve_env_vars(v) for k, v in headers.items()}
|
|
383
|
-
|
|
384
|
-
# def _create_session(self) -> requests.Session:
|
|
385
|
-
# """Create requests session with retry logic"""
|
|
386
|
-
# session = requests.Session()
|
|
387
|
-
|
|
388
|
-
# if self.retry_config:
|
|
389
|
-
# retry_strategy = Retry(
|
|
390
|
-
# total=self.retry_config.get("max_attempts", 3),
|
|
391
|
-
# backoff_factor=1 if self.retry_config.get("backoff") == "exponential" else 0,
|
|
392
|
-
# status_forcelist=self.retry_config.get("for_status_codes", [429, 500, 502, 503, 504]),
|
|
393
|
-
# )
|
|
394
|
-
# adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
395
|
-
# session.mount("http://", adapter)
|
|
396
|
-
# session.mount("https://", adapter)
|
|
397
|
-
|
|
398
|
-
# return session
|
|
399
|
-
|
|
400
|
-
# def execute(
|
|
401
|
-
# self,
|
|
402
|
-
# request_config: Dict[str, Any],
|
|
403
|
-
# state: Dict[str, Any],
|
|
404
|
-
# step_id: str = "api_call",
|
|
405
|
-
# ) -> Dict[str, Any]:
|
|
406
|
-
# """Execute API request with full error handling"""
|
|
407
|
-
|
|
408
|
-
# method = request_config.get("method", "GET").upper()
|
|
409
|
-
# path = self._prepare_path(request_config, state)
|
|
410
|
-
# url = f"{self.base_url}{path}"
|
|
411
|
-
|
|
412
|
-
# query_params = self._prepare_params(
|
|
413
|
-
# request_config.get("query_params", {}), state
|
|
414
|
-
# )
|
|
415
|
-
# headers = self._prepare_headers(request_config, state)
|
|
416
|
-
# body = self._prepare_body(request_config, state)
|
|
417
|
-
|
|
418
|
-
# try:
|
|
419
|
-
# response = self.session.request(
|
|
420
|
-
# method=method,
|
|
421
|
-
# url=url,
|
|
422
|
-
# headers=headers,
|
|
423
|
-
# params=query_params,
|
|
424
|
-
# json=body if request_config.get("body_type", "json") == "json" else None,
|
|
425
|
-
# data=body if request_config.get("body_type") != "json" else None,
|
|
426
|
-
# timeout=self.timeout,
|
|
427
|
-
# )
|
|
428
|
-
|
|
429
|
-
# try:
|
|
430
|
-
# response_data = response.json()
|
|
431
|
-
# except:
|
|
432
|
-
# response_data = {"raw": response.text}
|
|
433
|
-
|
|
434
|
-
# return {
|
|
435
|
-
# "status": response.status_code,
|
|
436
|
-
# "data": response_data,
|
|
437
|
-
# "headers": dict(response.headers),
|
|
438
|
-
# "success": response.ok,
|
|
439
|
-
# "url": url,
|
|
440
|
-
# "method": method,
|
|
441
|
-
# }
|
|
442
|
-
|
|
443
|
-
# except requests.exceptions.Timeout:
|
|
444
|
-
# return {
|
|
445
|
-
# "status": 0,
|
|
446
|
-
# "error": "Request timeout",
|
|
447
|
-
# "error_type": "timeout",
|
|
448
|
-
# "success": False,
|
|
449
|
-
# }
|
|
450
|
-
# except requests.exceptions.ConnectionError:
|
|
451
|
-
# return {
|
|
452
|
-
# "status": 0,
|
|
453
|
-
# "error": "Connection error",
|
|
454
|
-
# "error_type": "connection",
|
|
455
|
-
# "success": False,
|
|
456
|
-
# }
|
|
457
|
-
# except requests.exceptions.RequestException as e:
|
|
458
|
-
# return {
|
|
459
|
-
# "status": 0,
|
|
460
|
-
# "error": str(e),
|
|
461
|
-
# "error_type": "request",
|
|
462
|
-
# "success": False,
|
|
463
|
-
# }
|
|
464
|
-
|
|
465
|
-
# def _prepare_path(
|
|
466
|
-
# self, request_config: Dict[str, Any], state: Dict[str, Any]
|
|
467
|
-
# ) -> str:
|
|
468
|
-
# """Prepare URL path with variable substitution"""
|
|
469
|
-
# path = request_config.get("path", "")
|
|
470
|
-
# path_params = request_config.get("path_params", {})
|
|
471
|
-
|
|
472
|
-
# for key, value in path_params.items():
|
|
473
|
-
# resolved_value = ValueResolver.resolve(value, state)
|
|
474
|
-
# path = path.replace(f"{{{key}}}", str(resolved_value))
|
|
475
|
-
|
|
476
|
-
# return path
|
|
477
|
-
|
|
478
|
-
# def _prepare_params(
|
|
479
|
-
# self, params: Dict[str, Any], state: Dict[str, Any]
|
|
480
|
-
# ) -> Dict[str, Any]:
|
|
481
|
-
# """Prepare query parameters"""
|
|
482
|
-
# return {k: ValueResolver.resolve(v, state) for k, v in params.items()}
|
|
483
|
-
|
|
484
|
-
# def _prepare_headers(
|
|
485
|
-
# self, request_config: Dict[str, Any], state: Dict[str, Any]
|
|
486
|
-
# ) -> Dict[str, str]:
|
|
487
|
-
# """Prepare request headers"""
|
|
488
|
-
# headers = {**self.headers}
|
|
489
|
-
|
|
490
|
-
# if "headers" in request_config:
|
|
491
|
-
# for key, value in request_config["headers"].items():
|
|
492
|
-
# headers[key] = ValueResolver.resolve(value, state)
|
|
493
|
-
|
|
494
|
-
# return headers
|
|
495
|
-
|
|
496
|
-
# def _prepare_body(
|
|
497
|
-
# self, request_config: Dict[str, Any], state: Dict[str, Any]
|
|
498
|
-
# ) -> Optional[Dict[str, Any]]:
|
|
499
|
-
# """Prepare request body"""
|
|
500
|
-
# if "body" not in request_config:
|
|
501
|
-
# return None
|
|
502
|
-
|
|
503
|
-
# body = request_config["body"]
|
|
504
|
-
# return self._resolve_dict(body, state)
|
|
505
|
-
|
|
506
|
-
# def _resolve_dict(
|
|
507
|
-
# self, data: Dict[str, Any], state: Dict[str, Any]
|
|
508
|
-
# ) -> Dict[str, Any]:
|
|
509
|
-
# """Recursively resolve dictionary values"""
|
|
510
|
-
# result = {}
|
|
511
|
-
# for key, value in data.items():
|
|
512
|
-
# if isinstance(value, dict):
|
|
513
|
-
# result[key] = self._resolve_dict(value, state)
|
|
514
|
-
# elif isinstance(value, list):
|
|
515
|
-
# result[key] = [
|
|
516
|
-
# self._resolve_dict(item, state)
|
|
517
|
-
# if isinstance(item, dict)
|
|
518
|
-
# else ValueResolver.resolve(item, state)
|
|
519
|
-
# for item in value
|
|
520
|
-
# ]
|
|
521
|
-
# else:
|
|
522
|
-
# result[key] = ValueResolver.resolve(value, state)
|
|
523
|
-
# return result
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# class APIResponseHandler:
|
|
527
|
-
# """Main handler for API responses with error handling and post-processing"""
|
|
528
|
-
|
|
529
|
-
# def __init__(self, step_id: str, api_client: APIClient):
|
|
530
|
-
# self.step_id = step_id
|
|
531
|
-
# self.api_client = api_client
|
|
532
|
-
|
|
533
|
-
# def execute_with_handling(
|
|
534
|
-
# self,
|
|
535
|
-
# request_config: Dict[str, Any],
|
|
536
|
-
# response_config: Dict[str, Any],
|
|
537
|
-
# state: Dict[str, Any],
|
|
538
|
-
# ) -> Dict[str, Any]:
|
|
539
|
-
# """Execute API call with full response handling"""
|
|
540
|
-
|
|
541
|
-
# response = self.api_client.execute(request_config, state, self.step_id)
|
|
542
|
-
|
|
543
|
-
# error_handlers = response_config.get("error_handlers", [])
|
|
544
|
-
# error = self._check_errors(response, error_handlers, state)
|
|
545
|
-
|
|
546
|
-
# if error:
|
|
547
|
-
# return error.to_dict()
|
|
548
|
-
|
|
549
|
-
# mappings = response_config.get("mappings", [])
|
|
550
|
-
# if mappings:
|
|
551
|
-
# ResponseMapper.apply_mappings(response, mappings, state)
|
|
552
|
-
|
|
553
|
-
# if "post_process" in response_config:
|
|
554
|
-
# post_result = PostProcessor.process(
|
|
555
|
-
# response, response_config["post_process"], state
|
|
556
|
-
# )
|
|
557
|
-
|
|
558
|
-
# conditions = response_config.get("conditions", [])
|
|
559
|
-
# if conditions:
|
|
560
|
-
# matching_condition = ConditionEvaluator.evaluate_all(
|
|
561
|
-
# conditions, response, state
|
|
562
|
-
# )
|
|
563
|
-
|
|
564
|
-
# if matching_condition:
|
|
565
|
-
# if "mappings" in matching_condition:
|
|
566
|
-
# ResponseMapper.apply_mappings(
|
|
567
|
-
# response, matching_condition["mappings"], state
|
|
568
|
-
# )
|
|
569
|
-
|
|
570
|
-
# if "next" in matching_condition:
|
|
571
|
-
# state["_next_step"] = matching_condition["next"]
|
|
572
|
-
|
|
573
|
-
# if "action" in matching_condition:
|
|
574
|
-
# state["_action"] = matching_condition["action"]
|
|
575
|
-
|
|
576
|
-
# return {"success": True, "response": response, "state": state}
|
|
577
|
-
|
|
578
|
-
# def _check_errors(
|
|
579
|
-
# self,
|
|
580
|
-
# response: Dict[str, Any],
|
|
581
|
-
# error_handlers: List[Dict[str, Any]],
|
|
582
|
-
# state: Dict[str, Any],
|
|
583
|
-
# ) -> Optional[APIError]:
|
|
584
|
-
# for error_config in error_handlers:
|
|
585
|
-
# # Evaluate error condition
|
|
586
|
-
# if ConditionEvaluator.evaluate(error_config, response, state):
|
|
587
|
-
# return ErrorHandler.handle_error(
|
|
588
|
-
# self.step_id, response, error_config, state
|
|
589
|
-
# )
|
|
590
|
-
|
|
591
|
-
# return None
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
# # Example usage
|
|
595
|
-
# if __name__ == "__main__":
|
|
596
|
-
# # Example configuration
|
|
597
|
-
# api_config = {
|
|
598
|
-
# "name": "user_service",
|
|
599
|
-
# "base_url": "https://api.example.com",
|
|
600
|
-
# "headers": {
|
|
601
|
-
# "Authorization": "Bearer ${env.API_TOKEN}",
|
|
602
|
-
# "Content-Type": "application/json",
|
|
603
|
-
# },
|
|
604
|
-
# "timeout": 30,
|
|
605
|
-
# }
|
|
606
|
-
|
|
607
|
-
# request_config = {
|
|
608
|
-
# "method": "GET",
|
|
609
|
-
# "path": "/users/{user_id}",
|
|
610
|
-
# "path_params": {"user_id": "{user_id}"},
|
|
611
|
-
# }
|
|
612
|
-
|
|
613
|
-
# response_config = {
|
|
614
|
-
# "mappings": [
|
|
615
|
-
# {"from": "data.name", "to": "user_name"},
|
|
616
|
-
# {"from": "data.email", "to": "user_email"},
|
|
617
|
-
# ],
|
|
618
|
-
# "error_handlers": [
|
|
619
|
-
# {
|
|
620
|
-
# "status_code": 404,
|
|
621
|
-
# "field": "data.error_code",
|
|
622
|
-
# "operator": "==",
|
|
623
|
-
# "value": "ERR_NOT_FOUND",
|
|
624
|
-
# "message": "Sorry, we could not find the user you requested",
|
|
625
|
-
# "severity": "error",
|
|
626
|
-
# },
|
|
627
|
-
# {
|
|
628
|
-
# "status_code": 200,
|
|
629
|
-
# "field": "data.error_data",
|
|
630
|
-
# "operator": "==",
|
|
631
|
-
# "value": "ERR_NOT_FOUND",
|
|
632
|
-
# "message": "Sorry, we could not find the request",
|
|
633
|
-
# "severity": "warning",
|
|
634
|
-
# },
|
|
635
|
-
# ],
|
|
636
|
-
# "post_process": {
|
|
637
|
-
# "function": "response_processors.format_user_data",
|
|
638
|
-
# "inputs": {"user_id": "{user_id}"},
|
|
639
|
-
# "output": "formatted_user",
|
|
640
|
-
# },
|
|
641
|
-
# }
|
|
642
|
-
|
|
643
|
-
# # Initialize
|
|
644
|
-
# state = {"user_id": "12345"}
|
|
645
|
-
# api_client = APIClient(api_config)
|
|
646
|
-
# handler = APIResponseHandler("fetch_user", api_client)
|
|
647
|
-
|
|
648
|
-
# # Execute
|
|
649
|
-
# result = handler.execute_with_handling(request_config, response_config, state)
|
|
650
|
-
|
|
651
|
-
# print(json.dumps(result, indent=2))
|
|
File without changes
|
|
File without changes
|