soprano-sdk 0.2.4__py3-none-any.whl → 0.2.6__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.
@@ -10,6 +10,7 @@ class MFAChallenge(TypedDict):
10
10
  class MFAState(TypedDict):
11
11
  challengeType: Literal['OTP', 'dob']
12
12
  post_payload: dict[str, str]
13
+ post_headers: NotRequired[dict[str, str]]
13
14
  otpValue: NotRequired[str]
14
15
  status: Literal['IN_PROGRESS', 'COMPLETED', 'ERRORED', 'FAILED'] | None
15
16
  message: str
@@ -35,6 +36,10 @@ def enforce_mfa_if_required(state: dict, mfa_config: Optional[MFAConfig] = None)
35
36
  _mfa : MFAState = state['_mfa']
36
37
  if _mfa['status'] == 'COMPLETED':
37
38
  return True
39
+
40
+ # Use custom headers if provided, otherwise empty dict
41
+ headers = _mfa.get('post_headers', {})
42
+
38
43
  generate_token_response = requests.post(
39
44
  build_path(
40
45
  base_url=mfa_config.generate_token_base_url,
@@ -42,7 +47,7 @@ def enforce_mfa_if_required(state: dict, mfa_config: Optional[MFAConfig] = None)
42
47
  ),
43
48
  json=_mfa['post_payload'],
44
49
  timeout=mfa_config.api_timeout,
45
- headers={"Authorization": f"Bearer {state['bearer_token']}"}
50
+ headers=headers
46
51
  )
47
52
  _, error = get_response(generate_token_response)
48
53
 
@@ -65,6 +70,9 @@ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dic
65
70
  if not state[input_field_name]:
66
71
  return False
67
72
 
73
+ # Use custom headers if provided, otherwise empty dict
74
+ headers = _mfa.get('post_headers', {})
75
+
68
76
  post_payload = _mfa['post_payload']
69
77
  challenge_field_name = f"{_mfa['challengeType'].lower()}Challenge"
70
78
  post_payload.update({challenge_field_name: {"value": state[input_field_name]}})
@@ -75,7 +83,7 @@ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dic
75
83
  ),
76
84
  json=post_payload,
77
85
  timeout=mfa_config.api_timeout,
78
- headers={"Authorization": f"Bearer {state['bearer_token']}"}
86
+ headers=headers
79
87
  )
80
88
  _mfa['retry_count'] += 1
81
89
  response, error = get_response(validate_token_response)
@@ -95,7 +103,7 @@ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dic
95
103
  ),
96
104
  json=post_payload,
97
105
  timeout=mfa_config.api_timeout,
98
- headers={"Authorization": f"Bearer {state['bearer_token']}"}
106
+ headers=headers
99
107
  )
100
108
  if authorize.status_code == 204:
101
109
  _mfa['status'] = 'COMPLETED'
@@ -22,6 +22,13 @@ class WorkflowKeys:
22
22
  class ActionType(Enum):
23
23
  COLLECT_INPUT_WITH_AGENT = 'collect_input_with_agent'
24
24
  CALL_FUNCTION = 'call_function'
25
+ CALL_ASYNC_FUNCTION = 'call_async_function'
26
+
27
+
28
+ class InterruptType:
29
+ """Interrupt type markers for workflow pauses"""
30
+ USER_INPUT = '__WORKFLOW_INTERRUPT__'
31
+ ASYNC = '__ASYNC_INTERRUPT__'
25
32
 
26
33
 
27
34
  class DataType(Enum):
