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,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Switch node - routes messages based on rules.
|
|
3
|
+
Similar to Node-RED's switch node.
|
|
4
|
+
Supports dynamic rules with multiple outputs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
10
|
+
|
|
11
|
+
_info = Info()
|
|
12
|
+
_info.add_text("Routes messages to different outputs based on configurable rules. Each rule creates an output port - messages are sent to outputs where rules match.")
|
|
13
|
+
_info.add_header("Inputs")
|
|
14
|
+
_info.add_bullets(
|
|
15
|
+
("Input 0:", "Message to evaluate and route")
|
|
16
|
+
)
|
|
17
|
+
_info.add_header("Outputs")
|
|
18
|
+
_info.add_bullets(
|
|
19
|
+
("Output N:", "One output per rule - message sent to matching rule outputs")
|
|
20
|
+
)
|
|
21
|
+
_info.add_header("Configuration")
|
|
22
|
+
_info.add_bullets(
|
|
23
|
+
("Property:", "Message property to evaluate (e.g., payload, topic)"),
|
|
24
|
+
("Rules:", "List of conditions - each creates an output port"),
|
|
25
|
+
("Check all:", "If enabled, checks all rules; otherwise stops at first match")
|
|
26
|
+
)
|
|
27
|
+
_info.add_header("Operators")
|
|
28
|
+
_info.add_bullets(
|
|
29
|
+
("eq/neq:", "Equals / not equals"),
|
|
30
|
+
("lt/lte/gt/gte:", "Less than, greater than (or equal)"),
|
|
31
|
+
("between:", "Value is in range (format: 'min,max')"),
|
|
32
|
+
("contains/matches:", "String contains or regex matches"),
|
|
33
|
+
("true/false/null/nnull:", "Boolean and null checks"),
|
|
34
|
+
("empty/nempty:", "Check if value is empty"),
|
|
35
|
+
("else:", "Catch-all (always matches)")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SwitchNode(BaseNode):
|
|
40
|
+
"""
|
|
41
|
+
Switch node - routes messages based on rules.
|
|
42
|
+
Each rule creates a separate output port.
|
|
43
|
+
Messages are routed to the first matching rule's output.
|
|
44
|
+
"""
|
|
45
|
+
info = str(_info)
|
|
46
|
+
display_name = 'Switch'
|
|
47
|
+
icon = '⎇'
|
|
48
|
+
category = 'logic'
|
|
49
|
+
color = '#E9967A'
|
|
50
|
+
border_color = '#CA7C5F'
|
|
51
|
+
text_color = '#000000'
|
|
52
|
+
input_count = 1
|
|
53
|
+
output_count = 1 # Will be dynamically updated based on rules
|
|
54
|
+
|
|
55
|
+
DEFAULT_CONFIG = {
|
|
56
|
+
'property': MessageKeys.PAYLOAD,
|
|
57
|
+
'checkall': False,
|
|
58
|
+
'rules': [
|
|
59
|
+
{'operator': 'eq', 'value': '', 'valueType': 'str'}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
properties = [
|
|
64
|
+
{
|
|
65
|
+
'name': 'property',
|
|
66
|
+
'label': 'Property',
|
|
67
|
+
'type': 'text',
|
|
68
|
+
'help': 'The message property to evaluate (e.g., payload, topic)'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
'name': 'rules',
|
|
72
|
+
'label': 'Rules',
|
|
73
|
+
'type': 'rules', # Custom type for rules editor
|
|
74
|
+
'help': 'Define routing rules - each rule creates an output port'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
'name': 'checkall',
|
|
78
|
+
'label': 'Check all rules',
|
|
79
|
+
'type': 'checkbox',
|
|
80
|
+
'default': DEFAULT_CONFIG['checkall'],
|
|
81
|
+
'help': 'If checked, checks all rules and sends to all matching outputs. If unchecked, stops at first match.'
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
def __init__(self, node_id=None, name="switch"):
|
|
86
|
+
super().__init__(node_id, name)
|
|
87
|
+
self._update_output_count()
|
|
88
|
+
|
|
89
|
+
def configure(self, config: Dict[str, Any]):
|
|
90
|
+
"""Override configure to update output count when rules change."""
|
|
91
|
+
super().configure(config)
|
|
92
|
+
self._update_output_count()
|
|
93
|
+
|
|
94
|
+
def _update_output_count(self):
|
|
95
|
+
"""Update the output count based on the number of rules."""
|
|
96
|
+
rules = self.config.get('rules', [])
|
|
97
|
+
self.output_count = max(1, len(rules))
|
|
98
|
+
|
|
99
|
+
def _convert_value(self, value_str: str, value_type: str) -> Any:
|
|
100
|
+
"""
|
|
101
|
+
Convert string value to the appropriate type.
|
|
102
|
+
"""
|
|
103
|
+
if value_type == 'num':
|
|
104
|
+
try:
|
|
105
|
+
return float(value_str) if '.' in value_str else int(value_str)
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
return 0
|
|
108
|
+
elif value_type == 'bool':
|
|
109
|
+
return str(value_str).lower() in ('true', '1', 'yes', 'on')
|
|
110
|
+
elif value_type == 'json':
|
|
111
|
+
try:
|
|
112
|
+
import json
|
|
113
|
+
return json.loads(value_str)
|
|
114
|
+
except Exception:
|
|
115
|
+
return value_str
|
|
116
|
+
else: # 'str' or default
|
|
117
|
+
return str(value_str)
|
|
118
|
+
|
|
119
|
+
def _evaluate_rule(self, msg_value: Any, rule: Dict[str, Any]) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Evaluate a single rule against a message value.
|
|
122
|
+
|
|
123
|
+
Supported operators:
|
|
124
|
+
- eq: equals (==)
|
|
125
|
+
- neq: not equals (!=)
|
|
126
|
+
- lt: less than (<)
|
|
127
|
+
- lte: less than or equal (<=)
|
|
128
|
+
- gt: greater than (>)
|
|
129
|
+
- gte: greater than or equal (>=)
|
|
130
|
+
- between: value is between two numbers
|
|
131
|
+
- contains: string contains substring
|
|
132
|
+
- matches: regex match
|
|
133
|
+
- true: value is truthy
|
|
134
|
+
- false: value is falsy
|
|
135
|
+
- null: value is None
|
|
136
|
+
- nnull: value is not None
|
|
137
|
+
- empty: value is empty (empty string, list, dict, etc.)
|
|
138
|
+
- nempty: value is not empty
|
|
139
|
+
- haskey: dict has key
|
|
140
|
+
- else: always matches (catch-all)
|
|
141
|
+
"""
|
|
142
|
+
operator = rule.get('operator', 'eq')
|
|
143
|
+
rule_value = rule.get('value', '')
|
|
144
|
+
value_type = rule.get('valueType', 'str')
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Type-specific comparisons
|
|
148
|
+
if operator == 'eq':
|
|
149
|
+
compare_value = self._convert_value(rule_value, value_type)
|
|
150
|
+
return msg_value == compare_value
|
|
151
|
+
|
|
152
|
+
elif operator == 'neq':
|
|
153
|
+
compare_value = self._convert_value(rule_value, value_type)
|
|
154
|
+
return msg_value != compare_value
|
|
155
|
+
|
|
156
|
+
elif operator == 'lt':
|
|
157
|
+
compare_value = self._convert_value(rule_value, value_type)
|
|
158
|
+
return msg_value < compare_value
|
|
159
|
+
|
|
160
|
+
elif operator == 'lte':
|
|
161
|
+
compare_value = self._convert_value(rule_value, value_type)
|
|
162
|
+
return msg_value <= compare_value
|
|
163
|
+
|
|
164
|
+
elif operator == 'gt':
|
|
165
|
+
compare_value = self._convert_value(rule_value, value_type)
|
|
166
|
+
return msg_value > compare_value
|
|
167
|
+
|
|
168
|
+
elif operator == 'gte':
|
|
169
|
+
compare_value = self._convert_value(rule_value, value_type)
|
|
170
|
+
return msg_value >= compare_value
|
|
171
|
+
|
|
172
|
+
elif operator == 'between':
|
|
173
|
+
# Expects rule_value to be "min,max"
|
|
174
|
+
parts = str(rule_value).split(',')
|
|
175
|
+
if len(parts) == 2:
|
|
176
|
+
min_val = self._convert_value(parts[0].strip(), value_type)
|
|
177
|
+
max_val = self._convert_value(parts[1].strip(), value_type)
|
|
178
|
+
return min_val <= msg_value <= max_val
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
elif operator == 'contains':
|
|
182
|
+
return str(rule_value) in str(msg_value)
|
|
183
|
+
|
|
184
|
+
elif operator == 'matches':
|
|
185
|
+
# Regex match
|
|
186
|
+
pattern = str(rule_value)
|
|
187
|
+
return bool(re.search(pattern, str(msg_value)))
|
|
188
|
+
|
|
189
|
+
elif operator == 'true':
|
|
190
|
+
return bool(msg_value)
|
|
191
|
+
|
|
192
|
+
elif operator == 'false':
|
|
193
|
+
return not bool(msg_value)
|
|
194
|
+
|
|
195
|
+
elif operator == 'null':
|
|
196
|
+
return msg_value is None
|
|
197
|
+
|
|
198
|
+
elif operator == 'nnull':
|
|
199
|
+
return msg_value is not None
|
|
200
|
+
|
|
201
|
+
elif operator == 'empty':
|
|
202
|
+
if msg_value is None:
|
|
203
|
+
return True
|
|
204
|
+
if isinstance(msg_value, (str, list, dict, tuple)):
|
|
205
|
+
return len(msg_value) == 0
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
elif operator == 'nempty':
|
|
209
|
+
if msg_value is None:
|
|
210
|
+
return False
|
|
211
|
+
if isinstance(msg_value, (str, list, dict, tuple)):
|
|
212
|
+
return len(msg_value) > 0
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
elif operator == 'haskey':
|
|
216
|
+
if isinstance(msg_value, dict):
|
|
217
|
+
return str(rule_value) in msg_value
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
elif operator == 'else':
|
|
221
|
+
# Catch-all rule - always matches
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
else:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
except (TypeError, ValueError, AttributeError):
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
231
|
+
"""
|
|
232
|
+
Route message based on rules.
|
|
233
|
+
Each rule is checked in order, and messages are sent to matching output(s).
|
|
234
|
+
"""
|
|
235
|
+
property_path = self.config.get('property', MessageKeys.PAYLOAD)
|
|
236
|
+
check_all = self.config.get('checkall', False)
|
|
237
|
+
rules = self.config.get('rules', [])
|
|
238
|
+
|
|
239
|
+
if not rules:
|
|
240
|
+
# No rules defined, send to output 0
|
|
241
|
+
self.send(msg, 0)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
# Get the value to evaluate
|
|
245
|
+
msg_value = self._get_nested_value(msg, property_path)
|
|
246
|
+
|
|
247
|
+
# Check each rule
|
|
248
|
+
matched_any = False
|
|
249
|
+
for idx, rule in enumerate(rules):
|
|
250
|
+
if self._evaluate_rule(msg_value, rule):
|
|
251
|
+
self.send(msg, idx)
|
|
252
|
+
matched_any = True
|
|
253
|
+
|
|
254
|
+
# If not checking all rules, stop at first match
|
|
255
|
+
if not check_all:
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
# If no rules matched and we have an 'else' rule, it should have matched
|
|
259
|
+
# Otherwise, message is not sent anywhere (filtered out)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync Node - buffers messages based on an index and releases them on command.
|
|
3
|
+
Useful for synchronizing multiple message streams or batch processing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
8
|
+
|
|
9
|
+
_info = Info()
|
|
10
|
+
_info.add_text("Buffers messages by index and releases them on command. Useful for collecting messages out of order and releasing them in sequence, or for batch processing.")
|
|
11
|
+
_info.add_header("Inputs")
|
|
12
|
+
_info.add_bullets(
|
|
13
|
+
("Input 0:", "Messages to buffer (with index property) or release command")
|
|
14
|
+
)
|
|
15
|
+
_info.add_header("Outputs")
|
|
16
|
+
_info.add_bullets(
|
|
17
|
+
("Output 0:", "Buffered messages released in order, or as an array")
|
|
18
|
+
)
|
|
19
|
+
_info.add_header("Configuration")
|
|
20
|
+
_info.add_bullets(
|
|
21
|
+
("Index Property:", "Property name containing the message index (e.g., 'index', 'id')"),
|
|
22
|
+
("Release Payload:", "Payload value that triggers release (default: 'release')"),
|
|
23
|
+
("Output Mode:", "Sequential (one by one) or Array (all at once)"),
|
|
24
|
+
("Sort Order:", "Ascending, descending, or arrival order"),
|
|
25
|
+
("Clear After Release:", "Whether to clear the buffer after releasing"),
|
|
26
|
+
("Max Buffer Size:", "Maximum messages to buffer (oldest dropped when full)")
|
|
27
|
+
)
|
|
28
|
+
_info.add_header("Usage")
|
|
29
|
+
_info.add_text("Send messages with an index property to buffer them. Send a message with payload='release' to output all buffered messages in order.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SyncNode(BaseNode):
|
|
33
|
+
"""
|
|
34
|
+
Sync node - buffers messages by index and releases them on command.
|
|
35
|
+
Messages are stored until a "release" payload is received.
|
|
36
|
+
"""
|
|
37
|
+
info = str(_info)
|
|
38
|
+
display_name = 'Sync'
|
|
39
|
+
icon = '🔄'
|
|
40
|
+
category = 'logic'
|
|
41
|
+
color = '#E2D96E'
|
|
42
|
+
border_color = '#B8AF4A'
|
|
43
|
+
text_color = '#000000'
|
|
44
|
+
input_count = 1
|
|
45
|
+
output_count = 1
|
|
46
|
+
|
|
47
|
+
DEFAULT_CONFIG = {
|
|
48
|
+
'index_property': 'index',
|
|
49
|
+
'release_payload': 'release',
|
|
50
|
+
'output_mode': 'sequential',
|
|
51
|
+
'sort_order': 'ascending',
|
|
52
|
+
'clear_on_release': 'true',
|
|
53
|
+
'max_buffer_size': '1000',
|
|
54
|
+
'allow_duplicates': 'false'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
properties = [
|
|
58
|
+
{
|
|
59
|
+
'name': 'index_property',
|
|
60
|
+
'label': 'Index Property',
|
|
61
|
+
'type': 'text',
|
|
62
|
+
'default': DEFAULT_CONFIG['index_property'],
|
|
63
|
+
'help': 'Property name containing the message index (e.g., "index", "id", "sequence")'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
'name': 'release_payload',
|
|
67
|
+
'label': 'Release Payload',
|
|
68
|
+
'type': 'text',
|
|
69
|
+
'default': DEFAULT_CONFIG['release_payload'],
|
|
70
|
+
'help': 'Payload value that triggers release (default: "release")'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
'name': 'output_mode',
|
|
74
|
+
'label': 'Output Mode',
|
|
75
|
+
'type': 'select',
|
|
76
|
+
'options': [
|
|
77
|
+
{'value': 'sequential', 'label': 'Sequential (one by one)'},
|
|
78
|
+
{'value': 'array', 'label': 'Array (all at once)'}
|
|
79
|
+
],
|
|
80
|
+
'default': DEFAULT_CONFIG['output_mode'],
|
|
81
|
+
'help': 'How to output buffered messages'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
'name': 'sort_order',
|
|
85
|
+
'label': 'Sort Order',
|
|
86
|
+
'type': 'select',
|
|
87
|
+
'options': [
|
|
88
|
+
{'value': 'ascending', 'label': 'Ascending (0, 1, 2, ...)'},
|
|
89
|
+
{'value': 'descending', 'label': 'Descending (..., 2, 1, 0)'},
|
|
90
|
+
{'value': 'none', 'label': 'None (arrival order)'}
|
|
91
|
+
],
|
|
92
|
+
'default': DEFAULT_CONFIG['sort_order'],
|
|
93
|
+
'help': 'How to sort messages before release',
|
|
94
|
+
'showIf': {'output_mode': 'sequential'}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
'name': 'clear_on_release',
|
|
98
|
+
'label': 'Clear After Release',
|
|
99
|
+
'type': 'select',
|
|
100
|
+
'options': [
|
|
101
|
+
{'value': 'true', 'label': 'Yes (clear buffer)'},
|
|
102
|
+
{'value': 'false', 'label': 'No (keep buffer)'}
|
|
103
|
+
],
|
|
104
|
+
'default': DEFAULT_CONFIG['clear_on_release'],
|
|
105
|
+
'help': 'Clear buffer after releasing messages'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
'name': 'max_buffer_size',
|
|
109
|
+
'label': 'Max Buffer Size',
|
|
110
|
+
'type': 'text',
|
|
111
|
+
'default': DEFAULT_CONFIG['max_buffer_size'],
|
|
112
|
+
'help': 'Maximum number of messages to buffer (0 = unlimited)'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
'name': 'allow_duplicates',
|
|
116
|
+
'label': 'Allow Duplicate Indices',
|
|
117
|
+
'type': 'select',
|
|
118
|
+
'options': [
|
|
119
|
+
{'value': 'false', 'label': 'No (keep first)'},
|
|
120
|
+
{'value': 'true', 'label': 'Yes (overwrite with latest)'}
|
|
121
|
+
],
|
|
122
|
+
'default': DEFAULT_CONFIG['allow_duplicates'],
|
|
123
|
+
'help': 'Allow multiple messages for the same index (false = keep first, true = keep latest)'
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def __init__(self, node_id=None, name="sync"):
|
|
128
|
+
super().__init__(node_id, name)
|
|
129
|
+
# Buffer to store messages: {index: msg}
|
|
130
|
+
self._buffer: Dict[Any, Dict[str, Any]] = {}
|
|
131
|
+
# Track arrival order for non-sorted output
|
|
132
|
+
self._arrival_order: List[Any] = []
|
|
133
|
+
|
|
134
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
135
|
+
"""
|
|
136
|
+
Buffer message by index or release buffered messages.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
140
|
+
release_trigger = self.config.get('release_payload', 'release')
|
|
141
|
+
|
|
142
|
+
# Check if this is a release command
|
|
143
|
+
if payload == release_trigger:
|
|
144
|
+
self._release_messages(msg)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Otherwise, buffer the message
|
|
148
|
+
index_property = self.config.get('index_property', 'index')
|
|
149
|
+
|
|
150
|
+
# Get index from message
|
|
151
|
+
if index_property in msg:
|
|
152
|
+
msg_index = msg[index_property]
|
|
153
|
+
else:
|
|
154
|
+
# If no index property, use arrival order
|
|
155
|
+
msg_index = len(self._buffer)
|
|
156
|
+
|
|
157
|
+
# Check buffer size limit
|
|
158
|
+
max_size = self.get_config_int('max_buffer_size', 1000)
|
|
159
|
+
if max_size > 0 and len(self._buffer) >= max_size:
|
|
160
|
+
self.report_error(f"Buffer full ({max_size} messages), dropping oldest message")
|
|
161
|
+
# Remove oldest message
|
|
162
|
+
if self._arrival_order:
|
|
163
|
+
oldest_index = self._arrival_order.pop(0)
|
|
164
|
+
if oldest_index in self._buffer:
|
|
165
|
+
del self._buffer[oldest_index]
|
|
166
|
+
|
|
167
|
+
# Check if duplicate indices are allowed
|
|
168
|
+
allow_duplicates = self.get_config_bool('allow_duplicates', False)
|
|
169
|
+
|
|
170
|
+
if msg_index in self._buffer:
|
|
171
|
+
if allow_duplicates:
|
|
172
|
+
# Overwrite with latest message
|
|
173
|
+
self._buffer[msg_index] = msg
|
|
174
|
+
else:
|
|
175
|
+
# Keep first message, drop this one
|
|
176
|
+
# Silently ignore duplicate (already in buffer)
|
|
177
|
+
return
|
|
178
|
+
else:
|
|
179
|
+
# Store message in buffer (new index)
|
|
180
|
+
self._buffer[msg_index] = msg
|
|
181
|
+
# Track arrival order
|
|
182
|
+
self._arrival_order.append(msg_index)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.report_error(f"Error buffering message: {str(e)}")
|
|
186
|
+
|
|
187
|
+
def _release_messages(self, trigger_msg: Dict[str, Any]):
|
|
188
|
+
"""
|
|
189
|
+
Release buffered messages according to configuration.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
if not self._buffer:
|
|
193
|
+
# No messages to release
|
|
194
|
+
output_msg = self.create_message(
|
|
195
|
+
payload={
|
|
196
|
+
'status': 'empty',
|
|
197
|
+
'message': 'No messages in buffer'
|
|
198
|
+
},
|
|
199
|
+
topic=trigger_msg.get(MessageKeys.TOPIC, 'sync/release')
|
|
200
|
+
)
|
|
201
|
+
self.send(output_msg)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Get sorted indices
|
|
205
|
+
indices = self._get_sorted_indices()
|
|
206
|
+
|
|
207
|
+
output_mode = self.config.get('output_mode', 'sequential')
|
|
208
|
+
|
|
209
|
+
if output_mode == 'sequential':
|
|
210
|
+
# Send messages one by one
|
|
211
|
+
for idx in indices:
|
|
212
|
+
if idx in self._buffer:
|
|
213
|
+
self.send(self._buffer[idx])
|
|
214
|
+
|
|
215
|
+
# Send completion message
|
|
216
|
+
completion_msg = self.create_message(
|
|
217
|
+
payload={
|
|
218
|
+
'status': 'complete',
|
|
219
|
+
'released_count': len(indices),
|
|
220
|
+
'indices': indices
|
|
221
|
+
},
|
|
222
|
+
topic=trigger_msg.get(MessageKeys.TOPIC, 'sync/complete')
|
|
223
|
+
)
|
|
224
|
+
self.send(completion_msg)
|
|
225
|
+
|
|
226
|
+
elif output_mode == 'array':
|
|
227
|
+
# Send all messages as an array
|
|
228
|
+
messages = [self._buffer[idx] for idx in indices if idx in self._buffer]
|
|
229
|
+
|
|
230
|
+
array_msg = self.create_message(
|
|
231
|
+
payload={
|
|
232
|
+
'messages': messages,
|
|
233
|
+
'count': len(messages),
|
|
234
|
+
'indices': indices
|
|
235
|
+
},
|
|
236
|
+
topic=trigger_msg.get(MessageKeys.TOPIC, 'sync/array')
|
|
237
|
+
)
|
|
238
|
+
self.send(array_msg)
|
|
239
|
+
|
|
240
|
+
# Clear buffer if configured
|
|
241
|
+
if self.get_config_bool('clear_on_release', True):
|
|
242
|
+
self._buffer.clear()
|
|
243
|
+
self._arrival_order.clear()
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
self.report_error(f"Error releasing messages: {str(e)}")
|
|
247
|
+
|
|
248
|
+
def _get_sorted_indices(self) -> List[Any]:
|
|
249
|
+
"""
|
|
250
|
+
Get list of indices sorted according to configuration.
|
|
251
|
+
"""
|
|
252
|
+
sort_order = self.config.get('sort_order', 'ascending')
|
|
253
|
+
|
|
254
|
+
if sort_order == 'none':
|
|
255
|
+
# Return in arrival order
|
|
256
|
+
return self._arrival_order.copy()
|
|
257
|
+
|
|
258
|
+
# Get indices and try to sort them
|
|
259
|
+
indices = list(self._buffer.keys())
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
if sort_order == 'ascending':
|
|
263
|
+
return sorted(indices)
|
|
264
|
+
elif sort_order == 'descending':
|
|
265
|
+
return sorted(indices, reverse=True)
|
|
266
|
+
except TypeError:
|
|
267
|
+
# If indices can't be compared (mixed types), use arrival order
|
|
268
|
+
return self._arrival_order.copy()
|
|
269
|
+
|
|
270
|
+
return indices
|
|
271
|
+
|
|
272
|
+
def get_buffer_status(self) -> Dict[str, Any]:
|
|
273
|
+
"""
|
|
274
|
+
Get current buffer status.
|
|
275
|
+
"""
|
|
276
|
+
return {
|
|
277
|
+
'buffer_size': len(self._buffer),
|
|
278
|
+
'indices': list(self._buffer.keys()),
|
|
279
|
+
'arrival_order': self._arrival_order.copy()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def clear_buffer(self):
|
|
283
|
+
"""
|
|
284
|
+
Manually clear the buffer.
|
|
285
|
+
"""
|
|
286
|
+
self._buffer.clear()
|
|
287
|
+
self._arrival_order.clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# No external dependencies - uses only Python standard library
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template for creating custom nodes.
|
|
3
|
+
This demonstrates how third-party developers can create their own nodes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
8
|
+
|
|
9
|
+
# Build info content
|
|
10
|
+
_info = Info()
|
|
11
|
+
_info.add_text("Template node for creating custom third-party nodes.")
|
|
12
|
+
_info.add_text("Copy this file and modify to create your own nodes.")
|
|
13
|
+
_info.add_header("Inputs")
|
|
14
|
+
_info.add_bullet("Input 0:", "Any message to process through the template")
|
|
15
|
+
_info.add_header("Outputs")
|
|
16
|
+
_info.add_bullet("Output 0:", "Message with payload replaced by template output")
|
|
17
|
+
_info.add_header("Properties")
|
|
18
|
+
_info.add_bullet("Template:", "Text template with {{payload}} and {{topic}} placeholders")
|
|
19
|
+
_info.add_bullet("Output Format:", "Format type: plain, json, or html")
|
|
20
|
+
_info.add_header("Template Variables")
|
|
21
|
+
_info.add_bullet("{{payload}}:", "Replaced with the message payload")
|
|
22
|
+
_info.add_bullet("{{topic}}:", "Replaced with the message topic")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TemplateNode(BaseNode):
|
|
26
|
+
"""
|
|
27
|
+
Template node - example of a custom third-party node.
|
|
28
|
+
Copy this file and modify to create your own nodes.
|
|
29
|
+
"""
|
|
30
|
+
# Visual properties
|
|
31
|
+
display_name = 'Template'
|
|
32
|
+
icon = '📄'
|
|
33
|
+
category = 'system' # Categories: input, output, function, logic, custom
|
|
34
|
+
color = '#FFA07A'
|
|
35
|
+
border_color = '#FF7F50'
|
|
36
|
+
text_color = '#000000'
|
|
37
|
+
input_count = 0
|
|
38
|
+
output_count = 0
|
|
39
|
+
info = str(_info)
|
|
40
|
+
hidden = True # Hide this template node from the palette
|
|
41
|
+
|
|
42
|
+
# Property schema for the properties panel
|
|
43
|
+
# This defines what fields appear in the UI when the node is selected
|
|
44
|
+
properties = [
|
|
45
|
+
{
|
|
46
|
+
'name': 'template',
|
|
47
|
+
'label': 'Template',
|
|
48
|
+
'type': 'textarea' # Types: text, textarea, select, button
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
'name': 'format',
|
|
52
|
+
'label': 'Output Format',
|
|
53
|
+
'type': 'select',
|
|
54
|
+
'options': [
|
|
55
|
+
{'value': 'plain', 'label': 'Plain Text'},
|
|
56
|
+
{'value': 'json', 'label': 'JSON'},
|
|
57
|
+
{'value': 'html', 'label': 'HTML'}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def __init__(self, node_id=None, name="template"):
|
|
63
|
+
super().__init__(node_id, name)
|
|
64
|
+
self.configure({
|
|
65
|
+
'template': 'Hello {{payload}}',
|
|
66
|
+
'format': 'plain'
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
70
|
+
"""
|
|
71
|
+
Process incoming messages.
|
|
72
|
+
This method is called when a message arrives at the node.
|
|
73
|
+
"""
|
|
74
|
+
template = self.config.get('template', '')
|
|
75
|
+
format_type = self.config.get('format', 'plain')
|
|
76
|
+
|
|
77
|
+
# Simple template replacement (in production, use a proper template engine)
|
|
78
|
+
output = template.replace('{{payload}}', str(msg.get(MessageKeys.PAYLOAD, '')))
|
|
79
|
+
output = output.replace('{{topic}}', str(msg.get(MessageKeys.TOPIC, '')))
|
|
80
|
+
|
|
81
|
+
# Preserve original message properties (like frame_count) and update payload
|
|
82
|
+
# Note: send() handles deep copying, so we modify msg directly
|
|
83
|
+
msg[MessageKeys.PAYLOAD] = output
|
|
84
|
+
|
|
85
|
+
# Send to connected nodes
|
|
86
|
+
self.send(msg)
|