pynode-flow 0.2.0__tar.gz → 0.2.2__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.2/.claude/skills/pynode-workflows/SKILL.md +185 -0
  2. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/.gitignore +3 -0
  3. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/INSTALL.md +12 -2
  4. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/PKG-INFO +18 -4
  5. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/README.md +17 -1
  6. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/docs/DOCKER.md +0 -1
  7. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/examples/README.md +5 -0
  8. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/_version.py +3 -3
  9. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/helpers.py +9 -0
  10. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/nodes.py +22 -0
  11. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/services.py +40 -52
  12. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/workflows.py +54 -16
  13. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CameraNode/camera_node.py +120 -45
  14. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DebugNode/debug_node.py +9 -1
  15. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DelayNode/delay_node.py +75 -37
  16. pynode_flow-0.2.2/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +5 -0
  17. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +2 -2
  18. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +1 -9
  19. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +1 -1
  20. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +15 -52
  21. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/inference_node.py +4 -5
  22. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/requirements.txt +0 -1
  23. pynode_flow-0.2.2/pynode/nodes/LinkNode/__init__.py +13 -0
  24. pynode_flow-0.2.2/pynode/nodes/LinkNode/link_bus.py +104 -0
  25. pynode_flow-0.2.2/pynode/nodes/LinkNode/link_in_node.py +104 -0
  26. pynode_flow-0.2.2/pynode/nodes/LinkNode/link_out_node.py +88 -0
  27. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/mqtt_service.py +94 -30
  28. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RateProbeNode/rate_probe_node.py +37 -29
  29. pynode_flow-0.2.2/pynode/nodes/TemplateNode/requirements.txt +1 -0
  30. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/VideoReaderNode/video_reader_node.py +43 -22
  31. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/messages.py +1 -0
  32. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/server.py +17 -0
  33. pynode_flow-0.2.2/pynode/static/examples/01-hello-world.json +33 -0
  34. pynode_flow-0.2.2/pynode/static/examples/02-function-transform.json +45 -0
  35. pynode_flow-0.2.2/pynode/static/examples/03-switch-routing.json +80 -0
  36. pynode_flow-0.2.2/pynode/static/examples/04-flow-control.json +76 -0
  37. pynode_flow-0.2.2/pynode/static/examples/05-webcam-viewer.json +35 -0
  38. pynode_flow-0.2.2/pynode/static/examples/06-webcam-yolo.json +75 -0
  39. pynode_flow-0.2.2/pynode/static/examples/07-blur-people.json +95 -0
  40. pynode_flow-0.2.2/pynode/static/examples/08-video-yolo-recorder.json +73 -0
  41. pynode_flow-0.2.2/pynode/static/examples/09-mqtt-pub-sub.json +56 -0
  42. pynode_flow-0.2.2/pynode/static/examples/10-person-alert-mqtt.json +134 -0
  43. pynode_flow-0.2.2/pynode/static/examples/README.md +65 -0
  44. pynode_flow-0.2.2/pynode/static/examples/manifest.json +74 -0
  45. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/index.html +80 -0
  46. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/debug.js +280 -58
  47. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/events.js +72 -1
  48. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/main.js +5 -1
  49. pynode_flow-0.2.2/pynode/static/js/mqtt-services.js +346 -0
  50. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/nodes.js +9 -0
  51. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/properties.js +90 -283
  52. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/state.js +13 -4
  53. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/workflow.js +121 -60
  54. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/workflows.js +192 -89
  55. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/style.css +227 -32
  56. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/workflow_engine.py +7 -0
  57. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/workflow_manager.py +4 -0
  58. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/PKG-INFO +18 -4
  59. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/SOURCES.txt +24 -4
  60. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/requires.txt +0 -2
  61. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/scm_file_list.json +234 -214
  62. pynode_flow-0.2.2/pynode_flow.egg-info/scm_version.json +8 -0
  63. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pyproject.toml +2 -4
  64. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_api_nodes.py +49 -0
  65. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_api_workflows.py +83 -0
  66. pynode_flow-0.2.2/tests/test_camera_node_backend.py +119 -0
  67. pynode_flow-0.2.2/tests/test_debug_workflow_scope.py +122 -0
  68. pynode_flow-0.2.2/tests/test_delay_rate_limit.py +296 -0
  69. pynode_flow-0.2.2/tests/test_link_nodes.py +208 -0
  70. pynode_flow-0.2.2/tests/test_mqtt_services.py +158 -0
  71. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_video_reader_node.py +62 -6
  72. pynode_flow-0.2.0/pynode/nodes/GetiUploadNode/__init__.py +0 -3
  73. pynode_flow-0.2.0/pynode/nodes/GetiUploadNode/geti_upload_node.py +0 -278
  74. pynode_flow-0.2.0/pynode/nodes/GetiUploadNode/requirements.txt +0 -1
  75. pynode_flow-0.2.0/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -15
  76. pynode_flow-0.2.0/pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +0 -309
  77. pynode_flow-0.2.0/pynode_flow.egg-info/scm_version.json +0 -8
  78. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/.dockerignore +0 -0
  79. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/.github/workflows/ci.yml +0 -0
  80. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/.github/workflows/publish.yaml +0 -0
  81. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/DOCKER.md +0 -0
  82. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/Dockerfile +0 -0
  83. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/Dockerfile.cpu +0 -0
  84. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/LICENSE +0 -0
  85. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/MANIFEST.in +0 -0
  86. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/docker-compose.yml +0 -0
  87. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/docs/CREATING_NODES.md +0 -0
  88. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/docs/CUSTOM_NODES.md +0 -0
  89. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/docs/EXTENSIBILITY.md +0 -0
  90. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/docs/UI_COMPONENTS.md +0 -0
  91. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/examples/camera_workflow.py +0 -0
  92. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/examples/camera_yolo_workflow.py +0 -0
  93. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/install_nodes.bat +0 -0
  94. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/install_nodes.sh +0 -0
  95. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/plan-pynodeRepoReview.prompt.md +0 -0
  96. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/__init__.py +0 -0
  97. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/__main__.py +0 -0
  98. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/_perf.py +0 -0
  99. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/__init__.py +0 -0
  100. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/sse.py +0 -0
  101. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/api/uploads.py +0 -0
  102. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/config.py +0 -0
  103. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/install_nodes.py +0 -0
  104. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/main.py +0 -0
  105. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/node_registry.py +0 -0
  106. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
  107. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
  108. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
  109. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
  110. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/BatchNode/__init__.py +0 -0
  111. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/BatchNode/batch_node.py +0 -0
  112. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/BatchNode/requirements.txt +0 -0
  113. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CameraNode/__init__.py +0 -0
  114. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CameraNode/requirements.txt +0 -0
  115. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ChangeNode/__init__.py +0 -0
  116. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ChangeNode/change_node.py +0 -0
  117. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ChangeNode/requirements.txt +0 -0
  118. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
  119. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
  120. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
  121. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CounterNode/__init__.py +0 -0
  122. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CounterNode/counter_node.py +0 -0
  123. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CropNode/__init__.py +0 -0
  124. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CropNode/crop_node.py +0 -0
  125. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/CropNode/requirements.txt +0 -0
  126. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DebugNode/__init__.py +0 -0
  127. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DebugNode/requirements.txt +0 -0
  128. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DelayNode/requirements.txt +0 -0
  129. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
  130. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
  131. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
  132. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ErrorNode/error_node.py +0 -0
  133. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ErrorNode/requirements.txt +0 -0
  134. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FilterNode/__init__.py +0 -0
  135. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FilterNode/filter_node.py +0 -0
  136. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FilterNode/requirements.txt +0 -0
  137. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
  138. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
  139. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
  140. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FunctionNode/__init__.py +0 -0
  141. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FunctionNode/function_node.py +0 -0
  142. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/FunctionNode/requirements.txt +0 -0
  143. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/GateNode/gate_node.py +0 -0
  144. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/GateNode/requirements.txt +0 -0
  145. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
  146. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
  147. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
  148. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
  149. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
  150. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
  151. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
  152. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
  153. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
  154. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
  155. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
  156. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
  157. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
  158. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
  159. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
  160. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InjectNode/inject_node.py +0 -0
  161. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/InjectNode/requirements.txt +0 -0
  162. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/JoinNode/__init__.py +0 -0
  163. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/JoinNode/join_node.py +0 -0
  164. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/JoinNode/requirements.txt +0 -0
  165. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
  166. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
  167. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
  168. {pynode_flow-0.2.0/pynode/nodes/RangeNode → pynode_flow-0.2.2/pynode/nodes/LinkNode}/requirements.txt +0 -0
  169. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/__init__.py +0 -0
  170. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
  171. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
  172. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/requirements.txt +0 -0
  173. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
  174. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
  175. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
  176. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
  177. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
  178. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
  179. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/__init__.py +0 -0
  180. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
  181. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
  182. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/blend_node.py +0 -0
  183. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
  184. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/blur_node.py +0 -0
  185. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/color_space_node.py +0 -0
  186. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/colormap_node.py +0 -0
  187. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/contours_node.py +0 -0
  188. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
  189. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/draw_node.py +0 -0
  190. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
  191. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
  192. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/fft_node.py +0 -0
  193. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
  194. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
  195. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/histogram_node.py +0 -0
  196. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
  197. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
  198. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/in_range_node.py +0 -0
  199. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/morphology_node.py +0 -0
  200. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
  201. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/paste_node.py +0 -0
  202. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/perspective_node.py +0 -0
  203. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
  204. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/requirements.txt +0 -0
  205. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/resize_node.py +0 -0
  206. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/rotate_node.py +0 -0
  207. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/template_match_node.py +0 -0
  208. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/threshold_node.py +0 -0
  209. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
  210. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
  211. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
  212. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
  213. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
  214. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
  215. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
  216. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
  217. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
  218. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
  219. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
  220. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
  221. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
  222. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RangeNode/__init__.py +0 -0
  223. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RangeNode/range_node.py +0 -0
  224. {pynode_flow-0.2.0/pynode/nodes/RateProbeNode → pynode_flow-0.2.2/pynode/nodes/RangeNode}/requirements.txt +0 -0
  225. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RateProbeNode/__init__.py +0 -0
  226. {pynode_flow-0.2.0/pynode/nodes/SplitNode → pynode_flow-0.2.2/pynode/nodes/RateProbeNode}/requirements.txt +0 -0
  227. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
  228. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
  229. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
  230. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/README.md +0 -0
  231. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/__init__.py +0 -0
  232. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
  233. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/requirements.txt +0 -0
  234. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
  235. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
  236. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SplitNode/__init__.py +0 -0
  237. {pynode_flow-0.2.0/pynode/nodes/SwitchNode → pynode_flow-0.2.2/pynode/nodes/SplitNode}/requirements.txt +0 -0
  238. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SplitNode/split_node.py +0 -0
  239. {pynode_flow-0.2.0/pynode/nodes/TemplateNode → pynode_flow-0.2.2/pynode/nodes/SwitchNode}/requirements.txt +0 -0
  240. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SwitchNode/switch_node.py +0 -0
  241. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SyncNode/__init__.py +0 -0
  242. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/SyncNode/sync_node.py +0 -0
  243. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/TemplateNode/template_node.py +0 -0
  244. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/TrackerNode/__init__.py +0 -0
  245. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
  246. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
  247. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
  248. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
  249. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/UltralyticsNode/ultralytics_node.py +0 -0
  250. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/UnknownNode/__init__.py +0 -0
  251. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/VLLMNode/__init__.py +0 -0
  252. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
  253. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
  254. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
  255. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
  256. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/WebhookNode/__init__.py +0 -0
  257. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
  258. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/__init__.py +0 -0
  259. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/base_node.py +0 -0
  260. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/image_utils.py +0 -0
  261. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/info.py +0 -0
  262. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/__init__.py +0 -0
  263. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
  264. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
  265. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
  266. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/requirements.txt +0 -0
  267. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/images/favicon4.png +0 -0
  268. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/README.md +0 -0
  269. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/auth.js +0 -0
  270. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/clipboard.js +0 -0
  271. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/config.js +0 -0
  272. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/connections.js +0 -0
  273. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/history.js +0 -0
  274. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/minimap.js +0 -0
  275. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/palette.js +0 -0
  276. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/selection.js +0 -0
  277. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/ui-utils.js +0 -0
  278. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode/static/js/viewport.js +0 -0
  279. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/dependency_links.txt +0 -0
  280. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/entry_points.txt +0 -0
  281. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/pynode_flow.egg-info/top_level.txt +0 -0
  282. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/requirements.txt +0 -0
  283. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/setup.bat +0 -0
  284. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/setup.cfg +0 -0
  285. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/setup.py +0 -0
  286. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/setup.sh +0 -0
  287. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/__init__.py +0 -0
  288. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/conftest.py +0 -0
  289. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_api_auth.py +0 -0
  290. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_api_errors.py +0 -0
  291. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_api_security.py +0 -0
  292. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_base_node_messaging.py +0 -0
  293. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_concurrency.py +0 -0
  294. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_config.py +0 -0
  295. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_counter_sse.py +0 -0
  296. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_device_detection.py +0 -0
  297. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_message_reader_pickle.py +0 -0
  298. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_node_registration.py +0 -0
  299. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_node_types_api.py +0 -0
  300. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_persistence.py +0 -0
  301. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_serialization.py +0 -0
  302. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_workflow_engine.py +0 -0
  303. {pynode_flow-0.2.0 → pynode_flow-0.2.2}/tests/test_workflow_roundtrip.py +0 -0
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: pynode-workflows
3
+ description: Build, edit and validate PyNode workflow JSON files (nodes + connections) that users can import via the GUI or REST API. Use when asked to create example flows, generate a workflow, wire nodes together, or debug a workflow definition.
4
+ ---
5
+
6
+ # Building PyNode Workflows
7
+
8
+ A PyNode workflow is a JSON document describing nodes and the connections
9
+ between them. The GUI's **Import** button (hamburger menu) accepts a flat
10
+ workflow file and loads it into a **new tab** named after the file. The same
11
+ document can be POSTed to `/api/workflow`.
12
+
13
+ ## Workflow file format
14
+
15
+ ```json
16
+ {
17
+ "nodes": [
18
+ {
19
+ "id": "inject_1",
20
+ "type": "InjectNode",
21
+ "name": "Every 2s",
22
+ "config": { "props": [{"property": "payload", "value": "hi", "valueType": "str"}], "repeat": "2" },
23
+ "x": 80, "y": 80
24
+ }
25
+ ],
26
+ "connections": [
27
+ { "source": "inject_1", "target": "debug_1", "sourceOutput": 0, "targetInput": 0 }
28
+ ]
29
+ }
30
+ ```
31
+
32
+ Rules:
33
+ - `type` is the **Python class name** of the node (e.g. `InjectNode`, `MqttOutNode`).
34
+ Unknown types import as disabled placeholder nodes — always validate.
35
+ - `id` is any unique string. Use readable slugs (`camera_1`, `yolo_1`).
36
+ - `enabled` is optional (default `true`). `x`/`y` are canvas pixels; space
37
+ columns ~260px apart (80, 340, 600, 860, 1120), rows ~140px apart.
38
+ - `sourceOutput`/`targetInput` are 0-based port indexes.
39
+ - Config values are lenient: many nodes read numbers from strings
40
+ (`"confidence": "0.25"`) — copy the conventions below exactly.
41
+ - The on-disk `workflows/workflow.json` uses a different multi-workflow
42
+ wrapper (`{version, activeWorkflow, workflows: [...]}`); **never** write that
43
+ file directly — import through the GUI or API instead.
44
+
45
+ ## Message conventions
46
+
47
+ Messages are dicts in Node-RED style: `payload`, `topic`, `_msgid`, plus extras.
48
+ - Images travel at `msg.payload.image` as a numpy BGR array (or JPEG bytes if a
49
+ source has `encode_jpeg: true`). Downstream nodes locate it via their
50
+ `image_path` config, default `payload.image`.
51
+ - Detections travel at `msg.payload.detections`: a list of
52
+ `{"bbox": [x1,y1,x2,y2], "bbox_format": "xyxy", "class_name": str,
53
+ "class_id": int, "confidence": float}`, with `payload.detection_count` and
54
+ `payload.bbox_format` alongside.
55
+ - `MqttOutNode` publishes `msg.payload`; **`msg.topic` overrides the node's
56
+ configured topic** — don't set a `topic` in InjectNode when a downstream
57
+ MQTT-out node should control the topic.
58
+
59
+ ## Node catalog (config keys verified against source)
60
+
61
+ ### Core / logic
62
+ | Type | Ports in→out | Config (defaults) |
63
+ |---|---|---|
64
+ | `InjectNode` | 0→1 | `props`: list of `{property: "payload", value, valueType: str\|num\|bool\|json\|date\|env}` (dot paths OK, e.g. `payload.count`); `topic`: str; `once`: seconds (str, empty=off); `repeat`: seconds (str, empty=off). Manual trigger button built in. |
65
+ | `DebugNode` | 1→0 | `console`: bool; `complete`: `"payload"` (default) or `"msg"` for whole message; `drop_messages`: bool |
66
+ | `FunctionNode` | 1→N | `func`: Python source (body of a function receiving `msg`, `node`, `time`; must `return msg` / a list for multiple outputs / `None` to drop); `outputs`: int (default 1). Full builtins and `import` are available inside the body. |
67
+ | `SwitchNode` | 1→N | `property`: dot path (`payload`, `payload.count`); `checkall`: bool; `rules`: list of `{operator, value, valueType}` — **rule index = output index**. Operators: `eq neq lt lte gt gte between contains matches true false null nnull else`. |
68
+ | `ChangeNode` | 1→1 | `rules`: list of `{type: set\|delete\|change\|move, path: "msg.payload...", value, valueType}` |
69
+ | `GateNode` | 1→1 | `{}` — passes messages only while enabled; toggled live from its card |
70
+ | `LinkOutNode` | 1→0 | `channel`: str — publishes each incoming msg to the named channel on a process-wide bus. Reaches every `LinkInNode` with the same channel, **including in other workflows**. Empty channel = disabled. `drop_messages` defaults `"false"`. |
71
+ | `LinkInNode` | 0→1 | `channel`: str — emits every msg published on the named channel (by any `LinkOutNode`, cross-flow). Deep-copied on delivery (flow isolation). Only receives while its flow is deployed/running and the node is enabled. Empty channel = disabled. |
72
+ | `DelayNode` | 1→1 | `mode`: `delay`\|`delay_count`\|`rate`; `timeout`: secs (delay mode); `rate` + `rate_time`: msgs per secs; `rate_drop`: `drop`\|`queue` |
73
+ | `CounterNode` | 1→1 | `initial_value`: "0"; `increment`: "1"; `retain_payload`: "false" |
74
+ | `RateProbeNode` | 1→1 | `window_size`: seconds (float) — passes msgs through, shows msg/s |
75
+
76
+ ### Vision
77
+ | Type | Ports | Config |
78
+ |---|---|---|
79
+ | `CameraNode` | 0→1 | `device_index`: 0; `fps`: 30; `width`: 640; `height`: 480; `encode_jpeg`: false; `jpeg_quality`: 75 |
80
+ | `VideoReaderNode` | 0→1 | `source`: path (upload via node properties in GUI); `fps`: 0 (=native); `loop`: bool; `jpeg_quality`: 80 |
81
+ | `UltralyticsNode` | 1→1 | `model`: "yolo11n.pt" (auto-downloads); `confidence`: "0.25"; `iou`: "0.45"; `draw_results`: "true"/"false"; `max_det`: "300"; `include_image`: true; `include_predictions`: true; `drop_messages`: "true" (keep on!); `device`: "cpu" (portable) or `cuda:0`/`intel:gpu` |
82
+ | `LabelFilterNode` | 1→2 | `labels`: "person, car" (comma list); `match_mode`: `any`\|`all`; `case_sensitive`: false; `filter_detections`: true (strips non-matching detections); `detections_path`: "payload.detections". **Output 0 = matched, output 1 = unmatched.** |
83
+ | `ConfidenceFilterNode` | 1→2 | `threshold_source`: `manual`\|`msg`; `threshold`: 0.5; `detections_path`: "payload.detections"; `confidence_field`: "confidence". **Output 0 = ≥ threshold, output 1 = < threshold.** |
84
+ | `ImageViewerNode` | 1→0 | `width`: 320; `height`: 240; `image_path`: "payload.image"; `drop_messages`: true |
85
+ | `VideoWriterNode` | 1→0 | `path`: "./output"; `filename`: "video_{counter}"; `codec`: `mp4v`\|`avc1`\|`xvid`...; `framerate`: 30.0; `width`/`height`; `auto_resolution`: bool (match input); `clip_length`: 0 (=unlimited); `naming_mode`: "counter"; `counter_digits`: 4 |
86
+
87
+ Other vision nodes: `DrawPredictionsNode`, `CropNode`, `TrackerNode`,
88
+ `ImageFormatNode`, `SliceNode`, `BBoxMetricsNode`, `PolygonMetricsNode`,
89
+ `InferenceNode` (multi-backend), `FrameSourceNode` (multi-source camera lib) —
90
+ read their `DEFAULT_CONFIG`/`properties` in `pynode/nodes/<Name>/` before use.
91
+
92
+ ### Network
93
+ | Type | Ports | Config |
94
+ |---|---|---|
95
+ | `MqttInNode` | 0→1 | `serviceId`: broker id from mqtt_services.json; `topic`: subscribe topic (wildcards `#`/`+` OK); `qos`: "0" |
96
+ | `MqttOutNode` | 1→0 | `serviceId`; `topic`: publish topic; `qos`: "0"; `retain`: "false" |
97
+
98
+ MQTT brokers are **services** stored in `mqtt_services.json` next to the
99
+ workflow data (managed in the GUI via the node's Broker dropdown → add). A
100
+ workflow referencing an unknown `serviceId` imports fine but the node reports
101
+ "No MQTT broker configured" until the user picks a broker.
102
+
103
+ Also available: `WebhookNode`, `RESTEndpointNode`, `mDNSNode`.
104
+
105
+ ## FunctionNode authoring rules
106
+
107
+ The `func` string is the **body** of `def user_function(msg, node, time)` —
108
+ top-level `return` is required to emit. `msg` is a deep copy. Imports work
109
+ inside the body (`import cv2`, `import numpy as np`, `import random`).
110
+ Return a list to fan out across `outputs`; `None` drops the message.
111
+
112
+ Example — blur every detection bbox in-place:
113
+
114
+ ```python
115
+ import cv2
116
+ import numpy as np
117
+ p = msg.get('payload') or {}
118
+ img = p.get('image')
119
+ dets = p.get('detections') or []
120
+ if not isinstance(img, np.ndarray):
121
+ return msg
122
+ h, w = img.shape[:2]
123
+ for d in dets:
124
+ bbox = d.get('bbox') or []
125
+ if len(bbox) != 4:
126
+ continue
127
+ x1, y1, x2, y2 = [int(round(v)) for v in bbox]
128
+ x1 = max(0, min(x1, w - 1)); x2 = max(x1 + 1, min(x2, w))
129
+ y1 = max(0, min(y1, h - 1)); y2 = max(y1 + 1, min(y2, h))
130
+ k = max(9, ((x2 - x1) // 4)) | 1
131
+ p['image'][y1:y2, x1:x2] = cv2.GaussianBlur(img[y1:y2, x1:x2], (k, k), 0)
132
+ return msg
133
+ ```
134
+
135
+ When embedding code in workflow JSON, join lines with `\n` and keep
136
+ indentation exact.
137
+
138
+ ## Loading a workflow
139
+
140
+ - **GUI (import)**: hamburger menu → *Import* → choose the `.json` file → it
141
+ opens as a new tab → press *Deploy* to activate.
142
+ - **GUI (bundled examples)**: hamburger menu → *Examples* → pick one. These are
143
+ the files in `pynode/static/examples/`, served statically and listed via
144
+ `pynode/static/examples/manifest.json`. **When adding a new bundled example,
145
+ drop the `.json` in that folder and add an entry to `manifest.json`** (id,
146
+ file, title, description, requires) or it won't appear in the menu.
147
+ - **API**: `POST /api/workflow` with the JSON body imports into the active
148
+ workflow (add `?workflow=<id>` to target another; `X-API-Key` header if the
149
+ server has an API key). `GET /api/workflow` exports the current one —
150
+ building a flow in the GUI and exporting it is the best way to learn configs.
151
+
152
+ ## Validating a workflow file (do this for every file you produce)
153
+
154
+ Run a sandboxed import — never against the real server or the repo's
155
+ `workflows/` dir (a leaked test once destroyed real data; see
156
+ tests/conftest.py):
157
+
158
+ ```python
159
+ import json, tempfile, os
160
+ from pynode.server import create_app # use a venv with the vision extras
161
+
162
+ td = tempfile.mkdtemp()
163
+ app = create_app({'WORKFLOWS_DIR': os.path.join(td, 'wf'),
164
+ 'WORKFLOW_FILE': os.path.join(td, 'wf', 'workflow.json'),
165
+ 'UPLOAD_BASE_DIR': os.path.join(td, 'up'), 'TESTING': True})
166
+ client = app.test_client()
167
+ data = json.load(open('pynode/static/examples/01-hello-world.json'))
168
+ r = client.post('/api/workflow', json=data)
169
+ assert r.status_code == 201, r.get_json()
170
+ manager = app.extensions['workflow_manager']
171
+ with manager.state_lock:
172
+ eng = next(iter(manager.working_engines.values()))
173
+ unknown = [n.type for n in eng.nodes.values() if getattr(n, '_is_unknown_node', False)]
174
+ assert not unknown, f"unknown node types: {unknown}"
175
+ manager.shutdown()
176
+ ```
177
+
178
+ Checklist:
179
+ - every connection's `source`/`target` id exists; port indexes < the node's
180
+ `input_count`/`output_count`
181
+ - node `type` strings match class names exactly (case-sensitive)
182
+ - vision chains keep `drop_messages` on for slow consumers (YOLO, viewers)
183
+ - prefer `device: "cpu"` and auto-downloading models (`yolo11n.pt`) so flows
184
+ run anywhere; note anything machine-specific (camera index, broker IP) in an
185
+ accompanying README
@@ -25,6 +25,7 @@ share/python-wheels/
25
25
  .installed.cfg
26
26
  *.egg
27
27
  PIPFILE.lock
28
+ *.exe
28
29
 
29
30
  # PyInstaller
30
31
  *.manifest
@@ -149,6 +150,8 @@ Thumbs.db
149
150
 
150
151
  # Project specific - ignore all JSON files (workflows and backups)
151
152
  *.json
153
+ # ...but keep the bundled, GUI-loadable example workflows
154
+ !pynode/static/examples/*.json
152
155
 
153
156
  # PyTorch model files
154
157
  *.pt
@@ -16,6 +16,16 @@ pip install "pynode-flow[full]"
16
16
  Nodes whose optional dependencies are missing are simply skipped at startup, so
17
17
  a core-only install still runs fine.
18
18
 
19
+ > **Warning — conflicting `pynode` package:** an unrelated project on PyPI is
20
+ > named `pynode`, and it also installs a top-level `pynode` package. If it is
21
+ > present in the same environment the two clobber each other (symptoms include
22
+ > `ImportError: cannot import name '__version__' from 'pynode' (unknown
23
+ > location)`). Remove it before or after installing:
24
+ >
25
+ > ```bash
26
+ > pip uninstall pynode
27
+ > ```
28
+
19
29
  ### Optional extras
20
30
 
21
31
  Install only the node groups you need:
@@ -25,9 +35,9 @@ Install only the node groups you need:
25
35
  | `vision` | ultralytics, torch, torchvision, supervision | UltralyticsNode, TrackerNode, DrawPredictionsNode, … |
26
36
  | `mqtt` | paho-mqtt | MQTTNode |
27
37
  | `camera` | framesource[full] | FrameSourceNode |
28
- | `inference` | onnxruntime, geti-sdk (+ `vision`) | InferenceNode |
38
+ | `inference` | onnxruntime (+ `vision`) | InferenceNode |
29
39
  | `vlm` | transformers, qwen-vl-utils, Pillow (+ `vision`) | Qwen3VLMNode |
30
- | `upload` | roboflow, geti-sdk | RoboflowUploadNode, GetiUploadNode |
40
+ | `upload` | roboflow | RoboflowUploadNode |
31
41
  | `discovery` | zeroconf | mDNSNode |
32
42
  | `full` | all of the above | every PyPI-installable node |
33
43
  | `dev` | pytest, type stubs | (development only) |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynode-flow
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python based Node-RED-like visual workflow system
5
5
  Author: olkham
6
6
  License-Expression: MIT
@@ -38,7 +38,6 @@ Requires-Dist: framesource[full]; extra == "camera"
38
38
  Provides-Extra: inference
39
39
  Requires-Dist: pynode-flow[vision]; extra == "inference"
40
40
  Requires-Dist: onnxruntime; extra == "inference"
41
- Requires-Dist: geti-sdk>=2.0.0; extra == "inference"
42
41
  Provides-Extra: vlm
43
42
  Requires-Dist: pynode-flow[vision]; extra == "vlm"
44
43
  Requires-Dist: transformers>=4.50.0; extra == "vlm"
@@ -46,7 +45,6 @@ Requires-Dist: qwen-vl-utils>=0.0.8; extra == "vlm"
46
45
  Requires-Dist: Pillow>=9.0.0; extra == "vlm"
47
46
  Provides-Extra: upload
48
47
  Requires-Dist: roboflow>=1.0.0; extra == "upload"
49
- Requires-Dist: geti-sdk>=2.0.0; extra == "upload"
50
48
  Provides-Extra: discovery
51
49
  Requires-Dist: zeroconf>=0.38.0; extra == "discovery"
52
50
  Provides-Extra: full
@@ -86,6 +84,7 @@ https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2
86
84
  - **SwitchNode**: Route messages based on conditions
87
85
  - **DelayNode**: Delay message delivery
88
86
  - **GateNode**: Control message flow with real-time toggle
87
+ - **LinkNode**: Pass messages between flows via named channels (Link In / Link Out)
89
88
  - **RateProbeNode**: Monitor message throughput
90
89
  - **QueueLengthProbeNode**: Monitor queue lengths
91
90
  - **CounterNode**: Count messages
@@ -114,6 +113,12 @@ pip install "pynode-flow[full]"
114
113
  pynode
115
114
  ```
116
115
 
116
+ or specify a port:
117
+
118
+ ```bash
119
+ pynode --port 500
120
+ ```
121
+
117
122
  See [INSTALL.md](INSTALL.md) for the full list of extras (`vision`, `mqtt`,
118
123
  `camera`, `inference`, `vlm`, `upload`, `discovery`, `full`) and how to install
119
124
  per-node dependencies with `pynode-install-nodes`.
@@ -189,6 +194,15 @@ python -m pynode
189
194
 
190
195
  Navigate to `http://localhost:5000`
191
196
 
197
+ ### Load an Example Workflow
198
+
199
+ Ten ready-made learning workflows ship with PyNode — from a two-node hello
200
+ world up to a camera → YOLO → filter → MQTT alert pipeline (including a "blur
201
+ every detected person" privacy flow). Load any of them straight from the
202
+ editor: **☰ menu → Examples → pick one**, then press **Deploy**. See
203
+ [pynode/static/examples/README.md](pynode/static/examples/README.md) for the
204
+ guided tour and per-example prerequisites.
205
+
192
206
  ### Data Directory
193
207
 
194
208
  PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus timestamped backups in `_backups/`). The data directory is resolved in this order:
@@ -499,7 +513,7 @@ Nodes can:
499
513
 
500
514
  ### Example Flow Documentation Needed
501
515
  - ⬜ Bird seed level monitor
502
- - ⬜ Capture data and send to Roboflow / Geti
516
+ - ⬜ Capture data and send to Roboflow
503
517
  - ⬜ Track objects time in zone
504
518
  - ⬜ Live VLMs
505
519
  - ⬜ ANPR (Detect, Crop, OCR, MQTT)
@@ -19,6 +19,7 @@ https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2
19
19
  - **SwitchNode**: Route messages based on conditions
20
20
  - **DelayNode**: Delay message delivery
21
21
  - **GateNode**: Control message flow with real-time toggle
22
+ - **LinkNode**: Pass messages between flows via named channels (Link In / Link Out)
22
23
  - **RateProbeNode**: Monitor message throughput
23
24
  - **QueueLengthProbeNode**: Monitor queue lengths
24
25
  - **CounterNode**: Count messages
@@ -47,6 +48,12 @@ pip install "pynode-flow[full]"
47
48
  pynode
48
49
  ```
49
50
 
51
+ or specify a port:
52
+
53
+ ```bash
54
+ pynode --port 500
55
+ ```
56
+
50
57
  See [INSTALL.md](INSTALL.md) for the full list of extras (`vision`, `mqtt`,
51
58
  `camera`, `inference`, `vlm`, `upload`, `discovery`, `full`) and how to install
52
59
  per-node dependencies with `pynode-install-nodes`.
@@ -122,6 +129,15 @@ python -m pynode
122
129
 
123
130
  Navigate to `http://localhost:5000`
124
131
 
132
+ ### Load an Example Workflow
133
+
134
+ Ten ready-made learning workflows ship with PyNode — from a two-node hello
135
+ world up to a camera → YOLO → filter → MQTT alert pipeline (including a "blur
136
+ every detected person" privacy flow). Load any of them straight from the
137
+ editor: **☰ menu → Examples → pick one**, then press **Deploy**. See
138
+ [pynode/static/examples/README.md](pynode/static/examples/README.md) for the
139
+ guided tour and per-example prerequisites.
140
+
125
141
  ### Data Directory
126
142
 
127
143
  PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus timestamped backups in `_backups/`). The data directory is resolved in this order:
@@ -432,7 +448,7 @@ Nodes can:
432
448
 
433
449
  ### Example Flow Documentation Needed
434
450
  - ⬜ Bird seed level monitor
435
- - ⬜ Capture data and send to Roboflow / Geti
451
+ - ⬜ Capture data and send to Roboflow
436
452
  - ⬜ Track objects time in zone
437
453
  - ⬜ Live VLMs
438
454
  - ⬜ ANPR (Detect, Crop, OCR, MQTT)
@@ -210,7 +210,6 @@ The Docker image includes:
210
210
  - Flask web framework
211
211
  - OpenCV for image processing
212
212
  - Ultralytics YOLO
213
- - Geti SDK
214
213
  - ONNX Runtime
215
214
  - And all node-specific requirements
216
215
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  This directory contains example Python scripts that demonstrate how to use PyNode workflows programmatically without the UI.
4
4
 
5
+ > Looking for **GUI-loadable** examples instead? PyNode ships ten example
6
+ > workflows you can load from the editor's **☰ menu → Examples**. Their source
7
+ > and a guided tour live in
8
+ > [pynode/static/examples/](../pynode/static/examples/README.md).
9
+
5
10
  ## Examples
6
11
 
7
12
  ### 1. `camera_workflow.py` - Simple Camera Display
@@ -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.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
21
+ __version__ = version = '0.2.2'
22
+ __version_tuple__ = version_tuple = (0, 2, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g92a665661'
24
+ __commit_id__ = commit_id = 'g51b4c1354'
@@ -16,6 +16,15 @@ def _get_manager():
16
16
  return current_app.extensions['workflow_manager']
17
17
 
18
18
 
19
+ def _get_mqtt_manager():
20
+ """Return the MQTTServiceManager of the app handling the current request.
21
+
22
+ Each app owns its own manager (isolated services file), so the sandboxed
23
+ test app never touches the real mqtt_services.json.
24
+ """
25
+ return current_app.extensions['mqtt_manager']
26
+
27
+
19
28
  def _json_error(message, status):
20
29
  """JSON error envelope: {'success': False, 'error': str}."""
21
30
  return jsonify({'success': False, 'error': message}), status
@@ -29,6 +29,28 @@ def get_node_types():
29
29
  return jsonify(node_registry.get_node_types())
30
30
 
31
31
 
32
+ @nodes_bp.route('/api/link-channels', methods=['GET'])
33
+ def get_link_channels():
34
+ """List distinct, non-empty channel names already used by Link In/Out
35
+ nodes across every workflow's working engine.
36
+
37
+ Backs the properties panel's channel suggestion list, so users can pick
38
+ an existing channel instead of retyping it. Scans working engines (not
39
+ just deployed) so channels typed into not-yet-deployed nodes show up too.
40
+ """
41
+ manager = _get_manager()
42
+ channels = set()
43
+ with manager.state_lock:
44
+ for engine in manager.working_engines.values():
45
+ for node in engine.nodes.values():
46
+ if node.type not in ('LinkInNode', 'LinkOutNode'):
47
+ continue
48
+ channel = str(node.config.get('channel', '') or '').strip()
49
+ if channel:
50
+ channels.add(channel)
51
+ return jsonify({'success': True, 'channels': sorted(channels)})
52
+
53
+
32
54
  @nodes_bp.route('/api/nodes', methods=['GET'])
33
55
  def get_nodes():
34
56
  """Get all nodes in the working workflow."""
@@ -1,8 +1,14 @@
1
- """MQTT service management API."""
1
+ """MQTT service management API.
2
+
3
+ All handlers resolve the MQTT service manager off the current app
4
+ (``_get_mqtt_manager``) so the sandboxed test app operates on its own
5
+ isolated services file, never the real mqtt_services.json.
6
+ """
2
7
 
3
8
  from flask import Blueprint, jsonify
4
9
 
5
- from pynode.api.helpers import _INVALID_BODY_ERROR, _get_json_body, _json_error
10
+ from pynode.api.helpers import (
11
+ _INVALID_BODY_ERROR, _get_json_body, _get_mqtt_manager, _json_error)
6
12
 
7
13
  services_bp = Blueprint('services', __name__)
8
14
 
@@ -11,8 +17,7 @@ services_bp = Blueprint('services', __name__)
11
17
  def list_mqtt_services():
12
18
  """List all MQTT services."""
13
19
  try:
14
- from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
15
- services = mqtt_manager.list_services()
20
+ services = _get_mqtt_manager().list_services()
16
21
  return jsonify({'success': True, 'services': services})
17
22
  except Exception as e:
18
23
  return jsonify({'success': False, 'error': str(e)}), 500
@@ -22,7 +27,6 @@ def list_mqtt_services():
22
27
  def create_mqtt_service():
23
28
  """Create a new MQTT service."""
24
29
  try:
25
- from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
26
30
  data = _get_json_body()
27
31
  if data is None:
28
32
  return _json_error(_INVALID_BODY_ERROR, 400)
@@ -33,16 +37,32 @@ def create_mqtt_service():
33
37
  if not data.get('broker'):
34
38
  return jsonify({'success': False, 'error': 'Broker address is required'}), 400
35
39
 
36
- service = mqtt_manager.create_service(data)
37
- return jsonify({
38
- 'success': True,
39
- 'service': {
40
- 'id': service.id,
41
- 'name': service.name,
42
- 'broker': service.broker,
43
- 'port': service.port
44
- }
45
- })
40
+ service = _get_mqtt_manager().create_service(data)
41
+ return jsonify({'success': True, 'service': service.to_dict()})
42
+ except Exception as e:
43
+ return jsonify({'success': False, 'error': str(e)}), 500
44
+
45
+
46
+ @services_bp.route('/api/services/mqtt/test', methods=['POST'])
47
+ def test_mqtt_connection():
48
+ """Test an MQTT connection using the SUBMITTED form values.
49
+
50
+ Does not create or modify any saved service and does not disturb live
51
+ connections - it spins up a short-lived throwaway client that is always
52
+ cleaned up. Returns ``{'success': bool, 'error': str|None}`` where
53
+ ``success`` reflects whether the connection succeeded.
54
+ """
55
+ try:
56
+ from pynode.nodes.MQTTNode.mqtt_service import test_connection
57
+
58
+ data = _get_json_body()
59
+ if data is None:
60
+ return _json_error(_INVALID_BODY_ERROR, 400)
61
+ if not data.get('broker'):
62
+ return jsonify({'success': False, 'error': 'Broker address is required'}), 400
63
+
64
+ success, error = test_connection(data)
65
+ return jsonify({'success': success, 'error': error})
46
66
  except Exception as e:
47
67
  return jsonify({'success': False, 'error': str(e)}), 500
48
68
 
@@ -51,15 +71,11 @@ def create_mqtt_service():
51
71
  def get_mqtt_service(service_id):
52
72
  """Get a specific MQTT service by ID."""
53
73
  try:
54
- from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
55
- service = mqtt_manager.get_service(service_id)
74
+ service = _get_mqtt_manager().get_service(service_id)
56
75
  if not service:
57
76
  return jsonify({'success': False, 'error': 'Service not found'}), 404
58
77
 
59
- return jsonify({
60
- 'success': True,
61
- 'service': service.to_dict()
62
- })
78
+ return jsonify({'success': True, 'service': service.to_dict()})
63
79
  except Exception as e:
64
80
  return jsonify({'success': False, 'error': str(e)}), 500
65
81
 
@@ -68,19 +84,15 @@ def get_mqtt_service(service_id):
68
84
  def update_mqtt_service(service_id):
69
85
  """Update an existing MQTT service."""
70
86
  try:
71
- from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
72
87
  data = _get_json_body()
73
88
  if data is None:
74
89
  return _json_error(_INVALID_BODY_ERROR, 400)
75
90
 
76
- service = mqtt_manager.update_service(service_id, data)
91
+ service = _get_mqtt_manager().update_service(service_id, data)
77
92
  if not service:
78
93
  return jsonify({'success': False, 'error': 'Service not found'}), 404
79
94
 
80
- return jsonify({
81
- 'success': True,
82
- 'service': service.to_dict()
83
- })
95
+ return jsonify({'success': True, 'service': service.to_dict()})
84
96
  except Exception as e:
85
97
  return jsonify({'success': False, 'error': str(e)}), 500
86
98
 
@@ -89,9 +101,7 @@ def update_mqtt_service(service_id):
89
101
  def delete_mqtt_service(service_id):
90
102
  """Delete an MQTT service."""
91
103
  try:
92
- from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
93
-
94
- if not mqtt_manager.delete_service(service_id):
104
+ if not _get_mqtt_manager().delete_service(service_id):
95
105
  return jsonify({
96
106
  'success': False,
97
107
  'error': 'Service not found or still in use'
@@ -100,25 +110,3 @@ def delete_mqtt_service(service_id):
100
110
  return jsonify({'success': True})
101
111
  except Exception as e:
102
112
  return jsonify({'success': False, 'error': str(e)}), 500
103
-
104
-
105
- @services_bp.route('/api/services/mqtt/<service_id>/test', methods=['POST'])
106
- def test_mqtt_service(service_id):
107
- """Test connection to an MQTT service."""
108
- try:
109
- from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
110
-
111
- service = mqtt_manager.get_service(service_id)
112
- if not service:
113
- return jsonify({'success': False, 'error': 'Service not found'}), 404
114
-
115
- # Try to connect
116
- connected = service.connect()
117
-
118
- return jsonify({
119
- 'success': True,
120
- 'connected': connected,
121
- 'status': 'connected' if connected else 'failed'
122
- })
123
- except Exception as e:
124
- return jsonify({'success': False, 'error': str(e)}), 500