pynode-flow 0.2.2__tar.gz → 0.2.3__tar.gz

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 (303) hide show
  1. pynode_flow-0.2.3/DOCKER.md +189 -0
  2. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/PKG-INFO +20 -1
  3. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/README.md +19 -0
  4. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docker-compose.yml +6 -1
  5. pynode_flow-0.2.3/plan-pynodeReview-2026-07-18.prompt.md +135 -0
  6. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/_version.py +3 -3
  7. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/config.py +42 -0
  8. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/main.py +9 -0
  9. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/image_viewer_node.py +16 -9
  10. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/link_bus.py +7 -1
  11. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/omron_camera_node.py +13 -3
  12. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SwitchNode/switch_node.py +8 -2
  13. pynode_flow-0.2.3/pynode/nodes/UltralyticsNode/__init__.py +11 -0
  14. pynode_flow-0.2.3/pynode/nodes/UltralyticsNode/model_paths.py +115 -0
  15. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/UltralyticsNode/ultralytics_node.py +15 -9
  16. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/__init__.py +6 -5
  17. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/base_node.py +160 -52
  18. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_manager.py +28 -3
  19. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/workflow_engine.py +0 -1
  20. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/PKG-INFO +20 -1
  21. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/SOURCES.txt +4 -0
  22. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/scm_file_list.json +4 -0
  23. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/scm_version.json +2 -2
  24. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pyproject.toml +7 -0
  25. pynode_flow-0.2.3/tests/test_models_dir.py +230 -0
  26. pynode_flow-0.2.3/tests/test_send_copy_semantics.py +226 -0
  27. pynode_flow-0.2.2/DOCKER.md +0 -113
  28. pynode_flow-0.2.2/pynode/nodes/UltralyticsNode/__init__.py +0 -4
  29. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.claude/skills/pynode-workflows/SKILL.md +0 -0
  30. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.dockerignore +0 -0
  31. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.github/workflows/ci.yml +0 -0
  32. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.github/workflows/publish.yaml +0 -0
  33. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.gitignore +0 -0
  34. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/Dockerfile +0 -0
  35. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/Dockerfile.cpu +0 -0
  36. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/INSTALL.md +0 -0
  37. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/LICENSE +0 -0
  38. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/MANIFEST.in +0 -0
  39. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/CREATING_NODES.md +0 -0
  40. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/CUSTOM_NODES.md +0 -0
  41. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/DOCKER.md +0 -0
  42. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/EXTENSIBILITY.md +0 -0
  43. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/UI_COMPONENTS.md +0 -0
  44. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/examples/README.md +0 -0
  45. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/examples/camera_workflow.py +0 -0
  46. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/examples/camera_yolo_workflow.py +0 -0
  47. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/install_nodes.bat +0 -0
  48. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/install_nodes.sh +0 -0
  49. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/plan-pynodeRepoReview.prompt.md +0 -0
  50. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/__init__.py +0 -0
  51. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/__main__.py +0 -0
  52. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/_perf.py +0 -0
  53. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/__init__.py +0 -0
  54. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/helpers.py +0 -0
  55. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/nodes.py +0 -0
  56. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/services.py +0 -0
  57. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/sse.py +0 -0
  58. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/uploads.py +0 -0
  59. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/workflows.py +0 -0
  60. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/install_nodes.py +0 -0
  61. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/node_registry.py +0 -0
  62. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
  63. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
  64. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
  65. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
  66. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/__init__.py +0 -0
  67. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/batch_node.py +0 -0
  68. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/requirements.txt +0 -0
  69. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/__init__.py +0 -0
  70. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/camera_node.py +0 -0
  71. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/requirements.txt +0 -0
  72. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/__init__.py +0 -0
  73. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/change_node.py +0 -0
  74. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/requirements.txt +0 -0
  75. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
  76. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
  77. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
  78. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CounterNode/__init__.py +0 -0
  79. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CounterNode/counter_node.py +0 -0
  80. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CropNode/__init__.py +0 -0
  81. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CropNode/crop_node.py +0 -0
  82. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CropNode/requirements.txt +0 -0
  83. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/__init__.py +0 -0
  84. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/debug_node.py +0 -0
  85. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/requirements.txt +0 -0
  86. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DelayNode/delay_node.py +0 -0
  87. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DelayNode/requirements.txt +0 -0
  88. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
  89. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
  90. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
  91. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ErrorNode/error_node.py +0 -0
  92. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ErrorNode/requirements.txt +0 -0
  93. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/__init__.py +0 -0
  94. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/filter_node.py +0 -0
  95. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/requirements.txt +0 -0
  96. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
  97. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
  98. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
  99. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/__init__.py +0 -0
  100. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/function_node.py +0 -0
  101. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/requirements.txt +0 -0
  102. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/GateNode/gate_node.py +0 -0
  103. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/GateNode/requirements.txt +0 -0
  104. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
  105. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
  106. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
  107. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
  108. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
  109. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
  110. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
  111. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
  112. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
  113. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
  114. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
  115. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
  116. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
  117. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
  118. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
  119. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +0 -0
  120. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
  121. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
  122. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
  123. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/inference_node.py +0 -0
  124. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/requirements.txt +0 -0
  125. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InjectNode/inject_node.py +0 -0
  126. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InjectNode/requirements.txt +0 -0
  127. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/__init__.py +0 -0
  128. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/join_node.py +0 -0
  129. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/requirements.txt +0 -0
  130. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
  131. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
  132. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
  133. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/__init__.py +0 -0
  134. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/link_in_node.py +0 -0
  135. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/link_out_node.py +0 -0
  136. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/requirements.txt +0 -0
  137. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/__init__.py +0 -0
  138. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
  139. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
  140. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
  141. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/requirements.txt +0 -0
  142. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
  143. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
  144. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
  145. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
  146. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
  147. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/__init__.py +0 -0
  148. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
  149. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
  150. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blend_node.py +0 -0
  151. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
  152. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blur_node.py +0 -0
  153. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/color_space_node.py +0 -0
  154. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/colormap_node.py +0 -0
  155. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/contours_node.py +0 -0
  156. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
  157. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/draw_node.py +0 -0
  158. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
  159. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
  160. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/fft_node.py +0 -0
  161. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
  162. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
  163. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/histogram_node.py +0 -0
  164. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
  165. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
  166. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/in_range_node.py +0 -0
  167. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/morphology_node.py +0 -0
  168. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
  169. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/paste_node.py +0 -0
  170. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/perspective_node.py +0 -0
  171. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
  172. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/requirements.txt +0 -0
  173. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/resize_node.py +0 -0
  174. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/rotate_node.py +0 -0
  175. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/template_match_node.py +0 -0
  176. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/threshold_node.py +0 -0
  177. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
  178. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
  179. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
  180. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
  181. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
  182. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
  183. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
  184. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
  185. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
  186. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
  187. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
  188. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
  189. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
  190. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/__init__.py +0 -0
  191. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/range_node.py +0 -0
  192. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/requirements.txt +0 -0
  193. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/__init__.py +0 -0
  194. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
  195. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/requirements.txt +0 -0
  196. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
  197. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
  198. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
  199. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/README.md +0 -0
  200. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/__init__.py +0 -0
  201. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
  202. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/requirements.txt +0 -0
  203. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
  204. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
  205. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/__init__.py +0 -0
  206. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/requirements.txt +0 -0
  207. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/split_node.py +0 -0
  208. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SwitchNode/requirements.txt +0 -0
  209. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SyncNode/__init__.py +0 -0
  210. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SyncNode/sync_node.py +0 -0
  211. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TemplateNode/requirements.txt +0 -0
  212. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TemplateNode/template_node.py +0 -0
  213. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/__init__.py +0 -0
  214. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
  215. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
  216. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
  217. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/UnknownNode/__init__.py +0 -0
  218. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VLLMNode/__init__.py +0 -0
  219. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
  220. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
  221. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoReaderNode/video_reader_node.py +0 -0
  222. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
  223. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
  224. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/WebhookNode/__init__.py +0 -0
  225. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
  226. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/image_utils.py +0 -0
  227. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/info.py +0 -0
  228. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/__init__.py +0 -0
  229. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
  230. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
  231. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/requirements.txt +0 -0
  232. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/messages.py +0 -0
  233. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/server.py +0 -0
  234. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/01-hello-world.json +0 -0
  235. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/02-function-transform.json +0 -0
  236. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/03-switch-routing.json +0 -0
  237. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/04-flow-control.json +0 -0
  238. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/05-webcam-viewer.json +0 -0
  239. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/06-webcam-yolo.json +0 -0
  240. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/07-blur-people.json +0 -0
  241. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
  242. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
  243. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
  244. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/README.md +0 -0
  245. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/manifest.json +0 -0
  246. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/images/favicon4.png +0 -0
  247. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/index.html +0 -0
  248. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/README.md +0 -0
  249. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/auth.js +0 -0
  250. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/clipboard.js +0 -0
  251. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/config.js +0 -0
  252. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/connections.js +0 -0
  253. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/debug.js +0 -0
  254. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/events.js +0 -0
  255. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/history.js +0 -0
  256. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/main.js +0 -0
  257. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/minimap.js +0 -0
  258. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/mqtt-services.js +0 -0
  259. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/nodes.js +0 -0
  260. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/palette.js +0 -0
  261. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/properties.js +0 -0
  262. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/selection.js +0 -0
  263. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/state.js +0 -0
  264. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/ui-utils.js +0 -0
  265. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/viewport.js +0 -0
  266. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/workflow.js +0 -0
  267. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/workflows.js +0 -0
  268. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/style.css +0 -0
  269. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/workflow_manager.py +0 -0
  270. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/dependency_links.txt +0 -0
  271. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/entry_points.txt +0 -0
  272. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/requires.txt +0 -0
  273. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/top_level.txt +0 -0
  274. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/requirements.txt +0 -0
  275. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.bat +0 -0
  276. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.cfg +0 -0
  277. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.py +0 -0
  278. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.sh +0 -0
  279. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/__init__.py +0 -0
  280. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/conftest.py +0 -0
  281. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_auth.py +0 -0
  282. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_errors.py +0 -0
  283. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_nodes.py +0 -0
  284. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_security.py +0 -0
  285. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_workflows.py +0 -0
  286. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_base_node_messaging.py +0 -0
  287. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_camera_node_backend.py +0 -0
  288. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_concurrency.py +0 -0
  289. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_config.py +0 -0
  290. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_counter_sse.py +0 -0
  291. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_debug_workflow_scope.py +0 -0
  292. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_delay_rate_limit.py +0 -0
  293. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_device_detection.py +0 -0
  294. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_link_nodes.py +0 -0
  295. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_message_reader_pickle.py +0 -0
  296. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_mqtt_services.py +0 -0
  297. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_node_registration.py +0 -0
  298. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_node_types_api.py +0 -0
  299. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_persistence.py +0 -0
  300. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_serialization.py +0 -0
  301. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_video_reader_node.py +0 -0
  302. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_workflow_engine.py +0 -0
  303. {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_workflow_roundtrip.py +0 -0
@@ -0,0 +1,189 @@
1
+ # Docker Setup for PyNode
2
+
3
+ ## Prerequisites
4
+
5
+ - Docker Engine with GPU support (NVIDIA Container Toolkit)
6
+ - Docker Compose v1.28+ (for GPU support)
7
+ - NVIDIA GPU with CUDA support
8
+
9
+ ## Quick Start
10
+
11
+ ### Build and Run
12
+
13
+ ```bash
14
+ docker-compose up --build
15
+ ```
16
+
17
+ ### Run in Background
18
+
19
+ ```bash
20
+ docker-compose up -d
21
+ ```
22
+
23
+ ### View Logs
24
+
25
+ ```bash
26
+ docker-compose logs -f
27
+ ```
28
+
29
+ ### Stop the Application
30
+
31
+ ```bash
32
+ docker-compose down
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ ### Ports
38
+
39
+ The application runs on port `5000` by default. To change the port, edit `docker-compose.yml`:
40
+
41
+ ```yaml
42
+ ports:
43
+ - "8080:5000" # Change 8080 to your desired port
44
+ ```
45
+
46
+ ### GPU Support
47
+
48
+ The Docker Compose file is configured to use all available NVIDIA GPUs. To limit GPU access:
49
+
50
+ ```yaml
51
+ deploy:
52
+ resources:
53
+ reservations:
54
+ devices:
55
+ - driver: nvidia
56
+ device_ids: ['0'] # Use only GPU 0
57
+ capabilities: [gpu]
58
+ ```
59
+
60
+ ### Persistent Storage
61
+
62
+ The following directories are mounted for persistence:
63
+ - `./workflows` - Saved workflows
64
+ - `./logs` - Application logs
65
+
66
+ ## Building
67
+
68
+ ### Custom CUDA Version
69
+
70
+ To use a different CUDA version, edit the `Dockerfile`:
71
+
72
+ ```dockerfile
73
+ FROM nvidia/cuda:12.8.0-runtime-ubuntu22.04 # Change CUDA version
74
+ ```
75
+
76
+ ### Without GPU Support
77
+
78
+ For CPU-only deployment, use the CPU Dockerfile:
79
+
80
+ ```bash
81
+ docker build -f Dockerfile.cpu -t pynode-cpu .
82
+ docker run -p 5000:5000 pynode-cpu
83
+ ```
84
+
85
+ ## Accessing the Application
86
+
87
+ Once running, access the web interface at:
88
+ - http://localhost:5000
89
+
90
+ ## mDNS / Service Discovery
91
+
92
+ ### The Limitation
93
+
94
+ The default `docker-compose.yml` uses Docker's standard bridge network. mDNS
95
+ (multicast DNS) relies on link-local multicast traffic (UDP `224.0.0.251:5353`),
96
+ which the bridge network's NAT does not forward in either direction. With the
97
+ default compose file, PyNode's mDNS nodes can broadcast and listen *inside*
98
+ the container, but that traffic never reaches the LAN — so other machines on
99
+ your network cannot discover the container, and the container cannot discover
100
+ services running elsewhere.
101
+
102
+ ### `HOST_IP`
103
+
104
+ The `HOST_IP` environment variable (see `docker-compose.yml`) only controls
105
+ the IP address **advertised** in mDNS service records - it does *not* make
106
+ multicast traffic reach the LAN. Setting `HOST_IP` without one of the fixes
107
+ below just means the (unreachable) mDNS records will advertise the right
108
+ address instead of the container's internal one; it does not fix discovery.
109
+
110
+ ### Fixing It
111
+
112
+ **Option 1: `network_mode: host` (Linux hosts only, full fix)**
113
+
114
+ On Linux, host networking removes the network isolation between the
115
+ container and the host, so multicast works exactly as it would for a
116
+ process running directly on the host:
117
+
118
+ ```yaml
119
+ services:
120
+ pynode:
121
+ network_mode: host
122
+ # ports: mapping is ignored in host mode - the container shares the
123
+ # host's network stack directly, so remove/comment out `ports:` above.
124
+ ```
125
+
126
+ This does **not** work on Docker Desktop for Windows/macOS - Docker Desktop
127
+ runs containers inside a lightweight VM, and `network_mode: host` there still
128
+ does not bridge multicast across the VM boundary.
129
+
130
+ **Option 2: macvlan network**
131
+
132
+ A macvlan network gives the container its own IP address directly on the LAN
133
+ (as if it were a separate physical device), which also allows multicast to
134
+ work normally. This works on Docker Desktop as well as Linux, but requires
135
+ more setup (a dedicated IP, and typically a way for the host itself to reach
136
+ the container since macvlan interfaces are not reachable from the host by
137
+ default). See the
138
+ [Docker macvlan network documentation](https://docs.docker.com/network/drivers/macvlan/)
139
+ for setup details.
140
+
141
+ **Option 3: mDNS reflector on the host**
142
+
143
+ When host networking isn't available (e.g. Docker Desktop), run an mDNS
144
+ reflector on the host to bridge multicast between the host's network and the
145
+ container's bridge network:
146
+
147
+ - **avahi-daemon** (Linux) - enable reflection in `/etc/avahi/avahi-daemon.conf`:
148
+ ```ini
149
+ [reflector]
150
+ enable-reflector=yes
151
+ ```
152
+ - **mdns-repeater** - run as a sidecar/host process that repeats mDNS packets
153
+ between interfaces.
154
+
155
+ This is the option that works when host networking isn't an option, at the
156
+ cost of an extra moving part to run and maintain.
157
+
158
+ ### Port Matching
159
+
160
+ Whichever option you use, the port PyNode advertises in its mDNS records
161
+ must match the port other machines can actually reach it on (the *published*
162
+ port), not just the container's internal port. If you remap ports (e.g.
163
+ `"8080:5000"`), make sure discovery consumers connect using the published
164
+ port (`8080`), not the internal one.
165
+
166
+ ## Troubleshooting
167
+
168
+ ### GPU Not Detected
169
+
170
+ Verify NVIDIA Container Toolkit is installed:
171
+ ```bash
172
+ docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi
173
+ ```
174
+
175
+ ### Permission Issues
176
+
177
+ Ensure the mounted directories have proper permissions:
178
+ ```bash
179
+ mkdir -p workflows logs
180
+ chmod 777 workflows logs
181
+ ```
182
+
183
+ ### Rebuild from Scratch
184
+
185
+ ```bash
186
+ docker-compose down
187
+ docker-compose build --no-cache
188
+ docker-compose up
189
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynode-flow
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Python based Node-RED-like visual workflow system
5
5
  Author: olkham
6
6
  License-Expression: MIT
@@ -214,6 +214,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
214
214
 
215
215
  The resolved location is logged at startup (`Workflow data directory: ...`).
216
216
 
217
+ ### Models Directory
218
+
219
+ Nodes that download or generate model weights (e.g. the YOLO node's `.pt`
220
+ files and exported OpenVINO models) write them into a shared **models
221
+ directory** instead of the process working directory. It is resolved in this
222
+ order:
223
+
224
+ 1. `pynode --models-dir <path>` CLI flag,
225
+ 2. `PYNODE_MODELS_DIR` environment variable,
226
+ 3. `<data dir>/models` otherwise — so `<repo>/models` for a source checkout and
227
+ `~/.pynode/models` for a regular `pip install`.
228
+
229
+ > **Upgrading:** older PyNode versions downloaded model files into whatever the
230
+ > working directory happened to be, so stray `.pt` files and
231
+ > `*_openvino_model/` folders may exist in the repo root, `pynode/models/` or
232
+ > `pynode/nodes/`. PyNode still reads models from those legacy locations, but
233
+ > nothing is migrated automatically — you can move them into the models
234
+ > directory manually when convenient.
235
+
217
236
  ## Securing PyNode
218
237
 
219
238
  **PyNode executes arbitrary Python by design** (e.g. FunctionNode runs whatever code is in the workflow), so anyone who can reach the API can run code on the host. Authentication is the trust boundary — secure the server before exposing it beyond your own machine:
@@ -149,6 +149,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
149
149
 
150
150
  The resolved location is logged at startup (`Workflow data directory: ...`).
151
151
 
152
+ ### Models Directory
153
+
154
+ Nodes that download or generate model weights (e.g. the YOLO node's `.pt`
155
+ files and exported OpenVINO models) write them into a shared **models
156
+ directory** instead of the process working directory. It is resolved in this
157
+ order:
158
+
159
+ 1. `pynode --models-dir <path>` CLI flag,
160
+ 2. `PYNODE_MODELS_DIR` environment variable,
161
+ 3. `<data dir>/models` otherwise — so `<repo>/models` for a source checkout and
162
+ `~/.pynode/models` for a regular `pip install`.
163
+
164
+ > **Upgrading:** older PyNode versions downloaded model files into whatever the
165
+ > working directory happened to be, so stray `.pt` files and
166
+ > `*_openvino_model/` folders may exist in the repo root, `pynode/models/` or
167
+ > `pynode/nodes/`. PyNode still reads models from those legacy locations, but
168
+ > nothing is migrated automatically — you can move them into the models
169
+ > directory manually when convenient.
170
+
152
171
  ## Securing PyNode
153
172
 
154
173
  **PyNode executes arbitrary Python by design** (e.g. FunctionNode runs whatever code is in the workflow), so anyone who can reach the API can run code on the host. Authentication is the trust boundary — secure the server before exposing it beyond your own machine:
@@ -5,7 +5,12 @@ services:
5
5
  dockerfile: Dockerfile
6
6
  container_name: pynode
7
7
  ports:
8
- - "500:5000"
8
+ - "5000:5000"
9
+ # Uncomment for mDNS support on Linux hosts (see DOCKER.md > mDNS /
10
+ # Service Discovery). Not effective on Docker Desktop for Windows/macOS.
11
+ # Remove the `ports:` mapping above when enabling this - it is ignored
12
+ # in host networking mode.
13
+ # network_mode: host
9
14
  volumes:
10
15
  # Mount persistent data
11
16
  - ./workflows:/app/workflows
@@ -0,0 +1,135 @@
1
+ # PyNode Project Review Plan — 2026-07-18
2
+
3
+ Follow-up to the July 2026 overhaul (see `plan-pynodeRepoReview.prompt.md`). This plan captures
4
+ the full findings of the 2026-07-18 project review. Phases A–D are being executed now;
5
+ E–H are deferred and can be picked up in any later session.
6
+
7
+ Status legend: `[NOW]` in progress this session · `[LATER]` deferred · `[DECIDE]` needs a user decision.
8
+
9
+ ---
10
+
11
+ ## Phase A [NOW] — Quick bug fixes + compose port
12
+
13
+ 1. **Dead `include_predictions` toggle** — `pynode/nodes/UltralyticsNode/ultralytics_node.py:348`
14
+ reads `if include_predictions or True:` so the "Include Predictions in Output" checkbox does
15
+ nothing. Honor the flag (use `get_config_bool` for string-safety; same for `include_image`).
16
+ 2. **docker-compose port typo** — `docker-compose.yml` publishes `"500:5000"`; DOCKER.md documents
17
+ port 5000. Change to `"5000:5000"`.
18
+ 3. **Silent node-import failures** — `pynode/nodes/__init__.py` `_try_import_python_files` swallows
19
+ every exception with bare `continue`; a custom node with a syntax error vanishes from the palette
20
+ with no log output. Log warnings (missing optional deps may stay at debug level).
21
+ 4. Minor cleanups: dead `system_error_node` variable in `workflow_engine.py:367`; ImageViewerNode
22
+ MJPEG generator loops forever after node stop (holds a waitress thread); ImageViewerNode's shared
23
+ `last_sent_timestamp` makes the SSE broadcaster and the `/frame` polling route steal frames from
24
+ each other; `socket.inet_ntoa` in `mdns_manager.py` throws on IPv6 service records.
25
+
26
+ Acceptance: full test suite passes; toggling "Include Predictions" actually changes output;
27
+ node import errors appear in the log.
28
+
29
+ ## Phase B [NOW] — Model / node storage directories
30
+
31
+ Problem: `YOLO('yolo26n.pt')` downloads weights into the process CWD and `export(format='openvino')`
32
+ writes next to the source; strays currently exist in 4 places (repo root, `models/`,
33
+ `pynode/models/`, `pynode/nodes/`). A pip-installed PyNode launched from inside a venv would scatter
34
+ files there. Also `[tool.setuptools.package-data] nodes/**/*` would bake a stray `.pt` in
35
+ `pynode/nodes/` into a locally built wheel.
36
+
37
+ 1. `pynode/config.py`: add `PYNODE_MODELS_DIR` env var + `resolve_models_dir()` → default
38
+ `<data_dir>/models/`; `--models-dir` CLI flag in `main.py` (mirror the `--data-dir` pattern).
39
+ 2. `BaseNode.get_storage_dir(subdir=None)` → `<data_dir>/node_storage/<NodeType>/`, created lazily.
40
+ General rule going forward: nodes never write binaries outside their storage dir.
41
+ 3. UltralyticsNode: resolve bare model names to an absolute path under the models dir before calling
42
+ `YOLO()` (Ultralytics downloads named assets to an absolute path; the OpenVINO export then lands
43
+ in the same folder). Check legacy locations first to avoid re-downloading; never move/delete
44
+ existing user files automatically (`pynode/models/` holds real user models).
45
+ 4. Packaging: `[tool.setuptools.exclude-package-data]` for `*.pt` / `*_openvino_model` so strays
46
+ can't enter wheels.
47
+ 5. Document (README/INSTALL): env var, CLI flag, where models live per install type.
48
+
49
+ Deferred idea: settings UI for the models folder.
50
+
51
+ ## Phase C [NOW] — mDNS in Docker: fix + docs
52
+
53
+ Root cause: default bridge network; mDNS is link-local multicast (224.0.0.251:5353) which Docker's
54
+ bridge/NAT does not forward. `HOST_IP` only fixes the advertised address, not reachability.
55
+
56
+ 1. DOCKER.md: new "mDNS / service discovery" section — explain the limitation; document `HOST_IP`
57
+ (currently undocumented); give options in order: `network_mode: host` (Linux; ports mapping
58
+ ignored; does NOT help on Docker Desktop Win/Mac), macvlan, host-side avahi reflector
59
+ (`enable-reflector=yes`) or mdns-repeater sidecar.
60
+ 2. docker-compose.yml: commented-out `network_mode: host` alternative block with the caveats.
61
+ 3. Advertised `service_port` must match the externally reachable port (relates to A2).
62
+
63
+ ## Phase D [NOW] — send() copy semantics (biggest perf win)
64
+
65
+ `BaseNode.send()` deep-copies the message per recipient (`base_node.py:254`); raw numpy frames make
66
+ this ~6 MB memcpy per hop per recipient. Adopt Node-RED semantics: **last recipient gets the
67
+ original message, only additional wires get deep copies** — zero frame copies in the common
68
+ single-connection case. Contract change: *senders must not mutate a message after calling send().*
69
+
70
+ 1. Audit every node for post-`send()` mutation and source nodes for numpy buffer reuse; fix
71
+ offenders to create fresh messages instead.
72
+ 2. Implement last-recipient-no-copy in both the queued path and the sink "direct processing" path.
73
+ 3. Update/adjust tests that assert sender-side isolation; document the contract in base_node.py.
74
+
75
+ ## Phase E [LATER] — Remaining performance items
76
+
77
+ - Sink "direct processing" runs on the sender's thread (ImageViewer JPEG encode inside the camera
78
+ loop) and skips `drop_while_busy`; route sinks through the normal queue or apply drop checks.
79
+ - Idle worker threads busy-poll every 10–100 ms (`base_node.py:296`); switch to blocking
80
+ `queue.get()` with a `None` sentinel pushed in `on_stop()`.
81
+ - `sort_msg_keys` runs on every send per recipient; sort at display time in DebugNode instead.
82
+ - Waitress `threads=10` (`main.py:103`): each SSE client and MJPEG viewer permanently holds a
83
+ thread → silent API starvation. Make threads a CLI flag, default higher (~24), document.
84
+ - Dockerfile uses `cuda:12.6.0-devel` (~8 GB base); switch to `-runtime` or multi-stage.
85
+
86
+ ## Phase F [LATER] — UI/UX
87
+
88
+ - Canvas node search (Ctrl+F: find by name/type, pan to node).
89
+ - Per-node runtime status text on canvas (Node-RED `status()` equivalent: "model loaded", fps,
90
+ drop count) — SSE plumbing (`sse_handlers`) already exists.
91
+ - Palette manager UI (falls out of Phase G).
92
+ - Light theme (style.css is dark-only; tokenize colors to CSS variables first).
93
+ - Groups/subflows — biggest structural gap vs Node-RED; affects workflow JSON schema, decide
94
+ deliberately before other schema work.
95
+
96
+ ## Phase G [LATER] — Live node install from an approved registry
97
+
98
+ Constraints today: registries built once at import (`node_registry.py:154-156`); node `api_routes`
99
+ registered as real Flask routes at app creation (`api/nodes.py:418`, frozen after first request).
100
+
101
+ 1. **Registry**: curated JSON index in a separate GitHub repo (`pynode-registry`), served raw:
102
+ name, description, pip requirement/git URL, version, author, category, `verified` flag.
103
+ Approval = PR review.
104
+ 2. **External nodes dir**: also scan `<data_dir>/nodes/` (Node-RED `~/.node-red` pattern) —
105
+ cheap, enables "drop a folder in, restart". Later: `[project.entry-points."pynode.nodes"]`.
106
+ 3. **API**: `GET /api/palette/catalog` (cached), `POST /api/palette/install` (pip via
107
+ `sys.executable -m pip`, reuse install_nodes.py pattern), `DELETE` for removal.
108
+ 4. **Activation v1**: install + "restart to activate" banner.
109
+ **v2 (hot reload)**: make cache builders re-runnable, `palette-updated` SSE event, convert
110
+ per-node API routes to one catch-all `/api/nodes/<node_id>/<path:sub>` dispatching through the
111
+ registry dict at request time (removes the Flask route-freeze problem).
112
+ 5. **Security**: only names from the curated index, pinned versions, behind the API key. Never
113
+ accept arbitrary pip specs from the UI.
114
+
115
+ ## Phase H [DECIDE] — Naming
116
+
117
+ - **GitHub repo rename** `olkham/pynode` → `olkham/pynode-flow`: recommended; cheap, GitHub
118
+ redirects, removes PyPI/repo mismatch. Update `[project.urls]`, README.
119
+ - **Import package rename** (`pynode` → `pynode_flow`): argument for = the unrelated PyPI `pynode`
120
+ 0.1.0 namespace-collision trap (see 2026-07-18 incident); argument against = breaks all imports
121
+ and custom nodes; mismatched dist/import names are normal (opencv-python/cv2).
122
+ Recommendation: keep `pynode` import; add a startup guard that detects the conflicting `pynode`
123
+ dist (importlib.metadata) and fails loudly with "uninstall the unrelated pynode package".
124
+ If ever renaming, the pre-1.0 window is the only sane time.
125
+
126
+ ---
127
+
128
+ ## Execution notes (any session picking this up)
129
+
130
+ - User commits phases themselves; do not commit.
131
+ - Test safety (hard rules, see memory `pynode-test-safety-incident`): tests must build apps via
132
+ `create_app` with tmp paths (tests/conftest.py `api_client`); join every thread before fixture
133
+ teardown; after test work verify `git status --porcelain -- workflows/` is empty. A live PyNode
134
+ server may be running against this repo — its own saves to workflows/ are not test leakage.
135
+ - Suite size at time of writing: 306 tests.
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.2'
22
- __version_tuple__ = version_tuple = (0, 2, 2)
21
+ __version__ = version = '0.2.3'
22
+ __version_tuple__ = version_tuple = (0, 2, 3)
23
23
 
