soprano-sdk 0.2.6__tar.gz → 0.2.7__tar.gz
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-0.2.6 → soprano_sdk-0.2.7}/PKG-INFO +1 -1
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/pyproject.toml +1 -1
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/authenticators/mfa.py +31 -9
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/core/constants.py +4 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/core/engine.py +15 -2
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_mfa_scenarios.py +5 -8
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/.github/workflows/test_build_and_publish.yaml +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/.gitignore +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/.python-version +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/CLAUDE.md +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/LICENSE +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/README.md +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/concert_booking/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/concert_booking/booking_helpers.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/concert_booking/concert_ticket_booking.yaml +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/framework_example.yaml +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/greeting_functions.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/greeting_workflow.yaml +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/main.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/persistence/README.md +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/persistence/conversation_based.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/persistence/entity_based.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/persistence/mongodb_demo.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/return_functions.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/return_workflow.yaml +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/structured_output_example.yaml +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/README.md +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/crewai_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/tools/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/tools/crewai_tools.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/tools/langgraph_tools.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/supervisors/workflow_tools.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/tools/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/tools/address.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/validator.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/langgraph_demo.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/langgraph_selfloop_demo.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/langgraph_v.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/main.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/return_fsm.excalidraw +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/return_state_machine.png +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/legacy/ui.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/scripts/visualize_workflow.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/scripts/workflow_demo.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/scripts/workflow_demo_ui.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/agents/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/agents/adaptor.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/agents/factory.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/agents/structured_output.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/authenticators/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/core/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/core/rollback_strategies.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/core/state.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/engine.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/nodes/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/nodes/async_function.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/nodes/base.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/nodes/call_function.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/nodes/collect_input.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/nodes/factory.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/routing/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/routing/router.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/tools.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/utils/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/utils/function.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/utils/logger.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/utils/template.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/utils/tool.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/utils/tracing.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/validation/__init__.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/validation/schema.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/soprano_sdk/validation/validator.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/debug_jinja2.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_agent_factory.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_async_function.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_collect_input_refactor.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_external_values.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_inputs_validation.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_jinja2_path.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_jinja2_standalone.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_persistence.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_structured_output.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/tests/test_transition_routing.py +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/todo.md +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/uv.lock +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/.eslintrc.cjs +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/.gitignore +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/README.md +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/index.html +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/package-lock.json +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/package.json +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/App.jsx +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/CustomNode.jsx +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/assets/react.svg +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/src/main.jsx +0 -0
- {soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/workflow-visualizer/vite.config.js +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "soprano-sdk"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "YAML-driven workflow engine with AI agent integration for building conversational SOPs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -154,25 +154,47 @@ class MFANodeConfig:
|
|
|
154
154
|
model=model_name,
|
|
155
155
|
initial_message="{{_mfa.message}}",
|
|
156
156
|
instructions="""
|
|
157
|
-
You are an authentication value extractor. Your job is to identify and extract MFA codes from user input.
|
|
157
|
+
You are an authentication value extractor. Your job is to identify and extract MFA codes from user input, or detect if the user wants to cancel the authentication flow.
|
|
158
158
|
|
|
159
159
|
**Task:**
|
|
160
|
-
- Read the user's message
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
- Read the user's message carefully
|
|
161
|
+
- First, check if the user wants to cancel, stop, or exit the authentication process
|
|
162
|
+
- If they want to cancel, output: MFA_CANCELLED:
|
|
163
|
+
- Otherwise, extract ONLY the OTP/MFA code value and output in the format shown below
|
|
164
|
+
|
|
165
|
+
**Cancellation Detection:**
|
|
166
|
+
If the user expresses any intent to cancel, stop, exit, abort, or quit the authentication process, respond with: MFA_CANCELLED
|
|
167
|
+
|
|
168
|
+
Examples of cancellation phrases:
|
|
169
|
+
* "cancel" → MFA_CANCELLED:
|
|
170
|
+
* "I want to stop" → MFA_CANCELLED:
|
|
171
|
+
* "exit" → MFA_CANCELLED:
|
|
172
|
+
* "nevermind" → MFA_CANCELLED:
|
|
173
|
+
* "I don't want to continue" → MFA_CANCELLED:
|
|
174
|
+
* "stop this" → MFA_CANCELLED:
|
|
175
|
+
* "forget it" → MFA_CANCELLED:
|
|
176
|
+
* "abort" → MFA_CANCELLED:
|
|
177
|
+
* "quit" → MFA_CANCELLED:
|
|
178
|
+
|
|
179
|
+
**OTP Capture Examples:**
|
|
180
|
+
* "1234" → MFA_CAPTURED:1234
|
|
181
|
+
* "2345e" → MFA_CAPTURED:2345e
|
|
182
|
+
* "the code is 567890" → MFA_CAPTURED:567890
|
|
183
|
+
* "my otp is 123456" → MFA_CAPTURED:123456
|
|
167
184
|
|
|
168
185
|
**Output Format:**
|
|
169
|
-
MFA_CAPTURED:<
|
|
186
|
+
- For OTP/MFA codes: MFA_CAPTURED:<otp_value>
|
|
187
|
+
- For cancellation: MFA_CANCELLED:
|
|
170
188
|
|
|
171
189
|
"""),
|
|
172
190
|
transitions=[
|
|
173
191
|
dict(
|
|
174
192
|
pattern="MFA_CAPTURED:",
|
|
175
193
|
next=next_node
|
|
194
|
+
),
|
|
195
|
+
dict(
|
|
196
|
+
pattern="MFA_CANCELLED:",
|
|
197
|
+
next="mfa_cancelled"
|
|
176
198
|
)
|
|
177
199
|
]
|
|
178
200
|
)
|
|
@@ -114,6 +114,10 @@ class MFAConfig(BaseSettings):
|
|
|
114
114
|
default=30,
|
|
115
115
|
description="API request timeout in seconds"
|
|
116
116
|
)
|
|
117
|
+
mfa_cancelled_message: str = Field(
|
|
118
|
+
default="Authentication has been cancelled.",
|
|
119
|
+
description="Message to display when user cancels MFA authentication"
|
|
120
|
+
)
|
|
117
121
|
|
|
118
122
|
model_config = SettingsConfigDict(
|
|
119
123
|
case_sensitive=False,
|
|
@@ -39,8 +39,7 @@ class WorkflowEngine:
|
|
|
39
39
|
self.step_map = {step['id']: step for step in self.steps}
|
|
40
40
|
self.mfa_config = (mfa_config or MFAConfig()) if self.mfa_validator_steps else None
|
|
41
41
|
self.data_fields = self.load_data()
|
|
42
|
-
|
|
43
|
-
self.outcomes = self.config['outcomes']
|
|
42
|
+
self.outcomes = self.load_outcomes()
|
|
44
43
|
self.metadata = self.config.get('metadata', {})
|
|
45
44
|
|
|
46
45
|
self.StateType = create_state_model(self.data_fields)
|
|
@@ -260,6 +259,20 @@ class WorkflowEngine:
|
|
|
260
259
|
)
|
|
261
260
|
return data
|
|
262
261
|
|
|
262
|
+
def load_outcomes(self):
|
|
263
|
+
outcomes: list = self.config['outcomes']
|
|
264
|
+
|
|
265
|
+
if self.mfa_config:
|
|
266
|
+
mfa_cancelled_outcome = {
|
|
267
|
+
'id': 'mfa_cancelled',
|
|
268
|
+
'type': 'failure',
|
|
269
|
+
'message': self.mfa_config.mfa_cancelled_message
|
|
270
|
+
}
|
|
271
|
+
outcomes.append(mfa_cancelled_outcome)
|
|
272
|
+
logger.info(f"Auto-generated 'mfa_cancelled' outcome with message: {self.mfa_config.mfa_cancelled_message}")
|
|
273
|
+
|
|
274
|
+
return outcomes
|
|
275
|
+
|
|
263
276
|
|
|
264
277
|
def load_workflow(yaml_path: str, checkpointer=None, config=None, mfa_config: Optional[MFAConfig] = None) -> Tuple[CompiledStateGraph, WorkflowEngine]:
|
|
265
278
|
"""
|
|
@@ -447,7 +447,7 @@ def test_mfa_default_max_attempts():
|
|
|
447
447
|
|
|
448
448
|
assert max_attempts == 3, \
|
|
449
449
|
f"Expected default max_attempts=3, got {max_attempts}"
|
|
450
|
-
print(
|
|
450
|
+
print(" ✅ Correctly uses default max_attempts=3")
|
|
451
451
|
|
|
452
452
|
print("\n✅ TEST PASSED: Default max_attempts value verified")
|
|
453
453
|
print("=" * 80)
|
|
@@ -466,7 +466,6 @@ def test_mfa_custom_max_attempts():
|
|
|
466
466
|
|
|
467
467
|
# Create a temporary YAML with custom max_attempts
|
|
468
468
|
import tempfile
|
|
469
|
-
import shutil
|
|
470
469
|
|
|
471
470
|
yaml_path = os.path.join(get_examples_dir(), "concert_ticket_booking.yaml")
|
|
472
471
|
|
|
@@ -511,7 +510,7 @@ def test_mfa_custom_max_attempts():
|
|
|
511
510
|
|
|
512
511
|
assert max_attempts == 5, \
|
|
513
512
|
f"Expected custom max_attempts=5, got {max_attempts}"
|
|
514
|
-
print(
|
|
513
|
+
print(" ✅ Correctly uses custom max_attempts=5")
|
|
515
514
|
|
|
516
515
|
print("\n✅ TEST PASSED: Custom max_attempts value applied correctly")
|
|
517
516
|
print("=" * 80)
|
|
@@ -579,7 +578,7 @@ def test_mfa_custom_error_message():
|
|
|
579
578
|
|
|
580
579
|
assert error_message == custom_error, \
|
|
581
580
|
f"Expected custom error message, got {error_message}"
|
|
582
|
-
print(
|
|
581
|
+
print(" ✅ Correctly uses custom error message")
|
|
583
582
|
|
|
584
583
|
print("\n✅ TEST PASSED: Custom error message applied correctly")
|
|
585
584
|
print("=" * 80)
|
|
@@ -656,7 +655,7 @@ def test_mfa_custom_max_attempts_and_error():
|
|
|
656
655
|
|
|
657
656
|
assert error_message == custom_error, \
|
|
658
657
|
f"Expected custom error message, got {error_message}"
|
|
659
|
-
print(
|
|
658
|
+
print(" ✅ Correctly uses custom error message")
|
|
660
659
|
|
|
661
660
|
print("\n✅ TEST PASSED: Both custom values applied correctly")
|
|
662
661
|
print("=" * 80)
|
|
@@ -742,8 +741,6 @@ def test_mfa_custom_headers_with_jinja():
|
|
|
742
741
|
assert process_payment_step in engine.list_steps(), \
|
|
743
742
|
f"Expected {process_payment_step} step to exist"
|
|
744
743
|
|
|
745
|
-
step_info = engine.get_step_info(process_payment_step)
|
|
746
|
-
|
|
747
744
|
# Check the MFA start node - this is where the MFA config is stored
|
|
748
745
|
process_payment_mfa_start = 'process_payment_mfa_start'
|
|
749
746
|
assert process_payment_mfa_start in engine.list_steps(), \
|
|
@@ -756,7 +753,7 @@ def test_mfa_custom_headers_with_jinja():
|
|
|
756
753
|
assert 'headers' in mfa_start_info['mfa'], "MFA config should have headers"
|
|
757
754
|
|
|
758
755
|
headers_config = mfa_start_info['mfa']['headers']
|
|
759
|
-
print(
|
|
756
|
+
print("\nHeaders defined in MFA config:")
|
|
760
757
|
for key, value in headers_config.items():
|
|
761
758
|
print(f" {key}: {value}")
|
|
762
759
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soprano_sdk-0.2.6 → soprano_sdk-0.2.7}/examples/concert_booking/concert_ticket_booking.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|