soprano-sdk 0.2.0__tar.gz → 0.2.1__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.
Files changed (96) hide show
  1. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/PKG-INFO +1 -1
  2. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/pyproject.toml +1 -1
  3. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/authenticators/mfa.py +2 -2
  4. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/uv.lock +67 -69
  5. soprano_sdk-0.2.0/soprano_sdk/nodes/call_api.py +0 -651
  6. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/.github/workflows/test_build_and_publish.yaml +0 -0
  7. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/.gitignore +0 -0
  8. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/.python-version +0 -0
  9. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/CLAUDE.md +0 -0
  10. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/LICENSE +0 -0
  11. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/README.md +0 -0
  12. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/framework_example.yaml +0 -0
  13. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/greeting_functions.py +0 -0
  14. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/greeting_workflow.yaml +0 -0
  15. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/main.py +0 -0
  16. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/persistence/README.md +0 -0
  17. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/persistence/conversation_based.py +0 -0
  18. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/persistence/entity_based.py +0 -0
  19. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/persistence/mongodb_demo.py +0 -0
  20. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/return_functions.py +0 -0
  21. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/return_workflow.yaml +0 -0
  22. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/structured_output_example.yaml +0 -0
  23. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/README.md +0 -0
  24. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  25. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  26. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/tools/__init__.py +0 -0
  27. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/tools/crewai_tools.py +0 -0
  28. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/tools/langgraph_tools.py +0 -0
  29. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/supervisors/workflow_tools.py +0 -0
  30. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/tools/__init__.py +0 -0
  31. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/tools/address.py +0 -0
  32. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/examples/validator.py +0 -0
  33. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/langgraph_demo.py +0 -0
  34. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/langgraph_selfloop_demo.py +0 -0
  35. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/langgraph_v.py +0 -0
  36. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/main.py +0 -0
  37. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/return_fsm.excalidraw +0 -0
  38. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/return_state_machine.png +0 -0
  39. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/legacy/ui.py +0 -0
  40. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/scripts/visualize_workflow.py +0 -0
  41. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/scripts/workflow_demo.py +0 -0
  42. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/scripts/workflow_demo_ui.py +0 -0
  43. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/__init__.py +0 -0
  44. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/agents/__init__.py +0 -0
  45. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/agents/adaptor.py +0 -0
  46. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/agents/factory.py +0 -0
  47. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/agents/structured_output.py +0 -0
  48. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/authenticators/__init__.py +0 -0
  49. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/core/__init__.py +0 -0
  50. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/core/constants.py +0 -0
  51. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/core/engine.py +0 -0
  52. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/core/rollback_strategies.py +0 -0
  53. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/core/state.py +0 -0
  54. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/engine.py +0 -0
  55. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/nodes/__init__.py +0 -0
  56. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/nodes/base.py +0 -0
  57. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/nodes/call_function.py +0 -0
  58. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/nodes/collect_input.py +0 -0
  59. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/nodes/factory.py +0 -0
  60. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/routing/__init__.py +0 -0
  61. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/routing/router.py +0 -0
  62. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/tools.py +0 -0
  63. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/utils/__init__.py +0 -0
  64. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/utils/function.py +0 -0
  65. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/utils/logger.py +0 -0
  66. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/utils/template.py +0 -0
  67. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/utils/tool.py +0 -0
  68. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/utils/tracing.py +0 -0
  69. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/validation/__init__.py +0 -0
  70. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/validation/schema.py +0 -0
  71. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/soprano_sdk/validation/validator.py +0 -0
  72. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/debug_jinja2.py +0 -0
  73. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_agent_factory.py +0 -0
  74. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_collect_input_refactor.py +0 -0
  75. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_external_values.py +0 -0
  76. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_inputs_validation.py +0 -0
  77. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_jinja2_path.py +0 -0
  78. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_jinja2_standalone.py +0 -0
  79. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_persistence.py +0 -0
  80. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_structured_output.py +0 -0
  81. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/tests/test_transition_routing.py +0 -0
  82. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/todo.md +0 -0
  83. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/.eslintrc.cjs +0 -0
  84. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/.gitignore +0 -0
  85. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/README.md +0 -0
  86. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/index.html +0 -0
  87. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/package-lock.json +0 -0
  88. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/package.json +0 -0
  89. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/App.jsx +0 -0
  90. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/CustomNode.jsx +0 -0
  91. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  92. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  93. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  94. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/assets/react.svg +0 -0
  95. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/src/main.jsx +0 -0
  96. {soprano_sdk-0.2.0 → soprano_sdk-0.2.1}/workflow-visualizer/vite.config.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soprano-sdk
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "soprano-sdk"
7
- version = "0.2.0"
7
+ version = "0.2.1"
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"
@@ -103,7 +103,7 @@ class MFANodeConfig:
103
103
  return dict(
104
104
  id=f"{source_node}_mfa_start",
105
105
  action="call_function",
106
- function="conversational_sop.authenticators.mfa.enforce_mfa_if_required",
106
+ function="soprano_sdk.authenticators.mfa.enforce_mfa_if_required",
107
107
  output=f"{source_node}_mfa_start",
108
108
  mfa=mfa,
109
109
  transitions=[
@@ -127,7 +127,7 @@ class MFANodeConfig:
127
127
  description="Collect Input for MFA value",
128
128
  field=input_field_name,
129
129
  max_attempts=3,
130
- validator="conversational_sop.authenticators.mfa.mfa_validate_user_input",
130
+ validator="soprano_sdk.authenticators.mfa.mfa_validate_user_input",
131
131
  agent=dict(
132
132
  name="MFA Input Data Collector",
133
133
  model=model_name,
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 2
3
3
  requires-python = ">=3.12"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.13'",
@@ -704,6 +704,72 @@ wheels = [
704
704
  { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
705
705
  ]
706
706
 
707
+ [[package]]
708
+ name = "conversational-sop-framework"
709
+ version = "0.1.83"
710
+ source = { editable = "." }
711
+ dependencies = [
712
+ { name = "agno" },
713
+ { name = "crewai" },
714
+ { name = "jsonschema" },
715
+ { name = "langchain" },
716
+ { name = "langchain-community" },
717
+ { name = "langchain-core" },
718
+ { name = "langchain-openai" },
719
+ { name = "langfuse" },
720
+ { name = "langgraph" },
721
+ { name = "openai" },
722
+ { name = "pydantic" },
723
+ { name = "pydantic-ai" },
724
+ { name = "pyyaml" },
725
+ ]
726
+
727
+ [package.optional-dependencies]
728
+ dev = [
729
+ { name = "gradio" },
730
+ { name = "pytest" },
731
+ ]
732
+ persistence = [
733
+ { name = "langgraph-checkpoint-mongodb" },
734
+ { name = "pymongo" },
735
+ ]
736
+ supervisors = [
737
+ { name = "crewai" },
738
+ { name = "langchain-openai" },
739
+ ]
740
+
741
+ [package.dev-dependencies]
742
+ dev = [
743
+ { name = "conversational-sop-framework" },
744
+ ]
745
+
746
+ [package.metadata]
747
+ requires-dist = [
748
+ { name = "agno", specifier = ">=2.0.7" },
749
+ { name = "crewai", specifier = ">=0.186.1" },
750
+ { name = "crewai", marker = "extra == 'supervisors'", specifier = ">=0.1.0" },
751
+ { name = "gradio", marker = "extra == 'dev'", specifier = ">=5.46.0" },
752
+ { name = "jsonschema", specifier = ">=4.0.0" },
753
+ { name = "langchain", specifier = ">=1.0.7" },
754
+ { name = "langchain-community", specifier = ">=0.4.1" },
755
+ { name = "langchain-core", specifier = ">=0.3.67" },
756
+ { name = "langchain-openai", specifier = ">=1.0.3" },
757
+ { name = "langchain-openai", marker = "extra == 'supervisors'", specifier = ">=0.3.34" },
758
+ { name = "langfuse", specifier = ">=3.10.1" },
759
+ { name = "langgraph", specifier = "==1.0.2" },
760
+ { name = "langgraph-checkpoint-mongodb", marker = "extra == 'persistence'", specifier = ">=0.2.0" },
761
+ { name = "openai", specifier = ">=1.92.1" },
762
+ { name = "pydantic", specifier = ">=2.0.0" },
763
+ { name = "pydantic-ai", specifier = ">=1.22.0" },
764
+ { name = "pymongo", marker = "extra == 'persistence'", specifier = ">=4.0.0" },
765
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
766
+ { name = "pyyaml", specifier = ">=6.0" },
767
+ ]
768
+ provides-extras = ["dev", "persistence", "supervisors"]
769
+
770
+ [package.metadata.requires-dev]
771
+ dev = [{ name = "conversational-sop-framework", editable = "." }]
772
+
707
773
  [[package]]
708
774
  name = "crewai"
709
775
  version = "1.4.1"
@@ -4227,74 +4293,6 @@ wheels = [
4227
4293
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
4228
4294
  ]
4229
4295
 
4230
- [[package]]
4231
- name = "soprano-sdk"
4232
- version = "0.1.99"
4233
- source = { editable = "." }
4234
- dependencies = [
4235
- { name = "agno" },
4236
- { name = "crewai" },
4237
- { name = "jsonschema" },
4238
- { name = "langchain" },
4239
- { name = "langchain-community" },
4240
- { name = "langchain-core" },
4241
- { name = "langchain-openai" },
4242
- { name = "langfuse" },
4243
- { name = "langgraph" },
4244
- { name = "openai" },
4245
- { name = "pydantic" },
4246
- { name = "pydantic-ai" },
4247
- { name = "pytest" },
4248
- { name = "pyyaml" },
4249
- ]
4250
-
4251
- [package.optional-dependencies]
4252
- dev = [
4253
- { name = "gradio" },
4254
- { name = "pytest" },
4255
- ]
4256
- persistence = [
4257
- { name = "langgraph-checkpoint-mongodb" },
4258
- { name = "pymongo" },
4259
- ]
4260
- supervisors = [
4261
- { name = "crewai" },
4262
- { name = "langchain-openai" },
4263
- ]
4264
-
4265
- [package.dev-dependencies]
4266
- dev = [
4267
- { name = "soprano-sdk" },
4268
- ]
4269
-
4270
- [package.metadata]
4271
- requires-dist = [
4272
- { name = "agno", specifier = ">=2.0.7" },
4273
- { name = "crewai", specifier = ">=0.186.1" },
4274
- { name = "crewai", marker = "extra == 'supervisors'", specifier = ">=0.1.0" },
4275
- { name = "gradio", marker = "extra == 'dev'", specifier = ">=5.46.0" },
4276
- { name = "jsonschema", specifier = ">=4.0.0" },
4277
- { name = "langchain", specifier = ">=1.0.7" },
4278
- { name = "langchain-community", specifier = ">=0.4.1" },
4279
- { name = "langchain-core", specifier = ">=0.3.67" },
4280
- { name = "langchain-openai", specifier = ">=1.0.3" },
4281
- { name = "langchain-openai", marker = "extra == 'supervisors'", specifier = ">=0.3.34" },
4282
- { name = "langfuse", specifier = ">=3.10.1" },
4283
- { name = "langgraph", specifier = "==1.0.2" },
4284
- { name = "langgraph-checkpoint-mongodb", marker = "extra == 'persistence'", specifier = ">=0.2.0" },
4285
- { name = "openai", specifier = ">=1.92.1" },
4286
- { name = "pydantic", specifier = ">=2.0.0" },
4287
- { name = "pydantic-ai", specifier = ">=1.22.0" },
4288
- { name = "pymongo", marker = "extra == 'persistence'", specifier = ">=4.0.0" },
4289
- { name = "pytest", specifier = ">=9.0.1" },
4290
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
4291
- { name = "pyyaml", specifier = ">=6.0" },
4292
- ]
4293
- provides-extras = ["dev", "persistence", "supervisors"]
4294
-
4295
- [package.metadata.requires-dev]
4296
- dev = [{ name = "soprano-sdk", editable = "." }]
4297
-
4298
4296
  [[package]]
4299
4297
  name = "sqlalchemy"
4300
4298
  version = "2.0.44"
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes