pynode-flow 0.2.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.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow Engine for managing nodes and executing workflows.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Dict, List, Any, Type, Optional
|
|
8
|
+
|
|
9
|
+
from pynode.nodes.base_node import BaseNode
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkflowEngine:
|
|
15
|
+
"""
|
|
16
|
+
Manages a collection of nodes and their connections.
|
|
17
|
+
Handles workflow execution and lifecycle.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.nodes: Dict[str, BaseNode] = {}
|
|
22
|
+
self.node_types: Dict[str, Type[BaseNode]] = {}
|
|
23
|
+
self.running = False
|
|
24
|
+
self._lock = threading.RLock() # Use RLock (reentrant lock) to allow nested locking
|
|
25
|
+
self._system_error_node = None # System error node that's always present
|
|
26
|
+
|
|
27
|
+
def register_node_type(self, node_class: Type[BaseNode]):
|
|
28
|
+
"""
|
|
29
|
+
Register a node type that can be instantiated.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
node_class: A class that inherits from BaseNode
|
|
33
|
+
"""
|
|
34
|
+
self.node_types[node_class.__name__] = node_class
|
|
35
|
+
|
|
36
|
+
def create_node(self, node_type: str, node_id: Optional[str] = None,
|
|
37
|
+
name: str = "", config: Optional[Dict[str, Any]] = None) -> BaseNode:
|
|
38
|
+
"""
|
|
39
|
+
Create a new node instance and add it to the workflow.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
node_type: The type of node to create (must be registered)
|
|
43
|
+
node_id: Optional unique ID for the node
|
|
44
|
+
name: Optional name for the node
|
|
45
|
+
config: Optional configuration dictionary
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The created node instance
|
|
49
|
+
"""
|
|
50
|
+
if node_type not in self.node_types:
|
|
51
|
+
raise ValueError(f"Unknown node type: {node_type}")
|
|
52
|
+
|
|
53
|
+
with self._lock:
|
|
54
|
+
node_class = self.node_types[node_type]
|
|
55
|
+
node = node_class(node_id=node_id, name=name)
|
|
56
|
+
|
|
57
|
+
# Set workflow engine reference for error reporting
|
|
58
|
+
node.set_workflow_engine(self)
|
|
59
|
+
|
|
60
|
+
if config:
|
|
61
|
+
node.configure(config)
|
|
62
|
+
|
|
63
|
+
self.nodes[node.id] = node
|
|
64
|
+
return node
|
|
65
|
+
|
|
66
|
+
def get_node(self, node_id: str) -> Optional[BaseNode]:
|
|
67
|
+
"""
|
|
68
|
+
Get a node by its ID.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
node_id: The unique identifier of the node
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The node instance or None if not found
|
|
75
|
+
"""
|
|
76
|
+
return self.nodes.get(node_id)
|
|
77
|
+
|
|
78
|
+
def delete_node(self, node_id: str):
|
|
79
|
+
"""
|
|
80
|
+
Remove a node from the workflow.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
node_id: The unique identifier of the node to remove
|
|
84
|
+
"""
|
|
85
|
+
with self._lock:
|
|
86
|
+
if node_id in self.nodes:
|
|
87
|
+
node = self.nodes[node_id]
|
|
88
|
+
|
|
89
|
+
# Disconnect all outputs
|
|
90
|
+
node.outputs.clear()
|
|
91
|
+
|
|
92
|
+
# Disconnect all inputs (remove references from other nodes)
|
|
93
|
+
for other_node in self.nodes.values():
|
|
94
|
+
for output_idx in list(other_node.outputs.keys()):
|
|
95
|
+
other_node.disconnect(node, output_idx)
|
|
96
|
+
|
|
97
|
+
# Remove the node
|
|
98
|
+
del self.nodes[node_id]
|
|
99
|
+
|
|
100
|
+
def connect_nodes(self, source_id: str, target_id: str,
|
|
101
|
+
output_index: int = 0, input_index: int = 0):
|
|
102
|
+
"""
|
|
103
|
+
Connect two nodes together.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
source_id: ID of the source node
|
|
107
|
+
target_id: ID of the target node
|
|
108
|
+
output_index: Output port index on source node
|
|
109
|
+
input_index: Input port index on target node
|
|
110
|
+
"""
|
|
111
|
+
source = self.get_node(source_id)
|
|
112
|
+
target = self.get_node(target_id)
|
|
113
|
+
|
|
114
|
+
if not source or not target:
|
|
115
|
+
raise ValueError("Source or target node not found")
|
|
116
|
+
|
|
117
|
+
with self._lock:
|
|
118
|
+
source.connect(target, output_index, input_index)
|
|
119
|
+
|
|
120
|
+
def disconnect_nodes(self, source_id: str, target_id: str, output_index: int = 0):
|
|
121
|
+
"""
|
|
122
|
+
Disconnect two nodes.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
source_id: ID of the source node
|
|
126
|
+
target_id: ID of the target node
|
|
127
|
+
output_index: Output port index on source node
|
|
128
|
+
"""
|
|
129
|
+
source = self.get_node(source_id)
|
|
130
|
+
target = self.get_node(target_id)
|
|
131
|
+
|
|
132
|
+
if not source or not target:
|
|
133
|
+
raise ValueError("Source or target node not found")
|
|
134
|
+
|
|
135
|
+
with self._lock:
|
|
136
|
+
source.disconnect(target, output_index)
|
|
137
|
+
|
|
138
|
+
def start(self):
|
|
139
|
+
"""
|
|
140
|
+
Start the workflow - calls on_start() for all nodes.
|
|
141
|
+
"""
|
|
142
|
+
with self._lock:
|
|
143
|
+
if self.running:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
self.running = True
|
|
147
|
+
|
|
148
|
+
# Create system error node if it doesn't exist
|
|
149
|
+
self._ensure_system_error_node()
|
|
150
|
+
|
|
151
|
+
for node in self.nodes.values():
|
|
152
|
+
try:
|
|
153
|
+
node.on_start()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error starting node {node.id}: {e}")
|
|
156
|
+
|
|
157
|
+
def stop(self):
|
|
158
|
+
"""
|
|
159
|
+
Stop the workflow - calls on_stop() for all nodes.
|
|
160
|
+
"""
|
|
161
|
+
with self._lock:
|
|
162
|
+
if not self.running:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
self.running = False
|
|
166
|
+
|
|
167
|
+
for node in self.nodes.values():
|
|
168
|
+
try:
|
|
169
|
+
node.on_stop()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Error stopping node {node.id}: {e}")
|
|
172
|
+
|
|
173
|
+
def get_debug_messages(self, node_id: str) -> List[Dict[str, Any]]:
|
|
174
|
+
"""
|
|
175
|
+
Get debug messages from a debug node.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
node_id: ID of the debug node
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of debug messages
|
|
182
|
+
"""
|
|
183
|
+
node = self.get_node(node_id)
|
|
184
|
+
if node and hasattr(node, 'messages'):
|
|
185
|
+
return node.messages
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
def clear_debug_messages(self, node_id: str):
|
|
189
|
+
"""
|
|
190
|
+
Clear debug messages from a debug node.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
node_id: ID of the debug node
|
|
194
|
+
"""
|
|
195
|
+
node = self.get_node(node_id)
|
|
196
|
+
if node and hasattr(node, 'messages'):
|
|
197
|
+
node.messages.clear()
|
|
198
|
+
|
|
199
|
+
def broadcast_error(self, source_node_id: str, source_node_name: str, error_msg: str):
|
|
200
|
+
"""
|
|
201
|
+
Broadcast an error to all ErrorNodes in the workflow, including the system error node.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
source_node_id: ID of the node that generated the error
|
|
205
|
+
source_node_name: Name of the node that generated the error
|
|
206
|
+
error_msg: The error message
|
|
207
|
+
"""
|
|
208
|
+
with self._lock:
|
|
209
|
+
# Ensure system error node exists
|
|
210
|
+
self._ensure_system_error_node()
|
|
211
|
+
|
|
212
|
+
# Broadcast to all error nodes including system node
|
|
213
|
+
for node in self.nodes.values():
|
|
214
|
+
if node.type == 'ErrorNode' and hasattr(node, 'handle_error'):
|
|
215
|
+
try:
|
|
216
|
+
node.handle_error(source_node_id, source_node_name, error_msg)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error broadcasting to ErrorNode {node.id}: {e}")
|
|
219
|
+
|
|
220
|
+
def _ensure_system_error_node(self):
|
|
221
|
+
"""
|
|
222
|
+
Ensure the system error node exists. Creates it if not present.
|
|
223
|
+
This node is always available and doesn't appear on the canvas.
|
|
224
|
+
"""
|
|
225
|
+
if self._system_error_node is None or '__system_error__' not in self.nodes:
|
|
226
|
+
# Check if ErrorNode type is registered
|
|
227
|
+
if 'ErrorNode' in self.node_types:
|
|
228
|
+
try:
|
|
229
|
+
self._system_error_node = self.create_node(
|
|
230
|
+
'ErrorNode',
|
|
231
|
+
node_id='__system_error__',
|
|
232
|
+
name='System Errors',
|
|
233
|
+
config={}
|
|
234
|
+
)
|
|
235
|
+
# Mark as system node so it won't be exported/displayed
|
|
236
|
+
if hasattr(self._system_error_node, 'is_system_node'):
|
|
237
|
+
self._system_error_node.is_system_node = True
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(f"Failed to create system error node: {e}")
|
|
240
|
+
|
|
241
|
+
def get_system_errors(self) -> List[Dict[str, Any]]:
|
|
242
|
+
"""
|
|
243
|
+
Get errors from the system error node.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of error messages from system error node
|
|
247
|
+
"""
|
|
248
|
+
self._ensure_system_error_node()
|
|
249
|
+
if self._system_error_node and hasattr(self._system_error_node, 'get_errors'):
|
|
250
|
+
return self._system_error_node.get_errors()
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
def clear_system_errors(self):
|
|
254
|
+
"""
|
|
255
|
+
Clear errors from the system error node.
|
|
256
|
+
"""
|
|
257
|
+
self._ensure_system_error_node()
|
|
258
|
+
if self._system_error_node and hasattr(self._system_error_node, 'clear_errors'):
|
|
259
|
+
self._system_error_node.clear_errors()
|
|
260
|
+
|
|
261
|
+
def get_error_messages(self, node_id: str) -> List[Dict[str, Any]]:
|
|
262
|
+
"""
|
|
263
|
+
Get error messages from an error node.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
node_id: ID of the error node
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of error messages
|
|
270
|
+
"""
|
|
271
|
+
node = self.get_node(node_id)
|
|
272
|
+
if node and hasattr(node, 'get_errors'):
|
|
273
|
+
return node.get_errors()
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
def clear_error_messages(self, node_id: str):
|
|
277
|
+
"""
|
|
278
|
+
Clear error messages from an error node.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
node_id: ID of the error node
|
|
282
|
+
"""
|
|
283
|
+
node = self.get_node(node_id)
|
|
284
|
+
if node and hasattr(node, 'clear_errors'):
|
|
285
|
+
node.clear_errors()
|
|
286
|
+
|
|
287
|
+
def export_workflow(self) -> Dict[str, Any]:
|
|
288
|
+
"""
|
|
289
|
+
Export the entire workflow to a dictionary.
|
|
290
|
+
Excludes system nodes (like the system error node).
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary representation of the workflow
|
|
294
|
+
"""
|
|
295
|
+
nodes_data = []
|
|
296
|
+
connections_data = []
|
|
297
|
+
|
|
298
|
+
for node in self.nodes.values():
|
|
299
|
+
# Skip system nodes
|
|
300
|
+
if node.id == '__system_error__' or getattr(node, 'is_system_node', False):
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Handle unknown/placeholder nodes - export with original type
|
|
304
|
+
if getattr(node, '_is_unknown_node', False):
|
|
305
|
+
original_type = node.config.get('original_type', node.type)
|
|
306
|
+
original_config = node.config.get('original_config', {})
|
|
307
|
+
# Get the original name without the "(missing)" suffix
|
|
308
|
+
original_name = node.name
|
|
309
|
+
if original_name.endswith(' (missing)'):
|
|
310
|
+
original_name = original_name[:-10] # Remove ' (missing)'
|
|
311
|
+
|
|
312
|
+
nodes_data.append({
|
|
313
|
+
'id': node.id,
|
|
314
|
+
'type': original_type,
|
|
315
|
+
'name': original_name,
|
|
316
|
+
'config': original_config,
|
|
317
|
+
'enabled': False, # Unknown nodes are always disabled
|
|
318
|
+
'x': getattr(node, 'x', 0),
|
|
319
|
+
'y': getattr(node, 'y', 0),
|
|
320
|
+
'inputCount': node.input_count,
|
|
321
|
+
'outputCount': node.output_count
|
|
322
|
+
})
|
|
323
|
+
else:
|
|
324
|
+
nodes_data.append({
|
|
325
|
+
'id': node.id,
|
|
326
|
+
'type': node.type,
|
|
327
|
+
'name': node.name,
|
|
328
|
+
'config': node.config,
|
|
329
|
+
'enabled': node.enabled,
|
|
330
|
+
'x': getattr(node, 'x', 0),
|
|
331
|
+
'y': getattr(node, 'y', 0)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
for output_idx, targets in node.outputs.items():
|
|
335
|
+
for target_node, input_idx in targets:
|
|
336
|
+
# Skip connections to system nodes
|
|
337
|
+
if target_node.id == '__system_error__' or getattr(target_node, 'is_system_node', False):
|
|
338
|
+
continue
|
|
339
|
+
connections_data.append({
|
|
340
|
+
'source': node.id,
|
|
341
|
+
'target': target_node.id,
|
|
342
|
+
'sourceOutput': output_idx,
|
|
343
|
+
'targetInput': input_idx
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
'nodes': nodes_data,
|
|
348
|
+
'connections': connections_data
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def import_workflow(self, workflow_data: Dict[str, Any]):
|
|
352
|
+
"""
|
|
353
|
+
Import a workflow from a dictionary.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
workflow_data: Dictionary with 'nodes' and 'connections'
|
|
357
|
+
"""
|
|
358
|
+
with self._lock:
|
|
359
|
+
# Clear existing workflow (but preserve system error node)
|
|
360
|
+
system_error_node = self._system_error_node
|
|
361
|
+
self.nodes.clear()
|
|
362
|
+
self._system_error_node = None
|
|
363
|
+
|
|
364
|
+
# Create nodes
|
|
365
|
+
for node_data in workflow_data.get('nodes', []):
|
|
366
|
+
node_type = node_data['type']
|
|
367
|
+
|
|
368
|
+
if node_type in self.node_types:
|
|
369
|
+
# Known node type - create normally
|
|
370
|
+
node = self.create_node(
|
|
371
|
+
node_type=node_type,
|
|
372
|
+
node_id=node_data['id'],
|
|
373
|
+
name=node_data.get('name', ''),
|
|
374
|
+
config=node_data.get('config', {})
|
|
375
|
+
)
|
|
376
|
+
node.enabled = node_data.get('enabled', True)
|
|
377
|
+
else:
|
|
378
|
+
# Unknown node type - create placeholder
|
|
379
|
+
node = self._create_unknown_node(node_data)
|
|
380
|
+
logger.warning(f"Unknown node type '{node_type}' - created placeholder for node '{node_data['id']}'")
|
|
381
|
+
|
|
382
|
+
# Store position if provided
|
|
383
|
+
node.x = node_data.get('x', 0)
|
|
384
|
+
node.y = node_data.get('y', 0)
|
|
385
|
+
|
|
386
|
+
# Create connections
|
|
387
|
+
for conn_data in workflow_data.get('connections', []):
|
|
388
|
+
try:
|
|
389
|
+
self.connect_nodes(
|
|
390
|
+
source_id=conn_data['source'],
|
|
391
|
+
target_id=conn_data['target'],
|
|
392
|
+
output_index=conn_data.get('sourceOutput', 0),
|
|
393
|
+
input_index=conn_data.get('targetInput', 0)
|
|
394
|
+
)
|
|
395
|
+
except ValueError as e:
|
|
396
|
+
# Skip connections if source or target doesn't exist
|
|
397
|
+
logger.warning(f"Could not create connection: {e}")
|
|
398
|
+
|
|
399
|
+
# Recreate system error node if the workflow is running
|
|
400
|
+
if self.running:
|
|
401
|
+
self._ensure_system_error_node()
|
|
402
|
+
|
|
403
|
+
def _create_unknown_node(self, node_data: Dict[str, Any]) -> BaseNode:
|
|
404
|
+
"""
|
|
405
|
+
Create a placeholder node for an unknown node type.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
node_data: The original node data from the workflow
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
An UnknownNode instance that preserves the original data
|
|
412
|
+
"""
|
|
413
|
+
from pynode.nodes.UnknownNode import UnknownNode
|
|
414
|
+
|
|
415
|
+
original_type = node_data['type']
|
|
416
|
+
original_config = node_data.get('config', {})
|
|
417
|
+
|
|
418
|
+
# Try to preserve input/output counts from connections in the workflow
|
|
419
|
+
# Default to 1 each if we can't determine
|
|
420
|
+
input_count = node_data.get('inputCount', 1)
|
|
421
|
+
output_count = node_data.get('outputCount', 1)
|
|
422
|
+
|
|
423
|
+
# Preserve the user-given node name in the placeholder's display name;
|
|
424
|
+
# fall back to the original type when no name was given. Export strips
|
|
425
|
+
# the ' (missing)' suffix again, so the original name round-trips.
|
|
426
|
+
original_name = node_data.get('name') or original_type
|
|
427
|
+
|
|
428
|
+
# Create the unknown node with original type info
|
|
429
|
+
node = UnknownNode(
|
|
430
|
+
node_id=node_data['id'],
|
|
431
|
+
name=f"{original_name} (missing)",
|
|
432
|
+
original_type=original_type,
|
|
433
|
+
original_config=original_config,
|
|
434
|
+
input_count=input_count,
|
|
435
|
+
output_count=output_count
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Set workflow engine reference
|
|
439
|
+
node.set_workflow_engine(self)
|
|
440
|
+
|
|
441
|
+
# Store in nodes dict
|
|
442
|
+
self.nodes[node.id] = node
|
|
443
|
+
|
|
444
|
+
return node
|
|
445
|
+
|
|
446
|
+
def get_workflow_stats(self) -> Dict[str, Any]:
|
|
447
|
+
"""
|
|
448
|
+
Get statistics about the current workflow.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Dictionary with workflow statistics
|
|
452
|
+
"""
|
|
453
|
+
def _is_system(node):
|
|
454
|
+
return (node.id == '__system_error__'
|
|
455
|
+
or getattr(node, 'is_system_node', False))
|
|
456
|
+
|
|
457
|
+
node_types_count = {}
|
|
458
|
+
total_nodes = 0
|
|
459
|
+
total_connections = 0
|
|
460
|
+
|
|
461
|
+
# System nodes (e.g. the hidden __system_error__ ErrorNode) are an
|
|
462
|
+
# implementation detail: exclude them and any connections to them,
|
|
463
|
+
# mirroring export_workflow().
|
|
464
|
+
for node in self.nodes.values():
|
|
465
|
+
if _is_system(node):
|
|
466
|
+
continue
|
|
467
|
+
total_nodes += 1
|
|
468
|
+
node_type = node.type
|
|
469
|
+
node_types_count[node_type] = node_types_count.get(node_type, 0) + 1
|
|
470
|
+
|
|
471
|
+
for connections in node.outputs.values():
|
|
472
|
+
total_connections += sum(
|
|
473
|
+
1 for target, _ in connections if not _is_system(target))
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
'total_nodes': total_nodes,
|
|
477
|
+
'total_connections': total_connections,
|
|
478
|
+
'node_types': node_types_count,
|
|
479
|
+
'running': self.running
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
def __repr__(self):
|
|
483
|
+
return f"<WorkflowEngine(nodes={len(self.nodes)}, running={self.running})>"
|