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,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)
|
pynode/nodes/__init__.py
ADDED
|
@@ -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
|