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.
Files changed (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,491 @@
1
+ """
2
+ REST Endpoint Node - Exposes HTTP REST endpoints for external integration.
3
+
4
+ This node creates REST API endpoints that can be called externally to inject
5
+ messages into the workflow. Supports GET, POST, PUT, DELETE methods with
6
+ configurable paths and response handling.
7
+ """
8
+
9
+ import json
10
+ import threading
11
+ import logging
12
+ from typing import Optional, Dict, Any, List
13
+ from http.server import HTTPServer, BaseHTTPRequestHandler
14
+ from urllib.parse import urlparse, parse_qs
15
+ import time
16
+
17
+ try:
18
+ from ..base_node import BaseNode, MessageKeys, Info
19
+ except ImportError:
20
+ from base_node import BaseNode, MessageKeys, Info
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _info = Info()
25
+ _info.add_text("Exposes HTTP REST endpoints for external integration. Creates configurable API endpoints that can receive HTTP requests and inject them into the workflow.")
26
+ _info.add_header("Inputs")
27
+ _info.add_bullets(("Input 0:", "Response control messages (when responseMode is 'wait')"))
28
+ _info.add_header("Outputs")
29
+ _info.add_bullets(
30
+ ("Output 0:", "HTTP request data with:"),
31
+ )
32
+ _info.add_bullets(
33
+ ("req.method:", "HTTP method (GET, POST, etc.)"),
34
+ ("req.path:", "Request path"),
35
+ ("req.query:", "Query parameters"),
36
+ ("req.headers:", "Request headers"),
37
+ ("req.body:", "Request body (parsed JSON or raw)"),
38
+ ("payload:", "Request body or query parameters"),
39
+ )
40
+ _info.add_header("Configuration")
41
+ _info.add_bullets(
42
+ ("Port:", "HTTP server port (1024-65535)"),
43
+ ("Endpoint Path:", "URL path for the endpoint"),
44
+ ("Methods:", "Allowed HTTP methods"),
45
+ ("Response Mode:", "How to respond to requests (immediate, wait, custom)"),
46
+ ("Authentication:", "Optional auth (none, basic, bearer, API key)"),
47
+ )
48
+
49
+
50
+ class RESTEndpointNode(BaseNode):
51
+ """
52
+ REST Endpoint Node - Creates HTTP endpoints for external integration.
53
+
54
+ This node starts an HTTP server that listens for incoming requests on
55
+ configurable paths. When a request is received, it creates a message
56
+ containing the request details and sends it through the workflow.
57
+
58
+ Features:
59
+ - Configurable HTTP methods (GET, POST, PUT, DELETE, PATCH)
60
+ - Custom endpoint paths
61
+ - Request body parsing (JSON, form data)
62
+ - Query parameter extraction
63
+ - Header access
64
+ - Configurable response handling
65
+ """
66
+
67
+ # Visual properties
68
+ display_name = 'REST Endpoint'
69
+ info = str(_info)
70
+ icon = '🌐'
71
+ category = 'network'
72
+ color = '#87CEEB'
73
+ border_color = '#4682B4'
74
+ text_color = '#000000'
75
+ input_count = 1 # For dynamic response control
76
+ output_count = 1
77
+
78
+ DEFAULT_CONFIG = {
79
+ 'port': 9999,
80
+ 'path': '/api/webhook',
81
+ 'methods': 'POST',
82
+ 'responseMode': 'immediate',
83
+ 'responseCode': 200,
84
+ 'responseBody': '{"status": "received"}',
85
+ 'cors': True,
86
+ 'authType': 'none',
87
+ 'authCredentials': ''
88
+ }
89
+
90
+ properties = [
91
+ {
92
+ 'name': 'port',
93
+ 'label': 'Port',
94
+ 'type': 'number',
95
+ 'placeholder': DEFAULT_CONFIG['port'],
96
+ 'default': DEFAULT_CONFIG['port']
97
+ },
98
+ {
99
+ 'name': 'path',
100
+ 'label': 'Endpoint Path',
101
+ 'type': 'text',
102
+ 'placeholder': '/api/webhook',
103
+ 'default': DEFAULT_CONFIG['path']
104
+ },
105
+ {
106
+ 'name': 'methods',
107
+ 'label': 'Allowed Methods',
108
+ 'type': 'select',
109
+ 'options': [
110
+ {'value': 'GET', 'label': 'GET'},
111
+ {'value': 'POST', 'label': 'POST'},
112
+ {'value': 'PUT', 'label': 'PUT'},
113
+ {'value': 'DELETE', 'label': 'DELETE'},
114
+ {'value': 'PATCH', 'label': 'PATCH'},
115
+ {'value': 'ALL', 'label': 'All Methods'}
116
+ ],
117
+ 'default': DEFAULT_CONFIG['methods']
118
+ },
119
+ {
120
+ 'name': 'responseMode',
121
+ 'label': 'Response Mode',
122
+ 'type': 'select',
123
+ 'options': [
124
+ {'value': 'immediate', 'label': 'Immediate (200 OK)'},
125
+ {'value': 'wait', 'label': 'Wait for Response'},
126
+ {'value': 'custom', 'label': 'Custom Status'}
127
+ ],
128
+ 'default': DEFAULT_CONFIG['responseMode']
129
+ },
130
+ {
131
+ 'name': 'responseCode',
132
+ 'label': 'Response Code',
133
+ 'type': 'text',
134
+ 'placeholder': '200',
135
+ 'default': DEFAULT_CONFIG['responseCode']
136
+ },
137
+ {
138
+ 'name': 'responseBody',
139
+ 'label': 'Response Body',
140
+ 'type': 'text',
141
+ 'placeholder': DEFAULT_CONFIG['responseBody'],
142
+ 'default': DEFAULT_CONFIG['responseBody']
143
+ },
144
+ {
145
+ 'name': 'cors',
146
+ 'label': 'Enable CORS',
147
+ 'type': 'checkbox',
148
+ 'default': DEFAULT_CONFIG['cors']
149
+ },
150
+ {
151
+ 'name': 'authType',
152
+ 'label': 'Authentication',
153
+ 'type': 'select',
154
+ 'options': [
155
+ {'value': 'none', 'label': 'None'},
156
+ {'value': 'basic', 'label': 'Basic Auth'},
157
+ {'value': 'bearer', 'label': 'Bearer Token'},
158
+ {'value': 'apikey', 'label': 'API Key'}
159
+ ],
160
+ 'default': DEFAULT_CONFIG['authType']
161
+ },
162
+ {
163
+ 'name': 'authCredentials',
164
+ 'label': 'Auth Credentials',
165
+ 'type': 'text',
166
+ 'placeholder': 'username:password or token',
167
+ 'showIf': {'authType': ['basic', 'bearer', 'apikey']},
168
+ 'default': DEFAULT_CONFIG['authCredentials']
169
+ }
170
+ ]
171
+
172
+ def __init__(self, node_id: Optional[str] = None, name: str = "rest-endpoint"):
173
+ super().__init__(node_id, name)
174
+ self.configure(self.DEFAULT_CONFIG.copy())
175
+ self.server: Optional[HTTPServer] = None
176
+ self.server_thread: Optional[threading.Thread] = None
177
+ self._running = False
178
+ self._request_count = 0
179
+ self._pending_responses: Dict[str, Any] = {}
180
+ self._response_events: Dict[str, threading.Event] = {}
181
+
182
+ def on_deploy(self):
183
+ """Called when the workflow is deployed - start the server."""
184
+ self._start_server()
185
+
186
+ def on_start(self):
187
+ """Called when the workflow starts."""
188
+ if not self._running:
189
+ self._start_server()
190
+
191
+ def on_stop(self):
192
+ """Called when the workflow stops."""
193
+ self._stop_server()
194
+
195
+ def on_close(self):
196
+ """Called when the node is removed."""
197
+ self._stop_server()
198
+
199
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
200
+ """
201
+ Handle incoming messages for response control.
202
+
203
+ When responseMode is 'wait', this allows the workflow to send
204
+ a response back to the HTTP client.
205
+ """
206
+ request_id = msg.get('_request_id')
207
+ if request_id and request_id in self._response_events:
208
+ # Store the response data
209
+ self._pending_responses[request_id] = {
210
+ 'status': msg.get('statusCode', 200),
211
+ 'body': msg.get(MessageKeys.PAYLOAD, ''),
212
+ 'headers': msg.get('headers', {})
213
+ }
214
+ # Signal that response is ready
215
+ self._response_events[request_id].set()
216
+ else:
217
+ # Pass through if not a response message
218
+ self.send(msg)
219
+
220
+ def _create_handler(self):
221
+ """Create a request handler class with access to node instance."""
222
+ node = self
223
+
224
+ class RESTRequestHandler(BaseHTTPRequestHandler):
225
+ def log_message(self, format, *args):
226
+ logger.debug(f"REST Endpoint [{node.name}]: {format % args}")
227
+
228
+ def _send_cors_headers(self):
229
+ if node.config.get('cors', True):
230
+ self.send_header('Access-Control-Allow-Origin', '*')
231
+ self.send_header('Access-Control-Allow-Methods',
232
+ 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
233
+ self.send_header('Access-Control-Allow-Headers',
234
+ 'Content-Type, Authorization, X-API-Key')
235
+
236
+ def _check_auth(self) -> bool:
237
+ auth_type = node.config.get('authType', 'none')
238
+ credentials = node.config.get('authCredentials', '')
239
+
240
+ if auth_type == 'none':
241
+ return True
242
+
243
+ if auth_type == 'basic':
244
+ auth_header = self.headers.get('Authorization', '')
245
+ if auth_header.startswith('Basic '):
246
+ import base64
247
+ try:
248
+ decoded = base64.b64decode(auth_header[6:]).decode('utf-8')
249
+ return decoded == credentials
250
+ except Exception:
251
+ return False
252
+ return False
253
+
254
+ if auth_type == 'bearer':
255
+ auth_header = self.headers.get('Authorization', '')
256
+ if auth_header.startswith('Bearer '):
257
+ return auth_header[7:] == credentials
258
+ return False
259
+
260
+ if auth_type == 'apikey':
261
+ api_key = self.headers.get('X-API-Key', '')
262
+ return api_key == credentials
263
+
264
+ return False
265
+
266
+ def _check_method(self, method: str) -> bool:
267
+ allowed = node.config.get('methods', 'POST')
268
+ if allowed == 'ALL':
269
+ return True
270
+ return method == allowed
271
+
272
+ def _check_path(self) -> bool:
273
+ expected_path = node.config.get('path', '/api/webhook')
274
+ parsed = urlparse(self.path)
275
+ return parsed.path == expected_path
276
+
277
+ def _handle_request(self, method: str):
278
+ # Check path
279
+ if not self._check_path():
280
+ self.send_response(404)
281
+ self._send_cors_headers()
282
+ self.send_header('Content-Type', 'application/json')
283
+ self.end_headers()
284
+ self.wfile.write(b'{"error": "Not Found"}')
285
+ return
286
+
287
+ # Check method
288
+ if not self._check_method(method):
289
+ self.send_response(405)
290
+ self._send_cors_headers()
291
+ self.send_header('Content-Type', 'application/json')
292
+ self.end_headers()
293
+ self.wfile.write(b'{"error": "Method Not Allowed"}')
294
+ return
295
+
296
+ # Check auth
297
+ if not self._check_auth():
298
+ self.send_response(401)
299
+ self._send_cors_headers()
300
+ self.send_header('Content-Type', 'application/json')
301
+ self.end_headers()
302
+ self.wfile.write(b'{"error": "Unauthorized"}')
303
+ return
304
+
305
+ # Parse request
306
+ parsed = urlparse(self.path)
307
+ query_params = parse_qs(parsed.query)
308
+
309
+ # Read body for POST/PUT/PATCH
310
+ body = None
311
+ content_length = self.headers.get('Content-Length')
312
+ if content_length:
313
+ raw_body = self.rfile.read(int(content_length))
314
+ content_type = self.headers.get('Content-Type', '')
315
+
316
+ if 'application/json' in content_type:
317
+ try:
318
+ body = json.loads(raw_body.decode('utf-8'))
319
+ except json.JSONDecodeError:
320
+ body = raw_body.decode('utf-8')
321
+ elif 'application/x-www-form-urlencoded' in content_type:
322
+ body = parse_qs(raw_body.decode('utf-8'))
323
+ else:
324
+ try:
325
+ body = raw_body.decode('utf-8')
326
+ except UnicodeDecodeError:
327
+ body = raw_body
328
+
329
+ # Extract headers
330
+ headers = dict(self.headers)
331
+
332
+ # Create request ID for response tracking
333
+ import uuid
334
+ request_id = str(uuid.uuid4())
335
+
336
+ # Build message
337
+ msg = node.create_message(
338
+ payload=body if body is not None else query_params,
339
+ topic=f"http/{method.lower()}{parsed.path}"
340
+ )
341
+ msg['req'] = {
342
+ 'method': method,
343
+ 'path': parsed.path,
344
+ 'query': query_params,
345
+ 'headers': headers,
346
+ 'body': body,
347
+ 'url': self.path,
348
+ 'client_address': self.client_address[0]
349
+ }
350
+ msg['_request_id'] = request_id
351
+
352
+ node._request_count += 1
353
+
354
+ # Handle response based on mode
355
+ response_mode = node.config.get('responseMode', 'immediate')
356
+
357
+ if response_mode == 'wait':
358
+ # Set up event for waiting
359
+ event = threading.Event()
360
+ node._response_events[request_id] = event
361
+
362
+ # Send message through workflow
363
+ node.send(msg)
364
+
365
+ # Wait for response (with timeout)
366
+ if event.wait(timeout=30):
367
+ response_data = node._pending_responses.pop(request_id, {})
368
+ status = response_data.get('status', 200)
369
+ body_content = response_data.get('body', '')
370
+ extra_headers = response_data.get('headers', {})
371
+
372
+ self.send_response(status)
373
+ self._send_cors_headers()
374
+ for k, v in extra_headers.items():
375
+ self.send_header(k, v)
376
+ if 'Content-Type' not in extra_headers:
377
+ self.send_header('Content-Type', 'application/json')
378
+ self.end_headers()
379
+
380
+ if isinstance(body_content, (dict, list)):
381
+ self.wfile.write(json.dumps(body_content).encode('utf-8'))
382
+ else:
383
+ self.wfile.write(str(body_content).encode('utf-8'))
384
+ else:
385
+ # Timeout
386
+ self.send_response(504)
387
+ self._send_cors_headers()
388
+ self.send_header('Content-Type', 'application/json')
389
+ self.end_headers()
390
+ self.wfile.write(b'{"error": "Gateway Timeout"}')
391
+
392
+ # Cleanup
393
+ node._response_events.pop(request_id, None)
394
+ node._pending_responses.pop(request_id, None)
395
+ else:
396
+ # Immediate response
397
+ node.send(msg)
398
+
399
+ status = node.config.get('responseCode', 200)
400
+ body_str = node.config.get('responseBody', '{"status": "received"}')
401
+
402
+ self.send_response(status)
403
+ self._send_cors_headers()
404
+ self.send_header('Content-Type', 'application/json')
405
+ self.end_headers()
406
+ self.wfile.write(body_str.encode('utf-8'))
407
+
408
+ def do_OPTIONS(self):
409
+ self.send_response(200)
410
+ self._send_cors_headers()
411
+ self.end_headers()
412
+
413
+ def do_GET(self):
414
+ self._handle_request('GET')
415
+
416
+ def do_POST(self):
417
+ self._handle_request('POST')
418
+
419
+ def do_PUT(self):
420
+ self._handle_request('PUT')
421
+
422
+ def do_DELETE(self):
423
+ self._handle_request('DELETE')
424
+
425
+ def do_PATCH(self):
426
+ self._handle_request('PATCH')
427
+
428
+ return RESTRequestHandler
429
+
430
+ def _start_server(self):
431
+ """Start the HTTP server."""
432
+ if self._running:
433
+ return
434
+
435
+ port = int(self.config.get('port', 8080))
436
+
437
+ try:
438
+ handler = self._create_handler()
439
+ self.server = HTTPServer(('0.0.0.0', port), handler)
440
+ self._running = True
441
+
442
+ self.server_thread = threading.Thread(
443
+ target=self._run_server,
444
+ daemon=True,
445
+ name=f"RESTEndpoint-{self.id}"
446
+ )
447
+ self.server_thread.start()
448
+
449
+ path = self.config.get('path', '/api/webhook')
450
+ logger.info(f"REST Endpoint [{self.name}] started on port {port} at {path}")
451
+ self.set_status('running', f"Listening on :{port}{path}")
452
+
453
+ except OSError as e:
454
+ logger.error(f"REST Endpoint [{self.name}] failed to start: {e}")
455
+ self.set_status('error', f"Port {port} in use")
456
+ self._running = False
457
+
458
+ def _run_server(self):
459
+ """Server main loop."""
460
+ while self._running and self.server:
461
+ try:
462
+ self.server.handle_request()
463
+ except Exception as e:
464
+ if self._running:
465
+ logger.error(f"REST Endpoint [{self.name}] error: {e}")
466
+
467
+ def _stop_server(self):
468
+ """Stop the HTTP server."""
469
+ self._running = False
470
+
471
+ if self.server:
472
+ try:
473
+ self.server.shutdown()
474
+ except Exception:
475
+ pass
476
+ self.server = None
477
+
478
+ # Clear pending responses
479
+ for event in self._response_events.values():
480
+ event.set()
481
+ self._response_events.clear()
482
+ self._pending_responses.clear()
483
+
484
+ logger.info(f"REST Endpoint [{self.name}] stopped")
485
+ self.set_status('stopped', 'Server stopped')
486
+
487
+ def restart_server(self):
488
+ """Restart the HTTP server."""
489
+ self._stop_server()
490
+ time.sleep(0.5)
491
+ self._start_server()
@@ -0,0 +1,3 @@
1
+ from .range_node import RangeNode
2
+
3
+ __all__ = ['RangeNode']
@@ -0,0 +1,115 @@
1
+ """
2
+ Range Node - scales/maps values from one range to another.
3
+ """
4
+
5
+ from typing import Any, Dict
6
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
7
+
8
+ _info = Info()
9
+ _info.add_text("Maps numeric values from one range to another using linear interpolation. Useful for scaling sensor data, normalizing values, or converting between units.")
10
+ _info.add_header("Inputs")
11
+ _info.add_bullets(
12
+ ("Input 0:", "Message with numeric payload to scale")
13
+ )
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(
16
+ ("Output 0:", "Message with scaled payload value")
17
+ )
18
+ _info.add_header("Configuration")
19
+ _info.add_bullets(
20
+ ("Input Min/Max:", "The expected range of input values"),
21
+ ("Output Min/Max:", "The desired range of output values"),
22
+ ("Clamp:", "If enabled, output is constrained to the output range")
23
+ )
24
+ _info.add_header("Example")
25
+ _info.add_text("Input range 0-100 to output range 0-1: A value of 50 becomes 0.5")
26
+
27
+
28
+ class RangeNode(BaseNode):
29
+ """
30
+ Range Node - maps numeric values from one range to another.
31
+ Similar to Node-RED's range node.
32
+ """
33
+ info = str(_info)
34
+ display_name = 'Range'
35
+ icon = '📊'
36
+ category = 'function'
37
+ color = '#87CEEB'
38
+ border_color = '#4682B4'
39
+ text_color = '#000000'
40
+ input_count = 1
41
+ output_count = 1
42
+
43
+ properties = [
44
+ {
45
+ 'name': 'min_in',
46
+ 'label': 'Input Min',
47
+ 'type': 'number',
48
+ 'default': 0
49
+ },
50
+ {
51
+ 'name': 'max_in',
52
+ 'label': 'Input Max',
53
+ 'type': 'number',
54
+ 'default': 100
55
+ },
56
+ {
57
+ 'name': 'min_out',
58
+ 'label': 'Output Min',
59
+ 'type': 'number',
60
+ 'default': 0
61
+ },
62
+ {
63
+ 'name': 'max_out',
64
+ 'label': 'Output Max',
65
+ 'type': 'number',
66
+ 'default': 1
67
+ },
68
+ {
69
+ 'name': 'clamp',
70
+ 'label': 'Clamp to Output Range',
71
+ 'type': 'checkbox',
72
+ 'default': True
73
+ }
74
+ ]
75
+
76
+ def __init__(self, node_id=None, name="range"):
77
+ super().__init__(node_id, name)
78
+ self.configure({
79
+ 'min_in': 0,
80
+ 'max_in': 100,
81
+ 'min_out': 0,
82
+ 'max_out': 1,
83
+ 'clamp': True,
84
+ 'drop_messages': False
85
+ })
86
+
87
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
88
+ """Map payload value from input range to output range."""
89
+ try:
90
+ payload = msg.get(MessageKeys.PAYLOAD)
91
+ value = float(payload)
92
+
93
+ min_in = self.get_config_float('min_in', 0)
94
+ max_in = self.get_config_float('max_in', 100)
95
+ min_out = self.get_config_float('min_out', 0)
96
+ max_out = self.get_config_float('max_out', 1)
97
+ clamp = self.get_config_bool('clamp', True)
98
+
99
+ # Map value
100
+ if max_in == min_in:
101
+ mapped = min_out
102
+ else:
103
+ mapped = ((value - min_in) / (max_in - min_in)) * (max_out - min_out) + min_out
104
+
105
+ # Clamp if enabled
106
+ if clamp:
107
+ mapped = max(min(mapped, max(min_out, max_out)), min(min_out, max_out))
108
+
109
+ # Preserve original message properties (like frame_count)
110
+ # Note: send() handles deep copying, so we modify msg directly
111
+ msg[MessageKeys.PAYLOAD] = mapped
112
+ self.send(msg)
113
+
114
+ except (ValueError, TypeError) as e:
115
+ self.report_error(f"Invalid numeric value: {e}")
@@ -0,0 +1 @@
1
+ # No external dependencies - uses only Python standard library
@@ -0,0 +1,3 @@
1
+ from .rate_probe_node import RateProbeNode
2
+
3
+ __all__ = ['RateProbeNode']