@@ -0,0 +1,237 @@
1
+ """
2
+ Async Function Strategy - Handles asynchronous function calls with interrupt/resume pattern.
3
+
4
+ This strategy allows workflows to pause while waiting for an external async operation
5
+ to complete. The async function initiates an operation and returns a "pending" status.
6
+ The workflow then interrupts, and resumes when the external system calls back with the result.
7
+
8
+ Example YAML:
9
+ - id: verify_identity
10
+ action: call_async_function
11
+ function: "services.identity.start_verification"
12
+ output: verification_result
13
+ transitions:
14
+ - condition: "verified"
15
+ next: approved
16
+ - condition: "failed"
17
+ next: rejected
18
+
19
+ The async function should return:
20
+ - {"status": "pending", ...metadata} to trigger interrupt and wait for callback
21
+ - Any other dict for synchronous completion (no interrupt)
22
+
23
+ On resume, the async result is passed via Command(resume=async_result) and stored
24
+ in the output field, then transitions are evaluated.
25
+ """
26
+ from typing import Dict, Any
27
+
28
+ from langgraph.types import interrupt
29
+
30
+ from .base import ActionStrategy
31
+ from ..core.state import set_state_value, get_state_value
32
+ from ..core.constants import WorkflowKeys
33
+ from ..utils.logger import logger
34
+ from ..utils.template import get_nested_value
35
+
36
+
37
+ class AsyncFunctionStrategy(ActionStrategy):
38
+ """Strategy for executing async functions with interrupt/resume pattern."""
39
+
40
+ # Key for storing pending metadata in state
41
+ PENDING_KEY_PREFIX = '_async_pending_'
42
+
43
+ def __init__(self, step_config: Dict[str, Any], engine_context: Any):
44
+ super().__init__(step_config, engine_context)
45
+ self.function_path = step_config.get('function')
46
+ self.output_field = step_config.get('output')
47
+ self.transitions = self._get_transitions()
48
+ self.next_step = self._get_next_step()
49
+
50
+ if not self.function_path:
51
+ raise RuntimeError(f"Step '{self.step_id}' missing required 'function' property")
52
+
53
+ if not self.output_field:
54
+ raise RuntimeError(f"Step '{self.step_id}' missing required 'output' property")
55
+
56
+ @property
57
+ def _pending_key(self) -> str:
58
+ """State key for storing pending operation metadata."""
59
+ return f"{self.PENDING_KEY_PREFIX}{self.step_id}"
60
+
61
+ def _is_async_pending(self, state: Dict[str, Any]) -> bool:
62
+ """Check if this node is waiting for async operation to complete."""
63
+ return state.get(WorkflowKeys.STATUS) == f'{self.step_id}_pending'
64
+
65
+ def pre_execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
66
+ """Pre-execution hook."""
67
+ pass
68
+
69
+ def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
70
+ from ..utils.tracing import trace_node_execution
71
+
72
+ with trace_node_execution(
73
+ node_id=self.step_id,
74
+ node_type="call_async_function",
75
+ function=self.function_path,
76
+ output_field=self.output_field
77
+ ) as span:
78
+ # Check if we're resuming from a pending async operation
79
+ if self._is_async_pending(state):
80
+ span.add_event("async.resuming")
81
+ pending_metadata = state.get(self._pending_key, {})
82
+ else:
83
+ # First invocation - call the async function
84
+ result = self._call_function(state, span)
85
+
86
+ if self._is_pending_result(result):
87
+ # Async operation started - store metadata and prepare to interrupt
88
+ span.add_event("async.pending", {"metadata": str(result)})
89
+ self._set_status(state, "pending")
90
+ state[self._pending_key] = result
91
+ pending_metadata = result
92
+ else:
93
+ # Synchronous completion - no interrupt needed
94
+ span.add_event("async.sync_complete", {"result": str(result)})
95
+ return self._handle_sync_completion(state, result, span)
96
+
97
+ # Interrupt with pending metadata
98
+ # On resume, interrupt() returns the async result from Command(resume=...)
99
+ async_result = interrupt({
100
+ "type": "async",
101
+ "step_id": self.step_id,
102
+ "pending": pending_metadata
103
+ })
104
+
105
+ # Clean up pending state
106
+ if self._pending_key in state:
107
+ del state[self._pending_key]
108
+
109
+ span.add_event("async.resumed", {"result": str(async_result)})
110
+
111
+ # Store result and handle routing
112
+ return self._handle_async_completion(state, async_result, span)
113
+
114
+ def _call_function(self, state: Dict[str, Any], span) -> Any:
115
+ """Load and execute the async function."""
116
+ try:
117
+ logger.info(f"Loading async function: {self.function_path}")
118
+ func = self.engine_context.function_repository.load(self.function_path)
119
+ except Exception as e:
120
+ span.set_attribute("error", True)
121
+ span.set_attribute("error.type", "LoadError")
122
+ span.set_attribute("error.message", str(e))
123
+ raise RuntimeError(
124
+ f"Failed to load function '{self.function_path}' in step '{self.step_id}': {e}"
125
+ )
126
+
127
+ try:
128
+ logger.info(f"Calling async function: {self.function_path}")
129
+ result = func(state)
130
+ logger.info(f"Async function {self.function_path} returned: {result}")
131
+ return result
132
+ except Exception as e:
133
+ span.set_attribute("error", True)
134
+ span.set_attribute("error.type", type(e).__name__)
135
+ span.set_attribute("error.message", str(e))
136
+ raise RuntimeError(
137
+ f"Function '{self.function_path}' failed in step '{self.step_id}': {e}"
138
+ )
139
+
140
+ def _is_pending_result(self, result: Any) -> bool:
141
+ """Check if the function result indicates a pending async operation."""
142
+ if not isinstance(result, dict):
143
+ return False
144
+ return result.get("status") == "pending"
145
+
146
+ def _handle_sync_completion(
147
+ self,
148
+ state: Dict[str, Any],
149
+ result: Any,
150
+ span
151
+ ) -> Dict[str, Any]:
152
+ """Handle synchronous function completion (no async wait needed)."""
153
+ set_state_value(state, self.output_field, result)
154
+ self._track_computed_field(state)
155
+ return self._handle_routing(state, result, span)
156
+
157
+ def _handle_async_completion(
158
+ self,
159
+ state: Dict[str, Any],
160
+ async_result: Any,
161
+ span
162
+ ) -> Dict[str, Any]:
163
+ """Handle async operation completion after resume."""
164
+ set_state_value(state, self.output_field, async_result)
165
+ self._track_computed_field(state)
166
+ return self._handle_routing(state, async_result, span)
167
+
168
+ def _track_computed_field(self, state: Dict[str, Any]):
169
+ """Track this field as computed for rollback purposes."""
170
+ computed_fields = get_state_value(state, WorkflowKeys.COMPUTED_FIELDS, [])
171
+ if self.output_field not in computed_fields:
172
+ computed_fields.append(self.output_field)
173
+ set_state_value(state, WorkflowKeys.COMPUTED_FIELDS, computed_fields)
174
+
175
+ def _handle_routing(
176
+ self,
177
+ state: Dict[str, Any],
178
+ result: Any,
179
+ span
180
+ ) -> Dict[str, Any]:
181
+ """Determine next step based on transitions or default routing."""
182
+ if self.transitions:
183
+ return self._handle_transition_routing(state, result, span)
184
+ return self._handle_simple_routing(state, span)
185
+
186
+ def _handle_transition_routing(
187
+ self,
188
+ state: Dict[str, Any],
189
+ result: Any,
190
+ span
191
+ ) -> Dict[str, Any]:
192
+ """Route based on transition conditions matching the result."""
193
+ for transition in self.transitions:
194
+ check_value = result
195
+
196
+ # Support nested field references
197
+ if 'ref' in transition:
198
+ check_value = get_nested_value(result, transition['ref'])
199
+
200
+ condition = transition['condition']
201
+
202
+ # Support list of conditions
203
+ if isinstance(condition, list):
204
+ if check_value not in condition:
205
+ continue
206
+ elif check_value != condition:
207
+ continue
208
+
209
+ next_dest = transition['next']
210
+ logger.info(f"Async function matched transition, routing to {next_dest}")
211
+ span.add_event("transition.matched", {"next": next_dest})
212
+ self._set_status(state, next_dest)
213
+
214
+ if next_dest in self.engine_context.outcome_map:
215
+ self._set_outcome(state, next_dest)
216
+
217
+ return state
218
+
219
+ logger.warning(
220
+ f"No matching transition for async result '{result}' in step '{self.step_id}'"
221
+ )
222
+ span.add_event("transition.no_match", {"result": str(result)})
223
+ self._set_status(state, 'failed')
224
+ return state
225
+
226
+ def _handle_simple_routing(self, state: Dict[str, Any], span) -> Dict[str, Any]:
227
+ """Route to next step when no transitions are defined."""
228
+ self._set_status(state, 'success')
229
+
230
+ if self.next_step:
231
+ self._set_status(state, self.next_step)
232
+ span.add_event("routing.next_step", {"next": self.next_step})
233
+
234
+ if self.next_step in self.engine_context.outcome_map:
235
+ self._set_outcome(state, self.next_step)
236
+
237
+ return state
@@ -39,11 +39,17 @@ class CallFunctionStrategy(ActionStrategy):
39
39
  if 'mfa' in self.step_config:
