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,143 @@
1
+ """Install per-node dependencies for an installed PyNode package.
2
+
3
+ Each node under ``pynode/nodes/<NodeName>/`` may ship a ``requirements.txt``
4
+ listing packages it needs. Those files are *not* installed by ``pip install
5
+ pynode-flow`` -- pip only reads the dependencies/extras in ``pyproject.toml``.
6
+
7
+ Most node dependencies are covered by the ``pip install "pynode-flow[full]"``
8
+ extra. This command is the escape hatch for the rest: it walks the node folders
9
+ of the *installed* package (so it works from a plain ``pip install``, not only a
10
+ source checkout) and runs ``pip install -r`` for each node, which is the only
11
+ way to pull dependencies that cannot live in ``pyproject.toml`` -- e.g. vendor
12
+ SDKs like the Omron/Sentech ``stapipy`` (OmronCameraNode).
13
+
14
+ Usage::
15
+
16
+ pynode-install-nodes # install deps for every node
17
+ pynode-install-nodes --node MQTTNode # only the named node(s)
18
+ pynode-install-nodes --list # show nodes with a requirements.txt
19
+ pynode-install-nodes --dry-run # print what would be installed
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import logging
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def _nodes_dir() -> Path:
34
+ """Return the ``nodes`` directory that ships inside this package."""
35
+ return Path(__file__).resolve().parent / "nodes"
36
+
37
+
38
+ def _has_real_requirements(requirements_file: Path) -> bool:
39
+ """True if the file lists at least one non-comment, non-blank requirement."""
40
+ try:
41
+ for raw_line in requirements_file.read_text(encoding="utf-8").splitlines():
42
+ line = raw_line.strip()
43
+ if line and not line.startswith("#"):
44
+ return True
45
+ except OSError:
46
+ return False
47
+ return False
48
+
49
+
50
+ def find_node_requirements(nodes_dir: Path | None = None) -> list[tuple[str, Path]]:
51
+ """Return ``(node_name, requirements_path)`` for nodes with real requirements."""
52
+ nodes_dir = nodes_dir or _nodes_dir()
53
+ found: list[tuple[str, Path]] = []
54
+ if not nodes_dir.is_dir():
55
+ return found
56
+ for node_dir in sorted(p for p in nodes_dir.iterdir() if p.is_dir()):
57
+ if node_dir.name.startswith("__"):
58
+ continue
59
+ requirements_file = node_dir / "requirements.txt"
60
+ if requirements_file.is_file() and _has_real_requirements(requirements_file):
61
+ found.append((node_dir.name, requirements_file))
62
+ return found
63
+
64
+
65
+ def install_node_requirements(
66
+ only: list[str] | None = None,
67
+ dry_run: bool = False,
68
+ nodes_dir: Path | None = None,
69
+ ) -> int:
70
+ """Install per-node requirements. Returns the number of nodes that failed."""
71
+ requirements = find_node_requirements(nodes_dir)
72
+ if only:
73
+ wanted = {name.lower() for name in only}
74
+ requirements = [item for item in requirements if item[0].lower() in wanted]
75
+ missing = wanted - {name.lower() for name, _ in requirements}
76
+ for name in sorted(missing):
77
+ logger.warning("No node named %r with a requirements.txt was found", name)
78
+
79
+ if not requirements:
80
+ logger.info("No node dependencies to install.")
81
+ return 0
82
+
83
+ failures = 0
84
+ for node_name, requirements_file in requirements:
85
+ logger.info("Installing requirements for %s...", node_name)
86
+ cmd = [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]
87
+ if dry_run:
88
+ logger.info(" [dry-run] %s", " ".join(cmd))
89
+ continue
90
+ result = subprocess.run(cmd, check=False)
91
+ if result.returncode == 0:
92
+ logger.info(" ✓ %s dependencies installed", node_name)
93
+ else:
94
+ failures += 1
95
+ logger.error(" ✗ Failed to install dependencies for %s", node_name)
96
+
97
+ installed = len(requirements) - failures
98
+ logger.info(
99
+ "Done. Processed %d node(s); %d succeeded, %d failed.",
100
+ len(requirements),
101
+ installed if not dry_run else 0,
102
+ failures,
103
+ )
104
+ return failures
105
+
106
+
107
+ def main(argv: list[str] | None = None) -> int:
108
+ parser = argparse.ArgumentParser(
109
+ prog="pynode-install-nodes",
110
+ description="Install the per-node Python dependencies bundled with PyNode.",
111
+ )
112
+ parser.add_argument(
113
+ "--node",
114
+ action="append",
115
+ dest="nodes",
116
+ metavar="NodeName",
117
+ help="Only install this node's requirements (repeatable).",
118
+ )
119
+ parser.add_argument(
120
+ "--list",
121
+ action="store_true",
122
+ help="List nodes that ship a requirements.txt and exit.",
123
+ )
124
+ parser.add_argument(
125
+ "--dry-run",
126
+ action="store_true",
127
+ help="Show what would be installed without running pip.",
128
+ )
129
+ args = parser.parse_args(argv)
130
+
131
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
132
+
133
+ if args.list:
134
+ for node_name, requirements_file in find_node_requirements():
135
+ print(f"{node_name}: {requirements_file}")
136
+ return 0
137
+
138
+ failures = install_node_requirements(only=args.nodes, dry_run=args.dry_run)
139
+ return 1 if failures else 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ raise SystemExit(main())
pynode/main.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ PyNode - A Node-RED-like Visual Workflow System with Python Backend
3
+
4
+ This is a complete system for creating visual workflows with Python nodes.
5
+ Each node represents a function/operation and can be connected to create data pipelines.
6
+
7
+ Quick Start:
8
+ 1. Install: pip install -e .
9
+ 2. Run: pynode
10
+ 3. Open browser: http://localhost:5000
11
+
12
+ Usage Example:
13
+ from pynode.workflow_engine import WorkflowEngine
14
+ from pynode.nodes import InjectNode, DebugNode
15
+
16
+ engine = WorkflowEngine()
17
+ engine.register_node_type(InjectNode)
18
+ engine.register_node_type(DebugNode)
19
+
20
+ inject = engine.create_node('InjectNode', name='my_inject')
21
+ debug = engine.create_node('DebugNode', name='my_debug')
22
+ engine.connect_nodes(inject.id, debug.id)
23
+ engine.start()
24
+ """
25
+
26
+ import argparse
27
+ import logging
28
+ import os
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def main():
34
+ """Main entry point for PyNode application."""
35
+ # Parse command line arguments
36
+ parser = argparse.ArgumentParser(description='PyNode Visual Workflow System')
37
+ parser.add_argument('--production', action='store_true',
38
+ help='Run in production mode using Waitress server')
39
+ parser.add_argument('--host', default='0.0.0.0',
40
+ help='Host address to bind to (default: 0.0.0.0)')
41
+ parser.add_argument('--port', type=int, default=5000,
42
+ help='Port to bind to (default: 5000)')
43
+ parser.add_argument('--log-level', default='INFO',
44
+ help='Logging level (DEBUG, INFO, WARNING, ERROR). Default: INFO')
45
+ parser.add_argument('--cors-origins', default=None,
46
+ help='Comma-separated list of allowed CORS origins '
47
+ '(default: * i.e. all origins). '
48
+ 'Also settable via the PYNODE_CORS_ORIGINS env var.')
49
+ parser.add_argument('--api-key', default=None,
50
+ help='Require this API key on all /api/ requests '
51
+ '(X-API-Key header or api_key query parameter). '
52
+ 'Also settable via the PYNODE_API_KEY env var. '
53
+ 'Default: no authentication.')
54
+ parser.add_argument('--data-dir', default=None,
55
+ help='Directory for PyNode data; workflows are '
56
+ 'persisted under <data-dir>/workflows/. '
57
+ 'Also settable via the PYNODE_DATA_DIR env var. '
58
+ 'Default: the source checkout root when running '
59
+ 'from a checkout, otherwise ~/.pynode.')
60
+ args = parser.parse_args()
61
+
62
+ # Configure application-wide logging
63
+ logging.basicConfig(
64
+ level=getattr(logging, args.log_level.upper(), logging.INFO),
65
+ format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
66
+ )
67
+
68
+ # CORS and the data directory are configured when pynode.server is first
69
+ # imported (it builds the module-level default app), so the CLI flags
70
+ # must be exported to the environment BEFORE the import below.
71
+ # (This is why the server import lives inside main() rather than at
72
+ # module top.) The env var names live in pynode.config.
73
+ if args.cors_origins is not None:
74
+ os.environ['PYNODE_CORS_ORIGINS'] = args.cors_origins
75
+ if args.data_dir is not None:
76
+ os.environ['PYNODE_DATA_DIR'] = args.data_dir
77
+
78
+ from pynode.server import app, load_workflow_from_disk
79
+
80
+ # The API key is read dynamically from app.config on every request, so
81
+ # it can simply be set after import. CLI flag wins over the env var
82
+ # (which server.py already applied as the app.config default).
83
+ if args.api_key is not None:
84
+ app.config['PYNODE_API_KEY'] = args.api_key
85
+
86
+ if app.config.get('PYNODE_API_KEY'):
87
+ logger.info("API key authentication ENABLED: /api/ requests require "
88
+ "the X-API-Key header or api_key query parameter")
89
+ if args.cors_origins is not None:
90
+ logger.info(f"CORS allowed origins: {args.cors_origins}")
91
+
92
+ # Load workflow from disk on startup
93
+ logger.info("Loading workflow from disk...")
94
+ load_workflow_from_disk()
95
+
96
+ if args.production:
97
+ # Production mode with Waitress
98
+ try:
99
+ from waitress import serve
100
+ logger.info("Starting PyNode server in PRODUCTION mode...")
101
+ logger.info(f"Server running at: http://{args.host}:{args.port}")
102
+ logger.info("Supports unlimited concurrent connections")
103
+ serve(app, host=args.host, port=args.port, threads=10,
104
+ channel_timeout=120, backlog=1024, connection_limit=1000)
105
+ except ImportError:
106
+ logger.error("waitress not installed. Install with: pip install waitress")
107
+ logger.warning("Falling back to development server...")
108
+ app.run(debug=False, host=args.host, port=args.port, threaded=True)
109
+ else:
110
+ # Development mode with Flask built-in server
111
+ logger.info("Starting PyNode server in DEVELOPMENT mode...")
112
+ logger.info(f"Server running at: http://{args.host}:{args.port}")
113
+ logger.info("For production use: pynode --production")
114
+ app.run(debug=False, host=args.host, port=args.port, threaded=True)
115
+
116
+
117
+ if __name__ == '__main__':
118
+ main()
@@ -0,0 +1,156 @@
1
+ """Static node-type registries built once at import time.
2
+
3
+ Node classes are static for the lifetime of the process, so everything here
4
+ (the reference engine used for introspection, the node-types cache served by
5
+ /api/node-types, and the api_routes / sse_handlers registries used for
6
+ dynamic route registration and SSE broadcasting) is safely shared by every
7
+ Flask app / WorkflowManager instance.
8
+ """
9
+
10
+ import logging
11
+
12
+ from pynode.workflow_engine import WorkflowEngine
13
+ from pynode import nodes
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def create_workflow_engine():
19
+ """Create a new WorkflowEngine with all node types registered."""
20
+ engine = WorkflowEngine()
21
+ for node_class in nodes.get_all_node_types():
22
+ engine.register_node_type(node_class)
23
+ return engine
24
+
25
+
26
+ # Reference engine for node type introspection only
27
+ reference_engine = create_workflow_engine()
28
+
29
+ # Cache for node types (built once at startup or on first request)
30
+ _node_types_cache = None
31
+
32
+
33
+ def build_node_types_cache():
34
+ """Build the node types cache from registered node types."""
35
+ global _node_types_cache
36
+
37
+ from pynode.nodes.base_node import BaseNode
38
+ base_properties = getattr(BaseNode, 'properties', [])
39
+
40
+ # Define category ordering (compared case-insensitively; display casing is
41
+ # applied by the frontend, e.g. 'opencv' renders as 'OpenCV').
42
+ category_order = [
43
+ 'common',
44
+ 'input',
45
+ 'output',
46
+ 'function',
47
+ 'logic',
48
+ 'network',
49
+ 'vision',
50
+ 'analysis',
51
+ 'node probes',
52
+ 'opencv'
53
+ ]
54
+ category_rank = {c.lower(): i for i, c in enumerate(category_order)}
55
+
56
+ node_types = []
57
+ for name, node_class in reference_engine.node_types.items():
58
+ # Skip ErrorNode - it's a system node that shouldn't be manually added
59
+ if node_class.hidden:
60
+ continue
61
+
62
+ display_name = getattr(node_class, 'display_name', name)
63
+ icon = getattr(node_class, 'icon', '◆')
64
+ category = getattr(node_class, 'category', 'custom')
65
+ color = getattr(node_class, 'color', '#2d2d30')
66
+ border_color = getattr(node_class, 'border_color', '#555')
67
+ text_color = getattr(node_class, 'text_color', '#d4d4d4')
68
+ input_count = getattr(node_class, 'input_count', 1)
69
+ output_count = getattr(node_class, 'output_count', 1)
70
+
71
+ # Handle callable properties (get_properties classmethod)
72
+ if hasattr(node_class, 'get_properties') and callable(node_class.get_properties):
73
+ node_properties = node_class.get_properties()
74
+ else:
75
+ node_properties = getattr(node_class, 'properties', [])
76
+
77
+ # Ensure node_properties is iterable (list or tuple)
78
+ if not isinstance(node_properties, (list, tuple)):
79
+ node_properties = []
80
+
81
+ # Get property names from node-specific properties to avoid duplicates
82
+ node_prop_names = {prop.get('name') for prop in node_properties if isinstance(prop, dict)}
83
+
84
+ # Add base properties that aren't overridden by node-specific properties
85
+ merged_properties = [prop for prop in base_properties if prop.get('name') not in node_prop_names]
86
+ # Add all node-specific properties
87
+ merged_properties.extend(node_properties)
88
+
89
+ ui_component = getattr(node_class, 'ui_component', None)
90
+ ui_component_config = getattr(node_class, 'ui_component_config', {})
91
+ info = getattr(node_class, 'info', '')
92
+
93
+ node_types.append({
94
+ 'type': name,
95
+ 'name': display_name,
96
+ 'icon': icon,
97
+ 'category': category,
98
+ 'color': color,
99
+ 'borderColor': border_color,
100
+ 'textColor': text_color,
101
+ 'inputCount': input_count,
102
+ 'outputCount': output_count,
103
+ 'properties': merged_properties,
104
+ 'uiComponent': ui_component,
105
+ 'uiComponentConfig': ui_component_config,
106
+ 'info': info
107
+ })
108
+
109
+ # Sort node types by category order, then alphabetically by name within
110
+ # each category. Category comparison is case-insensitive so e.g. nodes
111
+ # declaring 'opencv' or 'OpenCV' land in the same bucket.
112
+ def get_category_sort_key(node_type):
113
+ category = str(node_type['category']).lower()
114
+ # Categories not in the list go to the end (custom/system/third party)
115
+ order_index = category_rank.get(category, len(category_order))
116
+ return (order_index, node_type['name'].lower(), node_type['name'])
117
+
118
+ node_types.sort(key=get_category_sort_key)
119
+
120
+ _node_types_cache = node_types
121
+ return _node_types_cache
122
+
123
+
124
+ def get_node_types():
125
+ """Return the node-types cache, building it on first use if needed."""
126
+ if _node_types_cache is None:
127
+ build_node_types_cache()
128
+ return _node_types_cache
129
+
130
+
131
+ # Build a registry of node types that have SSE handlers
132
+ # Maps node_type_name -> list of sse_handler defs
133
+ sse_handler_registry = {}
134
+
135
+ # Build a registry of node types that have api_routes
136
+ # Maps node_type_name -> list of route defs
137
+ api_route_registry = {}
138
+
139
+
140
+ def _build_node_registries():
141
+ """Scan all node classes and build registries for API routes and SSE handlers."""
142
+ for node_class in nodes.get_all_node_types():
143
+ node_type_name = node_class.__name__
144
+
145
+ api_routes = getattr(node_class, 'api_routes', [])
146
+ if api_routes:
147
+ api_route_registry[node_type_name] = api_routes
148
+
149
+ sse_handlers = getattr(node_class, 'sse_handlers', [])
150
+ if sse_handlers:
151
+ sse_handler_registry[node_type_name] = sse_handlers
152
+
153
+
154
+ # Build caches/registries at import
155
+ build_node_types_cache()
156
+ _build_node_registries()
@@ -0,0 +1,151 @@
1
+ """
2
+ Auto Sync Node - joins messages from multiple inputs and outputs them together.
3
+ Simply buffers the latest message from each input and outputs all when a new message arrives.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
8
+
9
+ _info = Info()
10
+ _info.add_text("Joins messages from multiple inputs by buffering the latest message from each input and outputting them together when any new message arrives.")
11
+ _info.add_header("Inputs")
12
+ _info.add_bullets(
13
+ ("Input 0-N:", "Messages to synchronize. The node buffers the latest message from each input."),
14
+ )
15
+ _info.add_header("Output")
16
+ _info.add_bullets(
17
+ ("payload.messages:", "Array of payloads from all buffered messages."),
18
+ ("payload.sync_values:", "Array of sync property values from each message."),
19
+ ("payload.delta:", "Difference between max and min sync values."),
20
+ )
21
+ _info.add_header("Properties")
22
+ _info.add_bullets(
23
+ ("Number of Inputs:", "How many input ports to synchronize (2-10)."),
24
+ ("Sync Property Path:", "Property path to extract for delta calculation (e.g., 'frame_count')."),
25
+ )
26
+
27
+
28
+ class AutoSyncNode(BaseNode):
29
+ """
30
+ Auto Sync node - joins messages from multiple inputs.
31
+ Buffers the latest message from each input and outputs them together
32
+ along with the delta between sync property values.
33
+ """
34
+ info = str(_info)
35
+ display_name = 'Auto Sync'
36
+ icon = '⚡'
37
+ category = 'logic'
38
+ color = '#FFB347'
39
+ border_color = '#CC8F39'
40
+ text_color = '#000000'
41
+ input_count = 2 # Default, can be changed via properties
42
+ output_count = 1
43
+
44
+ DEFAULT_CONFIG = {
45
+ 'input_count': 2,
46
+ 'sync_property': 'frame_count'
47
+ }
48
+
49
+ properties = [
50
+ {
51
+ 'name': 'input_count',
52
+ 'label': 'Number of Inputs',
53
+ 'type': 'number',
54
+ 'default': DEFAULT_CONFIG['input_count'],
55
+ 'min': 2,
56
+ 'max': 10,
57
+ 'help': 'Number of input ports to join'
58
+ },
59
+ {
60
+ 'name': 'sync_property',
61
+ 'label': 'Sync Property Path',
62
+ 'type': 'text',
63
+ 'default': DEFAULT_CONFIG['sync_property'],
64
+ 'help': 'Property path to compare for delta (e.g., "frame_count", "timestamp")'
65
+ }
66
+ ]
67
+
68
+ def __init__(self, node_id=None, name="auto sync"):
69
+ super().__init__(node_id, name)
70
+ self._buffers: Dict[int, Optional[Dict[str, Any]]] = {}
71
+
72
+ def configure(self, config: Dict[str, Any]):
73
+ """Configure node and update input count."""
74
+ super().configure(config)
75
+
76
+ new_input_count = self.get_config_int('input_count', 2)
77
+ if new_input_count != self.input_count:
78
+ self.input_count = new_input_count
79
+ self._buffers = {i: None for i in range(self.input_count)}
80
+
81
+ def on_start(self):
82
+ """Initialize buffers on start."""
83
+ super().on_start()
84
+ self._buffers = {i: None for i in range(self.input_count)}
85
+
86
+ def _get_sync_value(self, msg: Dict[str, Any]) -> Optional[float]:
87
+ """Extract the sync property value from a message."""
88
+ sync_property = self.config.get('sync_property', 'frame_count')
89
+
90
+ # Try direct message property first
91
+ value = msg.get(sync_property)
92
+
93
+ # If not found, try nested path
94
+ if value is None:
95
+ value = self._get_nested_value(msg, sync_property)
96
+
97
+ # If still not found, try in payload
98
+ if value is None and MessageKeys.PAYLOAD in msg:
99
+ value = self._get_nested_value(msg[MessageKeys.PAYLOAD], sync_property)
100
+
101
+ # Convert to float if possible
102
+ if value is not None:
103
+ try:
104
+ return float(value)
105
+ except (ValueError, TypeError):
106
+ return None
107
+
108
+ return None
109
+
110
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
111
+ """Process incoming messages."""
112
+ if input_index >= self.input_count:
113
+ self.report_error(f"Received message on invalid input {input_index}")
114
+ return
115
+
116
+ # Buffer the message
117
+ self._buffers[input_index] = msg
118
+
119
+ # Check if all inputs have messages
120
+ messages = []
121
+ sync_values = []
122
+
123
+ for i in range(self.input_count):
124
+ buffered = self._buffers.get(i)
125
+ if buffered is None:
126
+ return # Not all inputs have messages yet
127
+ messages.append(buffered)
128
+ sync_values.append(self._get_sync_value(buffered))
129
+
130
+ # All inputs have messages - compute delta and output
131
+ valid_values = [v for v in sync_values if v is not None]
132
+ if valid_values:
133
+ delta = max(valid_values) - min(valid_values)
134
+ else:
135
+ delta = None
136
+
137
+ # Build output
138
+ out_msg = {
139
+ MessageKeys.PAYLOAD: {
140
+ 'messages': [m.get(MessageKeys.PAYLOAD) for m in messages],
141
+ 'sync_values': sync_values,
142
+ 'delta': delta
143
+ }
144
+ }
145
+
146
+ # Copy other properties from first message
147
+ for key in messages[0]:
148
+ if key not in (MessageKeys.PAYLOAD, MessageKeys.MSG_ID):
149
+ out_msg[key] = messages[0][key]
150
+
151
+ self.send(out_msg)
@@ -0,0 +1,3 @@
1
+ from .bbox_metrics_node import BBoxMetricsNode
2
+
3
+ __all__ = ['BBoxMetricsNode']