abstractflow 0.1.0__py3-none-any.whl → 0.3.0__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.
Files changed (34) hide show
  1. abstractflow/__init__.py +75 -95
  2. abstractflow/__main__.py +2 -0
  3. abstractflow/adapters/__init__.py +11 -0
  4. abstractflow/adapters/agent_adapter.py +124 -0
  5. abstractflow/adapters/control_adapter.py +615 -0
  6. abstractflow/adapters/effect_adapter.py +645 -0
  7. abstractflow/adapters/event_adapter.py +307 -0
  8. abstractflow/adapters/function_adapter.py +97 -0
  9. abstractflow/adapters/subflow_adapter.py +74 -0
  10. abstractflow/adapters/variable_adapter.py +317 -0
  11. abstractflow/cli.py +2 -0
  12. abstractflow/compiler.py +2027 -0
  13. abstractflow/core/__init__.py +5 -0
  14. abstractflow/core/flow.py +247 -0
  15. abstractflow/py.typed +2 -0
  16. abstractflow/runner.py +348 -0
  17. abstractflow/visual/__init__.py +43 -0
  18. abstractflow/visual/agent_ids.py +29 -0
  19. abstractflow/visual/builtins.py +789 -0
  20. abstractflow/visual/code_executor.py +214 -0
  21. abstractflow/visual/event_ids.py +33 -0
  22. abstractflow/visual/executor.py +2789 -0
  23. abstractflow/visual/interfaces.py +347 -0
  24. abstractflow/visual/models.py +252 -0
  25. abstractflow/visual/session_runner.py +168 -0
  26. abstractflow/visual/workspace_scoped_tools.py +261 -0
  27. abstractflow-0.3.0.dist-info/METADATA +413 -0
  28. abstractflow-0.3.0.dist-info/RECORD +32 -0
  29. {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/licenses/LICENSE +2 -0
  30. abstractflow-0.1.0.dist-info/METADATA +0 -238
  31. abstractflow-0.1.0.dist-info/RECORD +0 -10
  32. {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/WHEEL +0 -0
  33. {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/entry_points.txt +0 -0
  34. {abstractflow-0.1.0.dist-info → abstractflow-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ """Sandboxed Python code execution for visual `Code` nodes.
2
+
3
+ This is a host-side utility used by the visual workflow compiler. It is kept in
4
+ the `abstractflow` package so visual workflows can be executed from other hosts
5
+ without importing the web backend implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ from typing import Any, Callable, Dict
12
+
13
+ # Try to import RestrictedPython, fall back to basic execution if not available
14
+ try:
15
+ from RestrictedPython import compile_restricted, safe_builtins
16
+ from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
17
+ from RestrictedPython.Guards import guarded_iter_unpack_sequence
18
+
19
+ RESTRICTED_PYTHON_AVAILABLE = True
20
+ except ImportError: # pragma: no cover
21
+ RESTRICTED_PYTHON_AVAILABLE = False
22
+
23
+
24
+ class CodeExecutionError(Exception):
25
+ """Error during code execution."""
26
+
27
+
28
+ def validate_code(code: str) -> None:
29
+ """Validate Python code for safety.
30
+
31
+ Raises:
32
+ CodeExecutionError: If code contains disallowed constructs.
33
+ """
34
+ try:
35
+ tree = ast.parse(code)
36
+ except SyntaxError as e:
37
+ raise CodeExecutionError(f"Syntax error: {e}") from e
38
+
39
+ for node in ast.walk(tree):
40
+ # Disallow imports
41
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
42
+ raise CodeExecutionError("Imports are not allowed")
43
+
44
+ # Disallow exec/eval
45
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
46
+ if node.func.id in ("exec", "eval", "compile", "__import__"):
47
+ raise CodeExecutionError(f"'{node.func.id}' is not allowed")
48
+
49
+ # Disallow dunder attributes
50
+ if isinstance(node, ast.Attribute) and node.attr.startswith("__") and node.attr.endswith("__"):
51
+ raise CodeExecutionError(f"Access to dunder attributes ('{node.attr}') is not allowed")
52
+
53
+
54
+ def create_code_handler(code: str, function_name: str = "transform") -> Callable[[Any], Any]:
55
+ """Create a handler function from user-provided Python code.
56
+
57
+ The code should define a function that takes input data and returns a result.
58
+ """
59
+ validate_code(code)
60
+
61
+ if RESTRICTED_PYTHON_AVAILABLE:
62
+ return _create_restricted_handler(code, function_name)
63
+ return _create_basic_handler(code, function_name)
64
+
65
+
66
+ def _create_restricted_handler(code: str, function_name: str) -> Callable[[Any], Any]:
67
+ """Create handler using RestrictedPython for sandboxed execution."""
68
+ byte_code = compile_restricted(code, filename="<user_code>", mode="exec")
69
+
70
+ if getattr(byte_code, "errors", None):
71
+ errors = getattr(byte_code, "errors", None)
72
+ if isinstance(errors, list) and errors:
73
+ raise CodeExecutionError(f"Compilation errors: {'; '.join(errors)}")
74
+
75
+ def handler(input_data: Any) -> Any:
76
+ restricted_globals = {
77
+ "__builtins__": safe_builtins,
78
+ "_getiter_": default_guarded_getiter,
79
+ "_getitem_": default_guarded_getitem,
80
+ "_iter_unpack_sequence_": guarded_iter_unpack_sequence,
81
+ # Allow some safe built-ins
82
+ "len": len,
83
+ "str": str,
84
+ "int": int,
85
+ "float": float,
86
+ "bool": bool,
87
+ "list": list,
88
+ "dict": dict,
89
+ "tuple": tuple,
90
+ "set": set,
91
+ "range": range,
92
+ "enumerate": enumerate,
93
+ "zip": zip,
94
+ "map": map,
95
+ "filter": filter,
96
+ "sorted": sorted,
97
+ "reversed": reversed,
98
+ "min": min,
99
+ "max": max,
100
+ "sum": sum,
101
+ "abs": abs,
102
+ "round": round,
103
+ "isinstance": isinstance,
104
+ "type": type,
105
+ "print": lambda *args, **kwargs: None, # Silent print
106
+ }
107
+
108
+ local_vars: Dict[str, Any] = {}
109
+
110
+ try:
111
+ exec(byte_code, restricted_globals, local_vars)
112
+ except Exception as e:
113
+ raise CodeExecutionError(f"Execution error: {e}") from e
114
+
115
+ # `exec(..., globals, locals)` stores definitions in `locals`, but functions
116
+ # resolve globals against the `globals` dict. Make user-defined helpers
117
+ # (and other top-level values) available to `transform`.
118
+ reserved = {"__builtins__", "_getiter_", "_getitem_", "_iter_unpack_sequence_"}
119
+ for name, value in local_vars.items():
120
+ if name in reserved:
121
+ continue
122
+ if name.startswith("__") and name.endswith("__"):
123
+ continue
124
+ if name not in restricted_globals:
125
+ restricted_globals[name] = value
126
+
127
+ func = local_vars.get(function_name)
128
+ if func is None:
129
+ raise CodeExecutionError(f"Function '{function_name}' not defined in code")
130
+ if not callable(func):
131
+ raise CodeExecutionError(f"'{function_name}' is not a callable function")
132
+
133
+ try:
134
+ return func(input_data)
135
+ except Exception as e:
136
+ raise CodeExecutionError(f"Runtime error: {e}") from e
137
+
138
+ return handler
139
+
140
+
141
+ def _create_basic_handler(code: str, function_name: str) -> Callable[[Any], Any]:
142
+ """Create handler with basic (less secure) execution.
143
+
144
+ Used as fallback when RestrictedPython is not available.
145
+ """
146
+ try:
147
+ byte_code = compile(code, filename="<user_code>", mode="exec")
148
+ except SyntaxError as e:
149
+ raise CodeExecutionError(f"Syntax error: {e}") from e
150
+
151
+ def handler(input_data: Any) -> Any:
152
+ limited_globals = {
153
+ "__builtins__": {
154
+ "len": len,
155
+ "str": str,
156
+ "int": int,
157
+ "float": float,
158
+ "bool": bool,
159
+ "list": list,
160
+ "dict": dict,
161
+ "tuple": tuple,
162
+ "set": set,
163
+ "range": range,
164
+ "enumerate": enumerate,
165
+ "zip": zip,
166
+ "map": map,
167
+ "filter": filter,
168
+ "sorted": sorted,
169
+ "reversed": reversed,
170
+ "min": min,
171
+ "max": max,
172
+ "sum": sum,
173
+ "abs": abs,
174
+ "round": round,
175
+ "isinstance": isinstance,
176
+ "type": type,
177
+ "print": lambda *args, **kwargs: None,
178
+ "True": True,
179
+ "False": False,
180
+ "None": None,
181
+ }
182
+ }
183
+
184
+ local_vars: Dict[str, Any] = {}
185
+
186
+ try:
187
+ exec(byte_code, limited_globals, local_vars)
188
+ except Exception as e:
189
+ raise CodeExecutionError(f"Execution error: {e}") from e
190
+
191
+ # Keep the same semantics as normal Python modules: helper functions and
192
+ # top-level constants defined alongside `transform()` should be visible
193
+ # at runtime. Avoid letting user code replace `__builtins__`.
194
+ reserved = {"__builtins__"}
195
+ for name, value in local_vars.items():
196
+ if name in reserved:
197
+ continue
198
+ if name.startswith("__") and name.endswith("__"):
199
+ continue
200
+ if name not in limited_globals:
201
+ limited_globals[name] = value
202
+
203
+ func = local_vars.get(function_name)
204
+ if func is None:
205
+ raise CodeExecutionError(f"Function '{function_name}' not defined in code")
206
+ if not callable(func):
207
+ raise CodeExecutionError(f"'{function_name}' is not a callable function")
208
+
209
+ try:
210
+ return func(input_data)
211
+ except Exception as e:
212
+ raise CodeExecutionError(f"Runtime error: {e}") from e
213
+
214
+ return handler
@@ -0,0 +1,33 @@
1
+ """Deterministic workflow IDs for VisualFlow custom event listeners.
2
+
3
+ Visual "On Event" nodes are compiled into dedicated listener workflows and started
4
+ alongside the main workflow run.
5
+
6
+ IDs must be stable across hosts so a VisualFlow JSON document can be executed
7
+ outside the web editor (CLI, AbstractCode, third-party apps).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+
15
+ _SAFE_ID_RE = re.compile(r"[^a-zA-Z0-9_-]+")
16
+
17
+
18
+ def _sanitize(value: str) -> str:
19
+ value = str(value or "").strip()
20
+ if not value:
21
+ return "unknown"
22
+ value = _SAFE_ID_RE.sub("_", value)
23
+ return value or "unknown"
24
+
25
+
26
+ def visual_event_listener_workflow_id(*, flow_id: str, node_id: str) -> str:
27
+ """Return the workflow_id used for a VisualFlow `on_event` listener workflow."""
28
+ return f"visual_event_listener_{_sanitize(flow_id)}_{_sanitize(node_id)}"
29
+
30
+
31
+
32
+
33
+