40
40
  state['_mfa'] = state.get('_mfa', {})
41
41
  state['_mfa']['post_payload'] = dict(transactionId=str(uuid.uuid4()))
42
+ state['_mfa']['post_headers'] = {}
42
43
  state['_mfa_config'] = self.engine_context.mfa_config
43
44
  template_loader = self.engine_context.get_config_value("template_loader", Environment())
44
45
  for k, v in self.step_config['mfa']['payload'].items():
45
46
  state['_mfa']['post_payload'][k] = compile_values(template_loader, state, v)
46
47
 
48
+ # Process headers if provided
49
+ if 'headers' in self.step_config['mfa']:
50
+ for k, v in self.step_config['mfa']['headers'].items():
51
+ state['_mfa']['post_headers'][k] = compile_values(template_loader, state, v)
52
+
47
53
  def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
48
54
  from ..utils.tracing import trace_node_execution
49
55
 
@@ -3,6 +3,7 @@ from typing import Dict, Any, Type, Callable
3
3
  from .base import ActionStrategy
4
4
  from .call_function import CallFunctionStrategy
5
5
  from .collect_input import CollectInputStrategy
6
+ from .async_function import AsyncFunctionStrategy
6
7
  from ..core.constants import ActionType
7
8
  from ..utils.logger import logger
8
9
 
@@ -44,3 +45,4 @@ class NodeFactory:
44
45
 
45
46
  NodeFactory.register(ActionType.COLLECT_INPUT_WITH_AGENT.value, CollectInputStrategy)
46
47
  NodeFactory.register(ActionType.CALL_FUNCTION.value, CallFunctionStrategy)
48
+ NodeFactory.register(ActionType.CALL_ASYNC_FUNCTION.value, AsyncFunctionStrategy)
@@ -32,6 +32,10 @@ class WorkflowRouter:
32
32
  logger.info(f"Self-loop: {self.step_id} (collecting)")
33
33
  return self.step_id
34
34
 
35
+ if status == f'{self.step_id}_pending':
36
+ logger.info(f"Self-loop: {self.step_id} (async pending)")
37
+ return self.step_id
38
+
35
39
  if status == f'{self.step_id}_error' :
36
40
  logger.info(f"Error encountered in {self.step_id}, ending workflow")
37
41
  return END
@@ -73,7 +77,8 @@ class WorkflowRouter:
73
77
  def get_routing_map(self, collector_nodes: List[str]) -> Dict[str, str]:
74
78
  routing_map = {}
75
79
 
76
- if self.action == 'collect_input_with_agent':
80
+ # Self-loop for nodes that can interrupt (agent input or async)
81
+ if self.action in ('collect_input_with_agent', 'call_async_function'):
77
82
  routing_map[self.step_id] = self.step_id
78
83
 
79
84
  for transition in self.transitions:
soprano_sdk/tools.py CHANGED
@@ -2,14 +2,15 @@
2
2
  Workflow Tools - Wraps workflows as callable tools for agent frameworks