24
- __commit_id__ = commit_id = 'g51b4c1354'
24
+ __commit_id__ = commit_id = 'g6a8c7c6b1'
@@ -11,6 +11,11 @@ Environment variables
11
11
  are persisted under ``<data dir>/workflows/`` (``workflow.json`` plus the
12
12
  ``_backups/`` folder). Overridden by the ``--data-dir`` CLI flag. See
13
13
  :func:`resolve_data_dir` for the full precedence.
14
+ - ``PYNODE_MODELS_DIR``: directory for shared model weights (e.g. YOLO
15
+ ``.pt`` files and exported OpenVINO models). Nodes must download/read model
16
+ binaries here instead of scattering them into the process CWD. Defaults to
17
+ ``<data dir>/models``. Overridden by the ``--models-dir`` CLI flag. See
18
+ :func:`resolve_models_dir` for the full precedence.
14
19
  - ``PYNODE_API_KEY``: API key required on all ``/api/`` requests
15
20
  (``X-API-Key`` header or ``api_key`` query parameter). Empty/unset = auth
16
21
  disabled. Overridden by the ``--api-key`` CLI flag.
@@ -23,6 +28,7 @@ import os
23
28
 
24
29
  # Environment variable names (single source of truth).
25
30
  ENV_DATA_DIR = 'PYNODE_DATA_DIR'
