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,591 @@
1
+ """
2
+ Webhook Node - Makes HTTP requests to external webhooks and APIs.
3
+
4
+ This node sends HTTP requests to configured endpoints when it receives
5
+ a message. Supports various HTTP methods, authentication options,
6
+ and flexible payload configuration.
7
+ """
8
+
9
+ import json
10
+ import threading
11
+ import logging
12
+ from typing import Optional, Dict, Any, List
13
+ from urllib.parse import urlencode
14
+ import time
15
+
16
+ try:
17
+ import requests
18
+ REQUESTS_AVAILABLE = True
19
+ except ImportError:
20
+ REQUESTS_AVAILABLE = False
21
+ # Fallback to urllib
22
+ import urllib.request
23
+ import urllib.error
24
+ import ssl
25
+
26
+ try:
27
+ from ..base_node import BaseNode, MessageKeys, Info
28
+ except ImportError:
29
+ from base_node import BaseNode, MessageKeys, Info
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ _info = Info()
34
+ _info.add_text("Makes HTTP requests to external webhooks and APIs. Sends configured HTTP requests when it receives a message.")
35
+ _info.add_header("Inputs")
36
+ _info.add_bullets(("Input 0:", "Message to trigger HTTP request"))
37
+ _info.add_header("Outputs")
38
+ _info.add_bullets(
39
+ ("Output 0:", "HTTP response with:"),
40
+ )
41
+ _info.add_bullets(
42
+ ("payload.statusCode:", "HTTP status code (200, 404, 500, etc.)"),
43
+ ("payload.body:", "Response body (parsed JSON or text)"),
44
+ ("payload.headers:", "Response headers"),
45
+ ("payload.url:", "Final URL after redirects"),
46
+ ("payload.elapsed:", "Response time in seconds"), ('payload.buffer:', 'Raw bytes (when Response Type is binary)'),)
47
+ _info.add_header("Configuration")
48
+ _info.add_bullets(
49
+ ("URL:", "Target endpoint URL"),
50
+ ("Method:", "HTTP method (GET, POST, PUT, DELETE, PATCH)"),
51
+ ("Content Type:", "Request content type (JSON, form, text)"),
52
+ ("Authentication:", "Optional auth (none, basic, bearer, API key)"),
53
+ ("Timeout:", "Request timeout in seconds"),
54
+ ("Retries:", "Number of retry attempts on failure"),
55
+ )
56
+
57
+
58
+ class WebhookNode(BaseNode):
59
+ """
60
+ Webhook Node - Sends HTTP requests to external services.
61
+
62
+ This node makes HTTP requests to external webhooks, REST APIs, or
63
+ other HTTP endpoints. It supports various HTTP methods, authentication
64
+ options, custom headers, and flexible payload configuration.
65
+
66
+ Features:
67
+ - Multiple HTTP methods (GET, POST, PUT, DELETE, PATCH)
68
+ - Various authentication types (Basic, Bearer, API Key)
69
+ - Custom headers
70
+ - JSON and form-encoded payloads
71
+ - Timeout configuration
72
+ - Retry logic with exponential backoff
73
+ - Response parsing and forwarding
74
+ """
75
+
76
+ # Visual properties
77
+ display_name = 'Webhook'
78
+ info = str(_info)
79
+ icon = '📤'
80
+ category = 'network'
81
+ color = '#90EE90'
82
+ border_color = '#228B22'
83
+ text_color = '#000000'
84
+ input_count = 1
85
+ output_count = 1
86
+
87
+ DEFAULT_CONFIG = {
88
+ 'url': '',
89
+ 'method': 'POST',
90
+ 'contentType': 'application/json',
91
+ 'payloadSource': 'msg.payload',
92
+ 'customPayload': '',
93
+ 'headers': '{}',
94
+ 'authType': 'none',
95
+ 'authCredentials': '',
96
+ 'apiKeyHeader': 'X-API-Key',
97
+ 'timeout': 30,
98
+ 'retries': 0,
99
+ 'retryDelay': 1000,
100
+ 'followRedirects': True,
101
+ 'validateSSL': False,
102
+ 'responseType': 'auto',
103
+ 'async': True
104
+ }
105
+
106
+ properties = [
107
+ {
108
+ 'name': 'url',
109
+ 'label': 'URL',
110
+ 'type': 'text',
111
+ 'placeholder': 'https://api.example.com/webhook'
112
+ },
113
+ {
114
+ 'name': 'method',
115
+ 'label': 'Method',
116
+ 'type': 'select',
117
+ 'options': [
118
+ {'value': 'GET', 'label': 'GET'},
119
+ {'value': 'POST', 'label': 'POST'},
120
+ {'value': 'PUT', 'label': 'PUT'},
121
+ {'value': 'DELETE', 'label': 'DELETE'},
122
+ {'value': 'PATCH', 'label': 'PATCH'}
123
+ ],
124
+ 'default': DEFAULT_CONFIG['method']
125
+
126
+ },
127
+ {
128
+ 'name': 'contentType',
129
+ 'label': 'Content Type',
130
+ 'type': 'select',
131
+ 'options': [
132
+ {'value': 'application/json', 'label': 'JSON'},
133
+ {'value': 'application/x-www-form-urlencoded', 'label': 'Form URL Encoded'},
134
+ {'value': 'text/plain', 'label': 'Plain Text'},
135
+ {'value': 'custom', 'label': 'Custom (set in headers)'}
136
+ ],
137
+ 'default': DEFAULT_CONFIG['contentType']
138
+ },
139
+ {
140
+ 'name': 'payloadSource',
141
+ 'label': 'Payload Source',
142
+ 'type': 'select',
143
+ 'options': [
144
+ {'value': 'msg.payload', 'label': 'msg.payload'},
145
+ {'value': 'msg', 'label': 'Entire Message'},
146
+ {'value': 'custom', 'label': 'Custom Template'}
147
+ ],
148
+ 'default': DEFAULT_CONFIG['payloadSource']
149
+ },
150
+ {
151
+ 'name': 'customPayload',
152
+ 'label': 'Custom Payload',
153
+ 'type': 'textarea',
154
+ 'placeholder': '{"key": "{{msg.payload}}"}',
155
+ 'showIf': {'payloadSource': 'custom'},
156
+ 'default': DEFAULT_CONFIG['customPayload']
157
+ },
158
+ {
159
+ 'name': 'headers',
160
+ 'label': 'Headers (JSON)',
161
+ 'type': 'textarea',
162
+ 'placeholder': '{"X-Custom-Header": "value"}',
163
+ 'default': DEFAULT_CONFIG['headers']
164
+ },
165
+ {
166
+ 'name': 'authType',
167
+ 'label': 'Authentication',
168
+ 'type': 'select',
169
+ 'options': [
170
+ {'value': 'none', 'label': 'None'},
171
+ {'value': 'basic', 'label': 'Basic Auth'},
172
+ {'value': 'bearer', 'label': 'Bearer Token'},
173
+ {'value': 'apikey', 'label': 'API Key Header'}
174
+ ],
175
+ 'default': DEFAULT_CONFIG['authType']
176
+ },
177
+ {
178
+ 'name': 'authCredentials',
179
+ 'label': 'Auth Credentials',
180
+ 'type': 'text',
181
+ 'placeholder': 'username:password or token',
182
+ 'showIf': {'authType': ['basic', 'bearer', 'apikey']},
183
+ 'default': DEFAULT_CONFIG['authCredentials']
184
+ },
185
+ {
186
+ 'name': 'apiKeyHeader',
187
+ 'label': 'API Key Header Name',
188
+ 'type': 'text',
189
+ 'placeholder': 'X-API-Key',
190
+ 'showIf': {'authType': 'apikey'},
191
+ 'default': DEFAULT_CONFIG['apiKeyHeader']
192
+ },
193
+ {
194
+ 'name': 'timeout',
195
+ 'label': 'Timeout (seconds)',
196
+ 'type': 'text',
197
+ 'placeholder': '30',
198
+ 'default': DEFAULT_CONFIG['timeout']
199
+ },
200
+ {
201
+ 'name': 'retries',
202
+ 'label': 'Retries',
203
+ 'type': 'text',
204
+ 'placeholder': '0',
205
+ 'default': DEFAULT_CONFIG['retries']
206
+ },
207
+ {
208
+ 'name': 'retryDelay',
209
+ 'label': 'Retry Delay (ms)',
210
+ 'type': 'text',
211
+ 'placeholder': '1000',
212
+ 'default': DEFAULT_CONFIG['retryDelay']
213
+ },
214
+ {
215
+ 'name': 'responseType',
216
+ 'label': 'Response Type',
217
+ 'type': 'select',
218
+ 'options': [
219
+ {'value': 'auto', 'label': 'Auto (JSON or text)'},
220
+ {'value': 'text', 'label': 'Text'},
221
+ {'value': 'json', 'label': 'JSON'},
222
+ {'value': 'binary', 'label': 'Binary Buffer'}
223
+ ],
224
+ 'default': DEFAULT_CONFIG['responseType']
225
+ },
226
+ {
227
+ 'name': 'followRedirects',
228
+ 'label': 'Follow Redirects',
229
+ 'type': 'checkbox',
230
+ 'default': DEFAULT_CONFIG['followRedirects']
231
+ },
232
+ {
233
+ 'name': 'validateSSL',
234
+ 'label': 'Validate SSL',
235
+ 'type': 'checkbox',
236
+ 'default': DEFAULT_CONFIG['validateSSL']
237
+ },
238
+ {
239
+ 'name': 'async',
240
+ 'label': 'Async (non-blocking)',
241
+ 'type': 'checkbox',
242
+ 'default': DEFAULT_CONFIG['async']
243
+ }
244
+ ]
245
+
246
+ def __init__(self, node_id: Optional[str] = None, name: str = "webhook"):
247
+ super().__init__(node_id, name)
248
+ self.configure(self.DEFAULT_CONFIG.copy())
249
+ self._request_count = 0
250
+ self._success_count = 0
251
+ self._error_count = 0
252
+
253
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
254
+ """Handle incoming messages by making HTTP requests."""
255
+ if self.config.get('async', True):
256
+ # Non-blocking request in separate thread
257
+ thread = threading.Thread(
258
+ target=self._make_request,
259
+ args=(msg,),
260
+ daemon=True
261
+ )
262
+ thread.start()
263
+ else:
264
+ # Blocking request
265
+ self._make_request(msg)
266
+
267
+ def _make_request(self, msg: Dict[str, Any]):
268
+ """Make the HTTP request."""
269
+ self._request_count += 1
270
+
271
+ # Get configuration
272
+ url = self._resolve_template(self.config.get('url', ''), msg)
273
+ method = self.config.get('method', 'POST')
274
+ content_type = self.config.get('contentType', 'application/json')
275
+ timeout = int(self.config.get('timeout', 30))
276
+ retries = int(self.config.get('retries', 0))
277
+ retry_delay = int(self.config.get('retryDelay', 1000)) / 1000.0
278
+
279
+ # Build headers
280
+ headers = self._build_headers(msg, content_type)
281
+
282
+ # Build payload
283
+ payload = self._build_payload(msg)
284
+
285
+ # Make request with retries
286
+ last_error = None
287
+ for attempt in range(retries + 1):
288
+ try:
289
+ response = self._execute_request(
290
+ url, method, headers, payload, timeout
291
+ )
292
+
293
+ # Success
294
+ self._success_count += 1
295
+ self._handle_success(msg, response)
296
+ return
297
+
298
+ except Exception as e:
299
+ last_error = e
300
+ if attempt < retries:
301
+ logger.warning(
302
+ f"Webhook [{self.name}] attempt {attempt + 1} failed: {e}. "
303
+ f"Retrying in {retry_delay}s..."
304
+ )
305
+ time.sleep(retry_delay)
306
+ retry_delay *= 2 # Exponential backoff
307
+
308
+ # All retries failed
309
+ self._error_count += 1
310
+ self._handle_error(msg, last_error)
311
+
312
+ def _build_headers(self, msg: Dict[str, Any], content_type: str) -> Dict[str, str]:
313
+ """Build request headers."""
314
+ headers = {}
315
+
316
+ # Parse custom headers
317
+ try:
318
+ custom_headers = self.config.get('headers', '{}')
319
+ if custom_headers:
320
+ headers.update(json.loads(custom_headers))
321
+ except json.JSONDecodeError:
322
+ logger.warning(f"Webhook [{self.name}] invalid headers JSON")
323
+
324
+ # Set content type
325
+ if content_type != 'custom':
326
+ headers['Content-Type'] = content_type
327
+
328
+ # Add authentication
329
+ auth_type = self.config.get('authType', 'none')
330
+ credentials = self.config.get('authCredentials', '')
331
+
332
+ if auth_type == 'basic' and credentials:
333
+ import base64
334
+ encoded = base64.b64encode(credentials.encode()).decode()
335
+ headers['Authorization'] = f'Basic {encoded}'
336
+
337
+ elif auth_type == 'bearer' and credentials:
338
+ headers['Authorization'] = f'Bearer {credentials}'
339
+
340
+ elif auth_type == 'apikey' and credentials:
341
+ header_name = self.config.get('apiKeyHeader', 'X-API-Key')
342
+ headers[header_name] = credentials
343
+
344
+ return headers
345
+
346
+ def _build_payload(self, msg: Dict[str, Any]) -> Any:
347
+ """Build request payload."""
348
+ source = self.config.get('payloadSource', 'msg.payload')
349
+
350
+ if source == 'msg.payload':
351
+ return msg.get(MessageKeys.PAYLOAD)
352
+
353
+ elif source == 'msg':
354
+ # Clone message without internal fields
355
+ payload = {k: v for k, v in msg.items() if not k.startswith('_')}
356
+ return payload
357
+
358
+ elif source == 'custom':
359
+ template = self.config.get('customPayload', '')
360
+ if template:
361
+ resolved = self._resolve_template(template, msg)
362
+ try:
363
+ return json.loads(resolved)
364
+ except json.JSONDecodeError:
365
+ return resolved
366
+ return None
367
+
368
+ return msg.get(MessageKeys.PAYLOAD)
369
+
370
+ def _resolve_template(self, template: str, msg: Dict[str, Any]) -> str:
371
+ """Resolve template variables like {{msg.payload}}."""
372
+ import re
373
+
374
+ def replace_var(match):
375
+ var_path = match.group(1)
376
+ parts = var_path.split('.')
377
+
378
+ value = msg
379
+ for part in parts:
380
+ if part == 'msg':
381
+ continue
382
+ if isinstance(value, dict) and part in value:
383
+ value = value[part]
384
+ else:
385
+ return match.group(0) # Return original if not found
386
+
387
+ if isinstance(value, (dict, list)):
388
+ return json.dumps(value)
389
+ return str(value)
390
+
391
+ return re.sub(r'\{\{([\w.]+)\}\}', replace_var, template)
392
+
393
+ def _execute_request(
394
+ self, url: str, method: str, headers: Dict[str, str],
395
+ payload: Any, timeout: int
396
+ ) -> Dict[str, Any]:
397
+ """Execute the HTTP request."""
398
+ validate_ssl = self.config.get('validateSSL', True)
399
+ follow_redirects = self.config.get('followRedirects', True)
400
+
401
+ if REQUESTS_AVAILABLE:
402
+ return self._request_with_requests(
403
+ url, method, headers, payload, timeout,
404
+ validate_ssl, follow_redirects
405
+ )
406
+ else:
407
+ return self._request_with_urllib(
408
+ url, method, headers, payload, timeout,
409
+ validate_ssl
410
+ )
411
+
412
+ def _request_with_requests(
413
+ self, url: str, method: str, headers: Dict[str, str],
414
+ payload: Any, timeout: int, validate_ssl: bool,
415
+ follow_redirects: bool
416
+ ) -> Dict[str, Any]:
417
+ """Make request using requests library."""
418
+ content_type = headers.get('Content-Type', 'application/json')
419
+
420
+ kwargs = {
421
+ 'headers': headers,
422
+ 'timeout': timeout,
423
+ 'verify': validate_ssl,
424
+ 'allow_redirects': follow_redirects
425
+ }
426
+
427
+ if method in ['POST', 'PUT', 'PATCH'] and payload is not None:
428
+ if 'application/json' in content_type:
429
+ kwargs['json'] = payload
430
+ elif 'application/x-www-form-urlencoded' in content_type:
431
+ kwargs['data'] = payload if isinstance(payload, dict) else str(payload)
432
+ else:
433
+ kwargs['data'] = payload if isinstance(payload, str) else json.dumps(payload)
434
+
435
+ elif method == 'GET' and isinstance(payload, dict):
436
+ kwargs['params'] = payload
437
+
438
+ response = requests.request(method, url, **kwargs)
439
+
440
+ response_type = self.config.get('responseType', 'auto')
441
+ if response_type == 'binary':
442
+ body = response.content
443
+ else:
444
+ body = self._parse_response_body(
445
+ response.text, response.headers.get('Content-Type', ''), response_type
446
+ )
447
+
448
+ result = {
449
+ 'status_code': response.status_code,
450
+ 'headers': dict(response.headers),
451
+ 'body': body,
452
+ 'url': response.url,
453
+ 'elapsed': response.elapsed.total_seconds()
454
+ }
455
+ if response_type == 'binary':
456
+ result['buffer'] = body
457
+ return result
458
+
459
+ def _request_with_urllib(
460
+ self, url: str, method: str, headers: Dict[str, str],
461
+ payload: Any, timeout: int, validate_ssl: bool
462
+ ) -> Dict[str, Any]:
463
+ """Make request using urllib (fallback)."""
464
+ content_type = headers.get('Content-Type', 'application/json')
465
+
466
+ # Prepare data
467
+ data = None
468
+ if method in ['POST', 'PUT', 'PATCH'] and payload is not None:
469
+ if 'application/json' in content_type:
470
+ data = json.dumps(payload).encode('utf-8')
471
+ elif isinstance(payload, dict):
472
+ data = urlencode(payload).encode('utf-8')
473
+ else:
474
+ data = str(payload).encode('utf-8')
475
+
476
+ # Create request
477
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
478
+
479
+ # SSL context
480
+ context = None
481
+ if not validate_ssl:
482
+ context = ssl.create_default_context()
483
+ context.check_hostname = False
484
+ context.verify_mode = ssl.CERT_NONE
485
+
486
+ response_type = self.config.get('responseType', 'auto')
487
+ try:
488
+ with urllib.request.urlopen(req, timeout=timeout, context=context) as response:
489
+ raw = response.read()
490
+ if response_type == 'binary':
491
+ body = raw
492
+ else:
493
+ body = self._parse_response_body(
494
+ raw.decode('utf-8'), response.headers.get('Content-Type', ''), response_type
495
+ )
496
+ result = {
497
+ 'status_code': response.status,
498
+ 'headers': dict(response.headers),
499
+ 'body': body,
500
+ 'url': response.url,
501
+ 'elapsed': 0
502
+ }
503
+ if response_type == 'binary':
504
+ result['buffer'] = body
505
+ return result
506
+ except urllib.error.HTTPError as e:
507
+ raw = e.read() if e.fp else b''
508
+ if response_type == 'binary':
509
+ body = raw
510
+ else:
511
+ body = self._parse_response_body(
512
+ raw.decode('utf-8'), e.headers.get('Content-Type', ''), response_type
513
+ )
514
+ result = {
515
+ 'status_code': e.code,
516
+ 'headers': dict(e.headers),
517
+ 'body': body,
518
+ 'url': url,
519
+ 'elapsed': 0
520
+ }
521
+ if response_type == 'binary':
522
+ result['buffer'] = body
523
+ return result
524
+
525
+ def _parse_response_body(self, body: str, content_type: str, response_type: str = 'auto') -> Any:
526
+ """Parse response body based on content type or explicit response_type."""
527
+ if response_type == 'json':
528
+ try:
529
+ return json.loads(body)
530
+ except json.JSONDecodeError:
531
+ return body
532
+ if response_type == 'text':
533
+ return body
534
+ # auto: detect from content-type
535
+ if 'application/json' in content_type:
536
+ try:
537
+ return json.loads(body)
538
+ except json.JSONDecodeError:
539
+ return body
540
+ return body
541
+
542
+ def _handle_success(self, original_msg: Dict[str, Any], response: Dict[str, Any]):
543
+ """Handle successful response."""
544
+ # Create message with response metadata in payload
545
+ payload = {
546
+ 'statusCode': response['status_code'],
547
+ 'body': response['body'],
548
+ 'headers': response['headers'],
549
+ 'url': response['url'],
550
+ 'elapsed': response['elapsed']
551
+ }
552
+ if 'buffer' in response:
553
+ payload['buffer'] = response['buffer']
554
+ msg = self.create_message(
555
+ payload=payload,
556
+ topic=original_msg.get(MessageKeys.TOPIC, '')
557
+ )
558
+
559
+ # Preserve original message fields (except payload)
560
+ for key in original_msg:
561
+ if key not in msg and key != MessageKeys.PAYLOAD and not key.startswith('_'):
562
+ msg[key] = original_msg[key]
563
+
564
+ self.send(msg)
565
+
566
+ def _handle_error(self, original_msg: Dict[str, Any], error: Exception):
567
+ """Handle request error."""
568
+ logger.error(f"Webhook [{self.name}] request failed: {error}")
569
+
570
+ # Report error to global error handling
571
+ self.report_error(f"HTTP request failed: {error}")
572
+
573
+ # Send error message to output with metadata in payload
574
+ msg = self.create_message(
575
+ payload={
576
+ 'statusCode': 0,
577
+ 'error': str(error),
578
+ 'body': None,
579
+ 'headers': {},
580
+ 'url': self.config.get('url', ''),
581
+ 'elapsed': 0
582
+ },
583
+ topic=f"{original_msg.get(MessageKeys.TOPIC, '')}/error"
584
+ )
585
+
586
+ # Preserve original message fields (except payload)
587
+ for key in original_msg:
588
+ if key not in msg and key != MessageKeys.PAYLOAD and not key.startswith('_'):
589
+ msg[key] = original_msg[key]
590
+
591
+ self.send(msg)
@@ -0,0 +1,109 @@
1
+ """
2
+ Node implementations for the PyNode workflow system.
3
+ Auto-generated - do not edit manually.
4
+ """
5
+
6
+ import importlib
7
+ import inspect
8
+ import logging
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Auto-discover node types
14
+ _node_classes = []
15
+ _node_names = []
16
+
17
+ # Get the nodes directory
18
+ _nodes_dir = Path(__file__).parent
19
+
20
+
21
+ def _discover_node_classes_in_module(module, folder_name):
22
+ """
23
+ Find all BaseNode subclasses in a module.
24
+ Returns list of (class_name, class_object) tuples.
25
+ """
26
+ from pynode.nodes.base_node import BaseNode
27
+
28
+ found_classes = []
29
+ for name, obj in inspect.getmembers(module, inspect.isclass):
30
+ # Check if it's a BaseNode subclass (but not BaseNode itself)
31
+ if (issubclass(obj, BaseNode) and
32
+ obj is not BaseNode and
33
+ name.endswith('Node') and
34
+ obj.__module__.startswith(f'pynode.nodes.{folder_name}')):
35
+ found_classes.append((name, obj))
36
+
37
+ return found_classes
38
+
39
+
40
+ def _try_import_from_init(folder_name):
41
+ """Try to import from __init__.py"""
42
+ try:
43
+ module = importlib.import_module(f'.{folder_name}', package='pynode.nodes')
44
+ return _discover_node_classes_in_module(module, folder_name)
45
+ except ImportError as e:
46
+ # Suppress missing dependency errors silently for cleaner output
47
+ if "No module named" not in str(e):
48
+ logger.warning(f"Error importing {folder_name}: {e}")
49
+ return []
50
+ except Exception as e:
51
+ logger.warning(f"Error importing {folder_name}: {e}")
52
+ return []
53
+
54
+
55
+ def _try_import_python_files(folder_path, folder_name):
56
+ """Try to import from individual Python files in the folder"""
57
+ found_classes = []
58
+
59
+ for py_file in folder_path.glob('*.py'):
60
+ if py_file.name.startswith('_') or py_file.name == '__init__.py':
61
+ continue
62
+
63
+ module_name = py_file.stem
64
+ try:
65
+ module = importlib.import_module(f'.{folder_name}.{module_name}', package='pynode.nodes')
66
+ found_classes.extend(_discover_node_classes_in_module(module, folder_name))
67
+ except ImportError:
68
+ # Suppress missing dependency errors silently
69
+ continue
70
+ except Exception:
71
+ continue
72
+
73
+ return found_classes
74
+
75
+
76
+ # Scan for node folders
77
+ for item in _nodes_dir.iterdir():
78
+ if not item.is_dir() or item.name.startswith('_') or item.name.startswith('.'):
79
+ continue
80
+
81
+ folder_name = item.name
82
+ found_classes = []
83
+
84
+ # Strategy 1: Try importing from __init__.py
85
+ found_classes = _try_import_from_init(folder_name)
86
+
87
+ # Strategy 2: If nothing found, scan individual Python files
88
+ if not found_classes:
89
+ found_classes = _try_import_python_files(item, folder_name)
90
+
91
+ # Register all found node classes
92
+ for class_name, node_class in found_classes:
93
+ if class_name not in _node_names: # Avoid duplicates
94
+ _node_classes.append(node_class)
95
+ _node_names.append(class_name)
96
+ globals()[class_name] = node_class
97
+
98
+
99
+ def get_all_node_types():
100
+ """
101
+ Get a list of all available node types.
102
+
103
+ Returns:
104
+ List of node classes
105
+ """
106
+ return _node_classes
107
+
108
+
109
+ __all__ = _node_names # type: ignore