3
3
  """
4
4
  from __future__ import annotations
5
+ import json
5
6
  import uuid
6
- from typing import Optional, Dict, Any
7
+ from typing import Optional, Dict, Any, Union
7
8
  from .utils.logger import logger
8
9
 
9
10
  from langfuse.langchain import CallbackHandler
10
11
 
11
12
  from .core.engine import load_workflow
12
- from .core.constants import MFAConfig
13
+ from .core.constants import MFAConfig, InterruptType
13
14
 
14
15
 
15
16
  class WorkflowTool:
@@ -103,35 +104,56 @@ class WorkflowTool:
103
104
  if not final_state.next and self.checkpointer:
104
105
  self.checkpointer.delete_thread(thread_id)
105
106
 
106
- # If workflow needs user input, return structured interrupt data
107
+ # If workflow needs user input or async operation, return structured interrupt data
107
108
  if "__interrupt__" in result and result["__interrupt__"]:
109
+ interrupt_value = result["__interrupt__"][0].value
110
+
111
+ # Check if this is an async interrupt
112
+ if isinstance(interrupt_value, dict) and interrupt_value.get("type") == "async":
113
+ span.set_attribute("workflow.status", "async_interrupted")
114
+ span.set_attribute("async.step_id", interrupt_value.get("step_id", ""))
115
+ pending_metadata = json.dumps(interrupt_value.get("pending", {}))
116
+ return f"{InterruptType.ASYNC}|{thread_id}|{self.name}|{pending_metadata}"
117
+
118
+ # User input interrupt (existing behavior)
108
119
  span.set_attribute("workflow.status", "interrupted")
109
- prompt = result["__interrupt__"][0].value
110
- return f"__WORKFLOW_INTERRUPT__|{thread_id}|{self.name}|{prompt}"
120
+ prompt = interrupt_value
121
+ return f"{InterruptType.USER_INPUT}|{thread_id}|{self.name}|{prompt}"
111
122
 
112
123
  # Workflow completed without interrupting
113
124
  span.set_attribute("workflow.status", "completed")
114
125
  return self.engine.get_outcome_message(result)
115
126
 
116
- def resume(self, thread_id: str, user_message: str) -> str:
117
- """Resume an interrupted workflow with user input
127
+ def resume(
128
+ self,
129
+ thread_id: str,
130
+ resume_value: Union[str, Dict[str, Any]]
131
+ ) -> str:
132
+ """Resume an interrupted workflow with user input or async result
118
133
 
119
134
  Args:
120
135
  thread_id: Thread ID of the interrupted workflow
121
- user_message: User's response to the interrupt prompt
136
+ resume_value: User's response (str) or async operation result (dict)
122
137
 
123
138
  Returns:
124
- Either another interrupt prompt or final outcome message
139
+ Either another interrupt prompt/async metadata or final outcome message
125
140
  """
126
141
  from langgraph.types import Command
127
142
 
128
143
  config = {"configurable": {"thread_id": thread_id}}
129
- result = self.graph.invoke(Command(resume=user_message), config=config)
144
+ result = self.graph.invoke(Command(resume=resume_value), config=config)
130
145
 
131
- # Check if workflow needs more input
146
+ # Check if workflow needs more input or has another async operation
132
147
  if "__interrupt__" in result and result["__interrupt__"]:
133
- prompt = result["__interrupt__"][0].value
134
- return f"__WORKFLOW_INTERRUPT__|{thread_id}|{self.name}|{prompt}"
148
+ interrupt_value = result["__interrupt__"][0].value
149
+
150
+ # Check if this is an async interrupt
151
+ if isinstance(interrupt_value, dict) and interrupt_value.get("type") == "async":
152
+ pending_metadata = json.dumps(interrupt_value.get("pending", {}))
153
+ return f"{InterruptType.ASYNC}|{thread_id}|{self.name}|{pending_metadata}"
154
+
155
+ # User input interrupt
156
+ return f"{InterruptType.USER_INPUT}|{thread_id}|{self.name}|{interrupt_value}"
135
157
 
136
158
  # Workflow completed
137
159
  return self.engine.get_outcome_message(result)
@@ -73,7 +73,7 @@ WORKFLOW_SCHEMA = {
73
73
  },
74
74
  "action": {
75
75
  "type": "string",
76
- "enum": ["collect_input_with_agent", "call_function"],
76
+ "enum": ["collect_input_with_agent", "call_function", "call_async_function"],
77
77
  "description": "Action type"
78
78
  },
79
79
  "field": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soprano-sdk
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -1,24 +1,25 @@
1
1
  soprano_sdk/__init__.py,sha256=YZVl_SwQ0C-E_5_f1AwUe_hPcbgCt8k7k4_WAHM8vjE,243
2
2
  soprano_sdk/engine.py,sha256=EFK91iTHjp72otLN6Kg-yeLye2J3CAKN0QH4FI2taL8,14838
3
- soprano_sdk/tools.py,sha256=v2QGEaXxKzCBZCTdmhZXMXiVcwccJivdQdCxETkp-lQ,7656
3
+ soprano_sdk/tools.py,sha256=xsJbY1hZzhXbNRTAVj9M_2saU0oa7J8O9DaRGGuPf30,8832
4
4
  soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
5
5
  soprano_sdk/agents/adaptor.py,sha256=Cm02YKFclrESu-Qq4CTknCgU7KaA7Z_2FspnQDkEVfU,3214
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=7aTueLu2jLxl213Q5_BkNQ534RgEwf6lodQa_PaJFWs,5980
9
+ soprano_sdk/authenticators/mfa.py,sha256=Zl1dcwCmuwsAbruFMqguJ4lY0PPnC6v2EZ-xTPULX04,6098
10
10
  soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- soprano_sdk/core/constants.py,sha256=F7gL3hkpq5mZgbgiBcEERmzxYJ4zZG2ZFx1YQp5Z580,3130