31
+ ENV_MODELS_DIR = 'PYNODE_MODELS_DIR'
26
32
  ENV_API_KEY = 'PYNODE_API_KEY'
27
33
  ENV_CORS_ORIGINS = 'PYNODE_CORS_ORIGINS'
28
34
 
@@ -37,6 +43,9 @@ CHECKOUT_DIR = os.path.dirname(PKG_DIR)
37
43
  # Name of the workflows subdirectory inside the data dir.
38
44
  WORKFLOWS_SUBDIR = 'workflows'
39
45
 
46
+ # Name of the models subdirectory inside the data dir (shared model weights).
47
+ MODELS_SUBDIR = 'models'
48
+
40
49
 
41
50
  def _is_source_checkout(base_dir):
42
51
  """True if ``base_dir`` looks like a writable PyNode source checkout.
@@ -91,3 +100,36 @@ def resolve_workflows_dir(cli_data_dir=None, environ=None, checkout_dir=None):
91
100
  resolve_data_dir(cli_data_dir=cli_data_dir, environ=environ,
92
101
  checkout_dir=checkout_dir),
93
102
  WORKFLOWS_SUBDIR)
103
+
104
+
105
+ def resolve_models_dir(cli_models_dir=None, environ=None, checkout_dir=None):
106
+ """Resolve the shared models directory for downloaded model weights.
107
+
108
+ Nodes that download or generate model binaries (YOLO ``.pt`` files,
109
+ exported OpenVINO models, etc.) must place them here rather than in the
110
+ process CWD, so a pip-installed PyNode launched from a read-only or
111
+ arbitrary directory does not scatter files around.
112
+
113
+ Precedence:
114
+
115
+ 1. ``cli_models_dir`` (the ``--models-dir`` CLI flag),
116
+ 2. the ``PYNODE_MODELS_DIR`` environment variable,
117
+ 3. ``<resolve_data_dir(...)>/models`` (the ``models`` subdirectory of the
118
+ resolved data dir — for a source checkout this is ``<repo>/models``,
119
+ for a pip install ``~/.pynode/models``).
120
+
121
+ Like :func:`resolve_data_dir`, the directory is only resolved here, never
122
+ created — creation happens lazily at the point of use.
123
+ """
124
+ environ = os.environ if environ is None else environ
125
+
126
+ if cli_models_dir:
127
+ return os.path.abspath(os.path.expanduser(cli_models_dir))
128
+
129
+ env_dir = environ.get(ENV_MODELS_DIR)
130
+ if env_dir:
131
+ return os.path.abspath(os.path.expanduser(env_dir))
132
+
133
+ return os.path.join(
134
+ resolve_data_dir(environ=environ, checkout_dir=checkout_dir),
135
+ MODELS_SUBDIR)
@@ -57,6 +57,11 @@ def main():
57
57
  'Also settable via the PYNODE_DATA_DIR env var. '
58
58
  'Default: the source checkout root when running '
59
59
  'from a checkout, otherwise ~/.pynode.')
60
+ parser.add_argument('--models-dir', default=None,
61
+ help='Directory for shared model weights (YOLO .pt '
62
+ 'files, exported OpenVINO models, etc.). '
63
+ 'Also settable via the PYNODE_MODELS_DIR env var. '
64
+ 'Default: <data-dir>/models.')
60
65
  args = parser.parse_args()
61
66
 
62
67
  # Configure application-wide logging
@@ -74,6 +79,10 @@ def main():
74
79
  os.environ['PYNODE_CORS_ORIGINS'] = args.cors_origins
75
80
  if args.data_dir is not None:
76
81
  os.environ['PYNODE_DATA_DIR'] = args.data_dir
82
+ # Node code (which has no Flask app context) resolves the models dir from
83
+ # the env var / defaults, so export the CLI flag before the server import.
84
+ if args.models_dir is not None:
85
+ os.environ['PYNODE_MODELS_DIR'] = args.models_dir
77
86
 
78
87
  from pynode.server import app, load_workflow_from_disk
79
88
 
@@ -88,7 +88,8 @@ class ImageViewerNode(BaseNode):
88
88
  super().__init__(node_id, name)
89
89
  self.current_frame = None
90
90
  self.frame_timestamp = 0
91
- self.last_sent_timestamp = 0
91
+ self.last_sent_timestamp = 0 # dedupe state for the polled GET 'frame' route
92
+ self._sse_last_sent = 0 # dedupe state for the SSE 'frame' broadcaster
92
93
 
93
94
  def on_input(self, msg: Dict[str, Any], input_index: int = 0):
94
95
  """