11
+ soprano_sdk/core/constants.py,sha256=21ful8skHgDeDJJMdB_oJDw7Xq3lkbO6KWOhHuuLIa0,3330
12
12
  soprano_sdk/core/engine.py,sha256=UTFJimrgUzfdDZgc4rW5nvkOKevAEGW8C5Zr1D0tgcs,11270
13
13
  soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
14
14
  soprano_sdk/core/state.py,sha256=k8ojLfWgjES3p9XWMeGU5s4UK-Xa5T8mS4VtZzTrcDw,2961
15
15
  soprano_sdk/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ soprano_sdk/nodes/async_function.py,sha256=v6WujLKm8NXX2iAkJ7Gz_QIVCtWFrpC6nnPyyfuCxXs,9354
16
17
  soprano_sdk/nodes/base.py,sha256=idFyOGGPnjsASYnrOF_NIh7eFcSuJqw61EoVN_WCTaU,2360
17
- soprano_sdk/nodes/call_function.py,sha256=PdmSSVXkMT5ptxeUjmKqND-wvqbaXp1i61QzIzFOfgQ,5535
18
+ soprano_sdk/nodes/call_function.py,sha256=afYBmj5Aditbkvb_7gD3CsXBEEUohcsC1_cdHfcOunE,5847
18
19
  soprano_sdk/nodes/collect_input.py,sha256=lEltZOU5ALvc57q8I_4SjzzEapVejy9mS0E73Jf7-sk,23759
19
- soprano_sdk/nodes/factory.py,sha256=l-Gysfgnao-o2dphhnbjjxcH3ojZanZNYN3CBH9dDbA,1624
20
+ soprano_sdk/nodes/factory.py,sha256=IbBzT4FKBnYw5PuSo7uDONV3HSFtoyqjBQQtXtUY2IY,1756
20
21
  soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- soprano_sdk/routing/router.py,sha256=SrNciTIXXdC9bAbbO5bX7PN9mlRbITjr4RZdNm4jEVA,3450
22
+ soprano_sdk/routing/router.py,sha256=Z218r4BMbmlL9282ombutAoKsIs1WHZ2d5YHnbCeet8,3698
22
23
  soprano_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
24
  soprano_sdk/utils/function.py,sha256=yqkY4MlHOenv-Q3NciiovK1lamyrGQljpy6Q41wviy8,1216
24
25
  soprano_sdk/utils/logger.py,sha256=hMYaNHt5syGOXRkglTUKzkgfSbWerix_pHQntcYyep8,157
@@ -26,9 +27,9 @@ soprano_sdk/utils/template.py,sha256=MG_B9TMx1ShpnSGo7s7TO-VfQzuFByuRNhJTvZ668kM
26
27
  soprano_sdk/utils/tool.py,sha256=hWN826HIKmLdswLCTURLH8hWlb2WU0MB8nIUErbpB-8,1877
27
28
  soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,1991
28
29
  soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
29
- soprano_sdk/validation/schema.py,sha256=7B60_8xIt5bLXTYEGEDwU2DwmQn2f4QFyyIca2FYM9s,15420
30
+ soprano_sdk/validation/schema.py,sha256=SlC4sq-ueEg0p_8Uox_cgPj9S-0AEEiOOlA1Vsu0DsE,15443
30
31
  soprano_sdk/validation/validator.py,sha256=GaCvHvjwVe88Z8yatQsueiPnqtq1oo5uN75gogzpQT0,8940
31
- soprano_sdk-0.2.4.dist-info/METADATA,sha256=W-e-Si4pewUkpuWewKz_pmkWvUeRVR8HPwQ-Z-7tpTQ,11297
32
- soprano_sdk-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
33
- soprano_sdk-0.2.4.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
34
- soprano_sdk-0.2.4.dist-info/RECORD,,
32
+ soprano_sdk-0.2.6.dist-info/METADATA,sha256=Ykmgc9OD9Q5nBvGSD-z67oPU7Cy4jynpvhjZkwdPxzQ,11297
33
+ soprano_sdk-0.2.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ soprano_sdk-0.2.6.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
35
+ soprano_sdk-0.2.6.dist-info/RECORD,,