@@ -126,8 +127,10 @@ class ImageViewerNode(BaseNode):
126
127
 
127
128
  def get_current_frame(self):
128
129
  """
129
- Get the current frame for display in the UI.
130
- Only returns frames that haven't been sent yet.
130
+ Get the current frame for display in the UI via the polled GET 'frame' route.
131
+ Only returns frames that haven't been sent yet via this route. Tracks its
132
+ own dedupe state (last_sent_timestamp) so it doesn't steal frames from the
133
+ SSE broadcaster (get_current_frame_sse), which polls independently.
131
134
  """
132
135
  if self.current_frame and self.frame_timestamp > self.last_sent_timestamp:
133
136
  self.last_sent_timestamp = self.frame_timestamp
@@ -135,18 +138,22 @@ class ImageViewerNode(BaseNode):
135
138
  return None
136
139
 
137
140
  def get_current_frame_sse(self):
138
- """SSE handler: return frame wrapped in 'data' key for client compatibility."""
139
- frame = self.get_current_frame()
140
- if frame:
141
- return {'data': frame}
141
+ """SSE handler: return frame wrapped in 'data' key for client compatibility.
142
+ Tracks its own dedupe state (_sse_last_sent) so it doesn't steal frames
143
+ from the polled GET 'frame' route (get_current_frame)."""
144
+ if self.current_frame and self.frame_timestamp > self._sse_last_sent:
145
+ self._sse_last_sent = self.frame_timestamp
146
+ return {'data': self.current_frame}
142
147
  return None
143
148
 
144
149
  def get_mjpeg_stream(self):
145
- """Generator for MJPEG streaming. Yields (content_type, image_bytes) tuples."""
150
+ """Generator for MJPEG streaming. Yields (content_type, image_bytes) tuples.
151
+ Exits once the node is stopped so the stream doesn't keep a server
152
+ thread alive forever."""
146
153
  import time
147
154
  import base64
148
155
  last_timestamp = 0
149
- while True:
156
+ while not self._stop_worker_flag:
150
157
  try:
151
158
  if self.frame_timestamp > last_timestamp and self.current_frame:
152
159
  last_timestamp = self.frame_timestamp
@@ -23,6 +23,7 @@ Design notes
23
23
  inventing a parallel notion of "alive".
24
24
  """
25
25
 
26
+ import copy
26
27
  import threading
27
28
 
28
29
 
@@ -68,8 +69,13 @@ class LinkBus:
68
69
  with self._lock:
69
70
  subs = self._subscribers.get(channel)
70
71
  targets = list(subs) if subs else []
72
+ # Each subscriber's LinkInNode.send() may take the single-recipient fast
73
+ # path (which shares the payload), so when more than one subscriber
74
+ # receives we hand each its own deepcopy - otherwise every receiving
75
+ # flow would alias one payload. A single subscriber shares (cheap).
76
+ isolate = len(targets) > 1
71
77
  for node in targets:
72
- _deliver(node, msg)
78
+ _deliver(node, copy.deepcopy(msg) if isolate else msg)
73
79
 
74
80
  def subscriber_count(self, channel):
75
81
  """Number of registered subscribers on ``channel`` (for tests/introspection)."""
@@ -494,7 +494,15 @@ class OmronCameraNode(BaseNode):
494
494
  # Image conversion
495
495
  # ------------------------------------------------------------------
496
496
  def _stapi_image_to_numpy(self, st, st_image) -> Optional[np.ndarray]:
497
- """Convert a STAPI image to a BGR numpy array."""
497
+ """Convert a STAPI image to a BGR numpy array.
498
+
499
+ The returned array must NOT alias the STAPI acquisition buffer: this is
500
+ called inside the ``retrieve_buffer`` context, and that buffer is
501
+ requeued (and overwritten by future frames) as soon as the context
502
+ exits. ``cv2.cvtColor`` allocates fresh output arrays, but the plain
503
+ ``arr.reshape(...)`` paths return a view over ``raw`` - those are copied
504
+ explicitly so a frame handed to ``send()`` owns its pixels (see the
505
+ source-node buffer-reuse rule in base_node's module docstring)."""
498
506
  raw = st_image.get_image_data()
499
507
  arr = np.frombuffer(raw, dtype=np.uint8)
500
508
  h, w = st_image.height, st_image.width
@@ -521,7 +529,8 @@ class OmronCameraNode(BaseNode):
521
529
  return cv2.cvtColor(bayer, cv2.COLOR_BayerRG2BGR)
522
530
 
523
531
  elif pix_fmt == st.EStPixelFormatNamingConvention.BGR8:
524
- return arr.reshape(h, w, 3)
532
+ # .copy() so we don't alias the STAPI buffer (requeued on ctx exit).
533
+ return arr.reshape(h, w, 3).copy()
525
534
 
526
535
  elif pix_fmt == st.EStPixelFormatNamingConvention.Mono8:
527
536
  gray = arr.reshape(h, w)
@@ -534,7 +543,8 @@ class OmronCameraNode(BaseNode):
534
543
  else:
535
544
  # Blind fallback: try 3-channel first, then 1-channel → gray2bgr
536
545
  try:
537
- return arr.reshape(h, w, 3)
546
+ # .copy() so we don't alias the STAPI buffer (requeued on exit).
547
+ return arr.reshape(h, w, 3).copy()
538
548
  except Exception:
539
549
  try:
540
550
  gray = arr.reshape(h, w)
@@ -4,6 +4,7 @@ Similar to Node-RED's switch node.
4
4
  Supports dynamic rules with multiple outputs.
5
5
  """
6
6
 
7
+ import copy
7
8
  import re
8
9
  from typing import Any, Dict, List
9
10
  from pynode.nodes.base_node import BaseNode, Info, MessageKeys
@@ -248,9 +249,14 @@ class SwitchNode(BaseNode):
248
249
  matched_any = False
249
250
  for idx, rule in enumerate(rules):
250
251
  if self._evaluate_rule(msg_value, rule):
251
- self.send(msg, idx)
252
+ # With check_all, the same msg can match several rules and go to
253
+ # multiple outputs. send()'s single-recipient fast path shares
254
+ # the payload, so the first output may share msg but every later
255
+ # output needs an isolated copy (otherwise the branches alias
256
+ # one payload). The common single-match case still shares.
257
+ self.send(msg if not matched_any else copy.deepcopy(msg), idx)
252
258
  matched_any = True
253
-
259
+
254
260
  # If not checking all rules, stop at first match
255
261
  if not check_all:
256
262
  break
@@ -0,0 +1,11 @@
1
+ # ultralytics_node imports torch at module level. Guard it so lightweight
2
+ # submodules (e.g. model_paths, which has no heavy imports) stay importable in
3
+ # a core-only install without torch/ultralytics. When the vision deps are
4
+ # missing the node is simply unavailable (node auto-discovery already tolerates
5
+ # this via its own ImportError handling).
6
+ try:
7
+ from .ultralytics_node import UltralyticsNode
8
+ __all__ = ['UltralyticsNode']
9
+ except ImportError:
10
+ __all__ = []
11
+