pynode-flow 0.2.7__tar.gz → 0.2.9__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 (380) hide show
  1. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/.claude/skills/pynode-workflows/SKILL.md +4 -3
  2. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/.dockerignore +3 -0
  3. pynode_flow-0.2.9/.github/workflows/ci.yml +90 -0
  4. pynode_flow-0.2.9/.github/workflows/git-hooks.yml +28 -0
  5. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/.gitignore +3 -0
  6. pynode_flow-0.2.9/.pre-commit-config.yml +9 -0
  7. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/DOCKER.md +3 -2
  8. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/Dockerfile +5 -3
  9. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/Dockerfile.cpu +1 -1
  10. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/INSTALL.md +10 -1
  11. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/PKG-INFO +31 -5
  12. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/README.md +23 -3
  13. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docker-compose.yml +8 -0
  14. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docker-entrypoint.sh +1 -1
  15. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docs/CREATING_NODES.md +41 -0
  16. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docs/EXTENSIBILITY.md +25 -22
  17. pynode_flow-0.2.9/docs/video_thumb_play.png +0 -0
  18. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/examples/camera_workflow.py +2 -4
  19. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/examples/camera_yolo_workflow.py +2 -4
  20. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/_version.py +3 -3
  21. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/__init__.py +4 -1
  22. pynode_flow-0.2.9/pynode/api/credentials.py +104 -0
  23. pynode_flow-0.2.9/pynode/api/health.py +68 -0
  24. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/services.py +16 -5
  25. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/config.py +8 -0
  26. pynode_flow-0.2.9/pynode/credential_store.py +475 -0
  27. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/main.py +113 -17
  28. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/AutoSyncNode/auto_sync_node.py +5 -5
  29. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +2 -2
  30. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/BatchNode/batch_node.py +2 -2
  31. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CameraNode/camera_node.py +3 -4
  32. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ChangeNode/__init__.py +2 -0
  33. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ChangeNode/change_node.py +126 -27
  34. pynode_flow-0.2.9/pynode/nodes/ClassMapNode/__init__.py +3 -0
  35. pynode_flow-0.2.9/pynode/nodes/ClassMapNode/class_map_node.py +329 -0
  36. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +2 -2
  37. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ControlSliderNode/control_slider_node.py +2 -2
  38. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CounterNode/counter_node.py +2 -2
  39. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CropNode/crop_node.py +3 -5
  40. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DebugNode/debug_node.py +2 -2
  41. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DelayNode/delay_node.py +6 -6
  42. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +23 -12
  43. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ErrorNode/error_node.py +3 -3
  44. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FilterNode/filter_node.py +2 -2
  45. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FrameSourceNode/frame_source_node.py +2 -2
  46. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FunctionNode/function_node.py +3 -3
  47. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/GateNode/gate_node.py +3 -3
  48. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageFormatNode/image_format_node.py +4 -4
  49. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageUploadNode/image_upload_node.py +1 -2
  50. pynode_flow-0.2.9/pynode/nodes/ImageViewerNode/image_viewer_node.py +271 -0
  51. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageWriterNode/imagewriter_node.py +3 -4
  52. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +5 -0
  53. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +13 -14
  54. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +4 -4
  55. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +1 -1
  56. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +32 -12
  57. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +7 -7
  58. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +5 -4
  59. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +2 -2
  60. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/inference_node.py +24 -14
  61. pynode_flow-0.2.9/pynode/nodes/InferenceNode/requirements.txt +16 -0
  62. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/JoinNode/join_node.py +2 -2
  63. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/LabelFilterNode/label_filter_node.py +3 -3
  64. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/LinkNode/link_in_node.py +4 -4
  65. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/LinkNode/link_out_node.py +2 -2
  66. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MQTTNode/mqtt_in_node.py +3 -4
  67. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MQTTNode/mqtt_out_node.py +3 -3
  68. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MQTTNode/mqtt_service.py +247 -91
  69. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MessageWriterNode/messagereader_node.py +11 -12
  70. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MessageWriterNode/messagewriter_node.py +5 -5
  71. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OmronCameraNode/omron_camera_node.py +5 -6
  72. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/arithmetic_node.py +8 -8
  73. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/bitwise_node.py +2 -3
  74. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/blend_node.py +2 -3
  75. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/blob_detector_node.py +2 -2
  76. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/blur_node.py +2 -2
  77. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/color_space_node.py +2 -2
  78. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/colormap_node.py +2 -2
  79. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/contours_node.py +2 -2
  80. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/denormalize_coords_node.py +5 -5
  81. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/draw_node.py +2 -2
  82. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/edge_detector_node.py +2 -2
  83. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/equirectangular_node.py +4 -4
  84. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/fft_node.py +2 -2
  85. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/focus_estimation_node.py +2 -2
  86. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/frequency_filter_node.py +2 -2
  87. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/histogram_node.py +2 -2
  88. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/hough_circles_node.py +2 -2
  89. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/hough_lines_node.py +2 -2
  90. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/in_range_node.py +2 -2
  91. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/morphology_node.py +2 -2
  92. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/normalize_coords_node.py +5 -5
  93. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/paste_node.py +7 -7
  94. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/perspective_node.py +2 -2
  95. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/realsense_depth_node.py +2 -2
  96. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/resize_node.py +2 -2
  97. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/rotate_node.py +2 -2
  98. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/template_match_node.py +2 -2
  99. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/threshold_node.py +2 -2
  100. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +2 -2
  101. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +2 -2
  102. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +3 -3
  103. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +35 -18
  104. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +84 -38
  105. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RangeNode/range_node.py +2 -2
  106. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RateProbeNode/rate_probe_node.py +6 -2
  107. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +42 -14
  108. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SliceNode/merge_predictions_node.py +13 -14
  109. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SliceNode/slice_collector_node.py +16 -16
  110. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SliceNode/slice_image_node.py +9 -9
  111. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/ndjson_protocol.py +4 -4
  112. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/nodered_snippets.py +5 -5
  113. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/tcp_in_node.py +3 -4
  114. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/tcp_out_node.py +2 -2
  115. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/udp_in_node.py +4 -4
  116. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/udp_out_node.py +2 -2
  117. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/udp_protocol.py +15 -15
  118. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SplitNode/split_node.py +3 -3
  119. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/__init__.py +5 -0
  120. pynode_flow-0.2.9/pynode/nodes/Supervision/reid_gallery.py +227 -0
  121. pynode_flow-0.2.9/pynode/nodes/Supervision/requirements.txt +6 -0
  122. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/supervision_annotate_node.py +17 -8
  123. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/supervision_filter_node.py +3 -3
  124. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/supervision_line_counter_node.py +13 -5
  125. pynode_flow-0.2.9/pynode/nodes/Supervision/supervision_reid_node.py +578 -0
  126. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/supervision_sink_node.py +2 -3
  127. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/supervision_smoother_node.py +2 -0
  128. pynode_flow-0.2.9/pynode/nodes/Supervision/supervision_tracker_node.py +487 -0
  129. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Supervision/supervision_zone_node.py +11 -8
  130. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SwitchNode/switch_node.py +4 -4
  131. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SyncNode/sync_node.py +7 -7
  132. pynode_flow-0.2.9/pynode/nodes/TemplateNode/requirements.txt +1 -0
  133. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/TemplateNode/template_node.py +2 -2
  134. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/UltralyticsNode/ultralytics_node.py +4 -4
  135. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/VLLMNode/vllm_node.py +9 -9
  136. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/VideoReaderNode/video_reader_node.py +8 -8
  137. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/WebhookNode/webhook_node.py +94 -22
  138. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/base_node.py +217 -67
  139. pynode_flow-0.2.9/pynode/nodes/detection_utils.py +208 -0
  140. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/image_utils.py +9 -8
  141. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/info.py +3 -4
  142. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -1
  143. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/mDNSNode/mdns_manager.py +3 -4
  144. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/messages.py +2 -2
  145. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/pacing.py +3 -3
  146. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/supervision_utils.py +142 -25
  147. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/server.py +98 -13
  148. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/18-track-trace.json +2 -1
  149. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/19-line-counter.json +2 -1
  150. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/20-zone-occupancy.json +2 -1
  151. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/22-heatmap.json +2 -1
  152. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/24-detection-log.json +2 -1
  153. pynode_flow-0.2.9/pynode/static/examples/25-reid-identities.json +134 -0
  154. pynode_flow-0.2.9/pynode/static/examples/26-reid-cross-camera.json +240 -0
  155. pynode_flow-0.2.9/pynode/static/examples/27-tracker-comparison.json +142 -0
  156. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/README.md +16 -0
  157. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/manifest.json +21 -0
  158. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/index.html +49 -1
  159. pynode_flow-0.2.9/pynode/static/js/credentials.js +267 -0
  160. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/mqtt-services.js +55 -5
  161. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/properties.js +64 -3
  162. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/style.css +31 -0
  163. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/workflow_engine.py +18 -14
  164. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/workflow_manager.py +58 -3
  165. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/PKG-INFO +31 -5
  166. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/SOURCES.txt +29 -0
  167. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/requires.txt +8 -1
  168. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/scm_file_list.json +29 -0
  169. pynode_flow-0.2.9/pynode_flow.egg-info/scm_version.json +8 -0
  170. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pyproject.toml +14 -2
  171. pynode_flow-0.2.9/ruff.toml +9 -0
  172. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/conftest.py +6 -1
  173. pynode_flow-0.2.9/tests/test_api_credentials.py +180 -0
  174. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_api_security.py +0 -1
  175. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_base_node_messaging.py +72 -5
  176. pynode_flow-0.2.9/tests/test_change_node.py +256 -0
  177. pynode_flow-0.2.9/tests/test_class_map_node.py +250 -0
  178. pynode_flow-0.2.9/tests/test_credential_store.py +308 -0
  179. pynode_flow-0.2.9/tests/test_credential_wiring.py +167 -0
  180. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_debug_workflow_scope.py +0 -1
  181. pynode_flow-0.2.9/tests/test_detection_utils.py +268 -0
  182. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_image_upload_node.py +1 -2
  183. pynode_flow-0.2.9/tests/test_image_viewer_node.py +217 -0
  184. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_link_nodes.py +0 -1
  185. pynode_flow-0.2.9/tests/test_mqtt_credentials.py +237 -0
  186. pynode_flow-0.2.9/tests/test_node_credentials.py +287 -0
  187. pynode_flow-0.2.9/tests/test_production_hardening.py +256 -0
  188. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_send_copy_semantics.py +71 -8
  189. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_socket_nodes.py +6 -6
  190. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_supervision_phase3_nodes.py +0 -1
  191. pynode_flow-0.2.9/tests/test_supervision_reid_node.py +468 -0
  192. pynode_flow-0.2.9/tests/test_supervision_tracker_node.py +385 -0
  193. pynode_flow-0.2.9/tests/test_webhook_credentials.py +186 -0
  194. pynode_flow-0.2.7/.github/workflows/ci.yml +0 -44
  195. pynode_flow-0.2.7/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -169
  196. pynode_flow-0.2.7/pynode/nodes/InferenceNode/requirements.txt +0 -12
  197. pynode_flow-0.2.7/pynode/nodes/Supervision/requirements.txt +0 -3
  198. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_tracker_node.py +0 -229
  199. pynode_flow-0.2.7/pynode_flow.egg-info/scm_version.json +0 -8
  200. pynode_flow-0.2.7/tests/test_supervision_tracker_node.py +0 -226
  201. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/.github/workflows/publish.yaml +0 -0
  202. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/LICENSE +0 -0
  203. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/MANIFEST.in +0 -0
  204. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docs/CUSTOM_NODES.md +0 -0
  205. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docs/DOCKER.md +0 -0
  206. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/docs/UI_COMPONENTS.md +0 -0
  207. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/examples/README.md +0 -0
  208. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/install_nodes.bat +0 -0
  209. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/install_nodes.sh +0 -0
  210. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/__init__.py +0 -0
  211. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/__main__.py +0 -0
  212. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/_perf.py +0 -0
  213. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/helpers.py +0 -0
  214. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/nodes.py +0 -0
  215. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/sse.py +0 -0
  216. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/uploads.py +0 -0
  217. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/api/workflows.py +0 -0
  218. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/install_nodes.py +0 -0
  219. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/node_registry.py +0 -0
  220. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
  221. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
  222. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/BatchNode/__init__.py +0 -0
  223. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/BatchNode/requirements.txt +0 -0
  224. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CameraNode/__init__.py +0 -0
  225. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CameraNode/requirements.txt +0 -0
  226. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ChangeNode/requirements.txt +0 -0
  227. {pynode_flow-0.2.7/pynode/nodes/ConfidenceFilterNode → pynode_flow-0.2.9/pynode/nodes/ClassMapNode}/requirements.txt +0 -0
  228. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
  229. {pynode_flow-0.2.7/pynode/nodes/DelayNode → pynode_flow-0.2.9/pynode/nodes/ConfidenceFilterNode}/requirements.txt +0 -0
  230. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CounterNode/__init__.py +0 -0
  231. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CropNode/__init__.py +0 -0
  232. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/CropNode/requirements.txt +0 -0
  233. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DebugNode/__init__.py +0 -0
  234. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DebugNode/requirements.txt +0 -0
  235. {pynode_flow-0.2.7/pynode/nodes/ErrorNode → pynode_flow-0.2.9/pynode/nodes/DelayNode}/requirements.txt +0 -0
  236. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
  237. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
  238. {pynode_flow-0.2.7/pynode/nodes/FilterNode → pynode_flow-0.2.9/pynode/nodes/ErrorNode}/requirements.txt +0 -0
  239. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FilterNode/__init__.py +0 -0
  240. {pynode_flow-0.2.7/pynode/nodes/FunctionNode → pynode_flow-0.2.9/pynode/nodes/FilterNode}/requirements.txt +0 -0
  241. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
  242. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
  243. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/FunctionNode/__init__.py +0 -0
  244. {pynode_flow-0.2.7/pynode/nodes/GateNode → pynode_flow-0.2.9/pynode/nodes/FunctionNode}/requirements.txt +0 -0
  245. {pynode_flow-0.2.7/pynode/nodes/ImageWriterNode → pynode_flow-0.2.9/pynode/nodes/GateNode}/requirements.txt +0 -0
  246. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
  247. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
  248. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
  249. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
  250. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
  251. {pynode_flow-0.2.7/pynode/nodes/InjectNode → pynode_flow-0.2.9/pynode/nodes/ImageWriterNode}/requirements.txt +0 -0
  252. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
  253. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
  254. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/InjectNode/inject_node.py +0 -0
  255. {pynode_flow-0.2.7/pynode/nodes/JoinNode → pynode_flow-0.2.9/pynode/nodes/InjectNode}/requirements.txt +0 -0
  256. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/JoinNode/__init__.py +0 -0
  257. {pynode_flow-0.2.7/pynode/nodes/LabelFilterNode → pynode_flow-0.2.9/pynode/nodes/JoinNode}/requirements.txt +0 -0
  258. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
  259. {pynode_flow-0.2.7/pynode/nodes/LinkNode → pynode_flow-0.2.9/pynode/nodes/LabelFilterNode}/requirements.txt +0 -0
  260. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/LinkNode/__init__.py +0 -0
  261. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/LinkNode/link_bus.py +0 -0
  262. {pynode_flow-0.2.7/pynode/nodes/RangeNode → pynode_flow-0.2.9/pynode/nodes/LinkNode}/requirements.txt +0 -0
  263. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MQTTNode/__init__.py +0 -0
  264. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MQTTNode/requirements.txt +0 -0
  265. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
  266. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
  267. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
  268. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/__init__.py +0 -0
  269. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/OpenCV/requirements.txt +0 -0
  270. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
  271. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
  272. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
  273. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
  274. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
  275. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
  276. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
  277. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
  278. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RangeNode/__init__.py +0 -0
  279. {pynode_flow-0.2.7/pynode/nodes/RateProbeNode → pynode_flow-0.2.9/pynode/nodes/RangeNode}/requirements.txt +0 -0
  280. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RateProbeNode/__init__.py +0 -0
  281. {pynode_flow-0.2.7/pynode/nodes/SplitNode → pynode_flow-0.2.9/pynode/nodes/RateProbeNode}/requirements.txt +0 -0
  282. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
  283. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
  284. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SliceNode/README.md +0 -0
  285. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SliceNode/__init__.py +0 -0
  286. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SliceNode/requirements.txt +0 -0
  287. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/__init__.py +0 -0
  288. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/interop/README.md +0 -0
  289. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/interop/nodered-example-flow.json +0 -0
  290. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/requirements.txt +0 -0
  291. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SocketNode/udp_probe.py +0 -0
  292. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SplitNode/__init__.py +0 -0
  293. {pynode_flow-0.2.7/pynode/nodes/SwitchNode → pynode_flow-0.2.9/pynode/nodes/SplitNode}/requirements.txt +0 -0
  294. {pynode_flow-0.2.7/pynode/nodes/TemplateNode → pynode_flow-0.2.9/pynode/nodes/SwitchNode}/requirements.txt +0 -0
  295. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/SyncNode/__init__.py +0 -0
  296. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
  297. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/UltralyticsNode/model_paths.py +0 -0
  298. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
  299. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/UnknownNode/__init__.py +0 -0
  300. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/VLLMNode/__init__.py +0 -0
  301. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
  302. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
  303. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
  304. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/WebhookNode/__init__.py +0 -0
  305. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/__init__.py +0 -0
  306. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/mDNSNode/__init__.py +0 -0
  307. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
  308. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/nodes/mDNSNode/requirements.txt +0 -0
  309. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/01-hello-world.json +0 -0
  310. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/02-function-transform.json +0 -0
  311. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/03-switch-routing.json +0 -0
  312. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/04-flow-control.json +0 -0
  313. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/05-webcam-viewer.json +0 -0
  314. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/06-webcam-yolo.json +0 -0
  315. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/07-blur-people.json +0 -0
  316. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
  317. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
  318. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
  319. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/16-socket-bridge.json +0 -0
  320. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/17-slider-crop.json +0 -0
  321. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/21-annotator-gallery.json +0 -0
  322. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/examples/23-privacy-blur.json +0 -0
  323. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/images/favicon4.png +0 -0
  324. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/README.md +0 -0
  325. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/auth.js +0 -0
  326. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/clipboard.js +0 -0
  327. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/config.js +0 -0
  328. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/connections.js +0 -0
  329. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/debug.js +0 -0
  330. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/events.js +0 -0
  331. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/geometry-editor.js +0 -0
  332. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/history.js +0 -0
  333. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/main.js +0 -0
  334. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/minimap.js +0 -0
  335. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/nodes.js +0 -0
  336. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/palette.js +0 -0
  337. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/selection.js +0 -0
  338. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/state.js +0 -0
  339. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/ui-utils.js +0 -0
  340. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/viewport.js +0 -0
  341. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/workflow.js +0 -0
  342. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode/static/js/workflows.js +0 -0
  343. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/dependency_links.txt +0 -0
  344. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/entry_points.txt +0 -0
  345. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/pynode_flow.egg-info/top_level.txt +0 -0
  346. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/requirements.txt +0 -0
  347. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/setup.bat +0 -0
  348. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/setup.cfg +0 -0
  349. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/setup.py +0 -0
  350. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/setup.sh +0 -0
  351. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/__init__.py +0 -0
  352. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_api_auth.py +0 -0
  353. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_api_errors.py +0 -0
  354. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_api_nodes.py +0 -0
  355. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_api_workflows.py +0 -0
  356. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_camera_node_backend.py +0 -0
  357. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_concurrency.py +0 -0
  358. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_config.py +0 -0
  359. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_control_slider_node.py +0 -0
  360. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_counter_sse.py +0 -0
  361. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_crop_node.py +0 -0
  362. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_debug_json_safety.py +0 -0
  363. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_delay_rate_limit.py +0 -0
  364. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_device_detection.py +0 -0
  365. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_frame_pacing.py +0 -0
  366. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_function_node.py +0 -0
  367. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_message_reader_pickle.py +0 -0
  368. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_models_dir.py +0 -0
  369. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_mqtt_services.py +0 -0
  370. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_node_registration.py +0 -0
  371. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_node_types_api.py +0 -0
  372. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_persistence.py +0 -0
  373. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_range_node.py +0 -0
  374. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_serialization.py +0 -0
  375. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_supervision_annotate_node.py +0 -0
  376. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_supervision_utils.py +0 -0
  377. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_supervision_zone_nodes.py +0 -0
  378. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_video_reader_node.py +0 -0
  379. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_workflow_engine.py +0 -0
  380. {pynode_flow-0.2.7 → pynode_flow-0.2.9}/tests/test_workflow_roundtrip.py +0 -0
@@ -90,12 +90,13 @@ Other vision nodes: `DrawPredictionsNode`, `CropNode`,
90
90
  read their `DEFAULT_CONFIG`/`properties` in `pynode/nodes/<Name>/` before use.
91
91
 
92
92
  ### Supervision
93
- Nodes built on the `supervision` library (`[vision]` extra). They register even
94
- without it installed, reporting an error and passing messages through.
93
+ Nodes built on the `supervision` library plus Roboflow's `trackers` (both in the
94
+ `[vision]` extra) and `reid` (`[reid]` extra). They register even without them installed, reporting an error and passing messages through.
95
95
 
96
96
  | Type | Ports | Config |
97
97
  |---|---|---|
98
- | `SupervisionTrackerNode` | 1→1 | `track_thresh`: "0.25"; `track_buffer`: "30" (frames a lost track is kept, scaled by `frame_rate`); `match_thresh`: "0.8" (IoU); `min_consecutive_frames`: "1" (raise to 2-3 to kill ghost tracks); `frame_rate`: "30" (**set to the real source fps**). Adds `track_id` to each detection plus `payload.tracks`/`track_count`. Has a `reset` action — a new object needs 2 frames before it is reported. **Does not draw** — wire an `SupervisionAnnotateNode` after it. |
98
+ | `SupervisionTrackerNode` | 1→1 | `algorithm`: `bytetrack` (default)\|`sort`\|`ocsort`\|`botsort` (from Roboflow's `trackers` package — `sv.ByteTrack` was removed in supervision 0.31); `track_thresh`: "0.25" (confidence to start a track); `high_thresh`: "" (blank = same as `track_thresh`); `track_buffer`: "30" (frames a lost track is kept, scaled by `frame_rate`); `min_iou`: "0.2" (**a real IoU** — the legacy `match_thresh` was an IoU *distance* and is auto-translated as `1 - value`; do not write `match_thresh` in new flows); `min_consecutive_frames`: "1" (raise to 2-3 to kill ghost tracks); `frame_rate`: "30" (**set to the real source fps**); `keep_untracked`: false (true = also pass detections with no track yet, without a `track_id`); `camera_motion`: false (ocsort/botsort, needs `payload.image`); botsort only: `reid_model`: `none`\|a Re-ID alias (needs `[reid]` extra + `payload.image`), `reid_device`: `auto`\|`cpu`\|`cuda`, `reid_appearance_thresh`: "0.25". Adds `track_id` (**0-based**) to each detection plus `payload.tracks`/`track_count`. Has a `reset` action — a new object needs 2 frames before it is reported. Thresholds apply live; changing algorithm/buffer/frame rate/camera motion/Re-ID rebuilds the tracker (ids restart). **Does not draw** — wire an `SupervisionAnnotateNode` after it. |
99
+ | `SupervisionReIDNode` | 1→**2** | Appearance re-identification (`reid` package, `[reid]` extra). `model`: `osnet_x1_0_msmt17_combineall` (default)\|`osnet_x1_0_mot17`\|`osnet_x1_0_sportsmot`\|`osnet_x1_0_dancetrack`\|`osnet_x1_0_soccernet`\|`fastreid_mot17_sbs50`\|`custom` (+ `custom_model`: hf://, https:// or local path); `device`: `auto`\|`cpu`\|`cuda`; `classes`: "person" (comma names/ids, blank = all); `match_distance`: "0.3" (max cosine distance, lower = stricter); `gallery`: "" (blank = private; **same name on several nodes = shared identities / cross-camera**); `use_as_track_id`: false (true = `track_id` becomes the identity so annotate/zone/line nodes follow it; original kept in `sv.data.local_track_id`); `refresh_interval`: "10" (frames between re-embeds of a tracked object); `ttl_seconds`: "0" (0 = never forget); `max_identities`: "1000"; `min_box_size`: "16"; `include_embeddings`: false. **Needs `payload.image`**; place after an SV Tracker (works without one, slower). Output 0 = msg with `reid_id` on each identified detection + `payload.identities`/`identity_count`; **output 1 = events** `{event:'new'\|'reidentified', reid_id, track_id?, bbox, class_name, source, gallery, distance?, absent_seconds?, previous_source?}` (topic `reid/<event>`). Model loads in the background on first frame (frames pass through until ready). `reset` action clears the gallery. SV Annotate labels show `ID<n>` when a `reid_id` is present. |
99
100
  | `SupervisionAnnotateNode` | 1→1 | `annotators`: list from `box round_box box_corner ellipse circle triangle dot color label percentage_bar blur pixelate background_overlay trace heatmap` (applied in a fixed order: pixel effects → shapes → labels; default `["box","label"]`); `color_lookup`: `class`\|`track`\|`index` (`track` falls back to `class` without ids); `thickness`/`text_scale`: "auto" (resolution-scaled) or a number; `show_class`/`show_confidence`/`show_track_id`: bool (label content); `trace_length`: "30". `trace` needs a `SupervisionTrackerNode` upstream; `heatmap` accumulates over time. `reset` action clears trace/heatmap state. |
100
101
  | `SupervisionLineCounterNode` | 1→**2** | `line`: "x1,y1,x2,y2" (image px; the GUI has a draw-on-frame editor); `anchors`: `corners`\|`center`\|`bottom_center`; `min_crossing_frames`: "1"; `draw`: "true"/"false". **Needs tracked detections** (SV Tracker upstream). Output 0 = every msg + `payload.line_in`/`line_out`/`line_in_by_class`/`line_out_by_class` and the line drawn; **output 1 = one msg per crossing** `{event:'line_crossing', direction, track_id, class_id, class_name, bbox, in_count, out_count}`. Actions: `reset` (zero counts), `set_geometry` (live line update, resets counts). |
101
102
  | `SupervisionZoneNode` | 1→**2** | `polygon`: JSON "[[x,y],...]" (image px; draw-on-frame editor in GUI); `anchor`: `bottom_center`\|`center`\|`top_center`; `filter_to_zone`: bool (drop outside detections); `draw`: "true"/"false"; `opacity`: "0.15". Output 0 = every msg + `payload.zone_count` and `in_zone` on each detection; **output 1 = enter/exit events** (needs track ids) `{event:'enter'\|'exit', track_id, class_id, class_name, bbox, zone_count}`. Actions: `reset` (event state), `set_geometry` (live polygon update). |
@@ -20,3 +20,6 @@ build/
20
20
  .DS_Store
21
21
  logs/
22
22
  *.log
23
+ # Credential store and its key: never bake them into an image
24
+ workflows/credentials.json
25
+ **/.credential_key
@@ -0,0 +1,90 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ # Weekly run against the latest releases of every dependency: upstream
7
+ # removals (e.g. supervision 0.31 dropping sv.ByteTrack) surface here
8
+ # instead of in a user's install.
9
+ schedule:
10
+ - cron: "0 6 * * 1"
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version:
21
+ - "3.10"
22
+ - 3.11
23
+ - 3.12
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - name: Install Python ${{ matrix.python-version }}
29
+ uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ cache: pip
33
+ cache-dependency-path: pyproject.toml
34
+
35
+ - name: Install system libraries for OpenCV
36
+ run: sudo apt-get update && sudo apt-get install -y libgl1 libglib2.0-0
37
+
38
+ - name: Install package
39
+ run: pip install -e .[dev]
40
+
41
+ - name: Run tests
42
+ run: pytest -q
43
+
44
+ # The core job installs no ML packages, so every Supervision / tracker /
45
+ # Re-ID test is skipped there (pytest.importorskip). This job installs the
46
+ # CPU builds so those nodes are actually exercised.
47
+ test-vision:
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+
52
+ - name: Install Python
53
+ uses: actions/setup-python@v5
54
+ with:
55
+ python-version: "3.12"
56
+ cache: pip
57
+ cache-dependency-path: pyproject.toml
58
+
59
+ - name: Install system libraries for OpenCV
60
+ run: sudo apt-get update && sudo apt-get install -y libgl1 libglib2.0-0
61
+
62
+ - name: Install CPU torch
63
+ run: pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
64
+
65
+ - name: Install package with vision, reid and mqtt extras
66
+ run: pip install -e ".[dev,reid,mqtt]"
67
+
68
+ - name: Show resolved versions
69
+ run: pip list | grep -i -E "supervision|trackers|reid|torch|ultralytics|numpy|opencv|paho"
70
+
71
+ - name: Run tests (deprecations from our own code are errors)
72
+ run: pytest -q -W "error::DeprecationWarning:pynode"
73
+
74
+ typecheck:
75
+ runs-on: ubuntu-latest
76
+ continue-on-error: true
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+
80
+ - uses: actions/setup-python@v5
81
+ with:
82
+ python-version: "3.11"
83
+ cache: pip
84
+ cache-dependency-path: pyproject.toml
85
+
86
+ - name: Install package and pyright
87
+ run: pip install -e .[dev] pyright
88
+
89
+ - name: Run pyright
90
+ run: pyright
@@ -0,0 +1,28 @@
1
+ name: "Git Hooks"
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request: ~
8
+
9
+ permissions: {}
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ run-prek:
17
+ name: prek run --all-files
18
+ runs-on: ubuntu-26.04
19
+ timeout-minutes: 1
20
+
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
+ with:
25
+ persist-credentials: false
26
+
27
+ - name: Run prek
28
+ uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1
@@ -155,6 +155,9 @@ Thumbs.db
155
155
  # ...and the example Node-RED flow shipped with the SocketNode nodes
156
156
  !pynode/nodes/SocketNode/interop/*.json
157
157
 
158
+ # Credential store key, generated next to credentials.json on first use
159
+ .credential_key
160
+
158
161
  # PyTorch model files
159
162
  *.pt
160
163
  models/
@@ -0,0 +1,9 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.17.0
4
+ hooks:
5
+ - id: ruff-check
6
+ args: [
7
+ "--fix",
8
+ "--show-fixes",
9
+ ]
@@ -60,8 +60,9 @@ deploy:
60
60
  ### Persistent Storage
61
61
 
62
62
  The following directories are mounted for persistence:
63
- - `./workflows` - Saved workflows
64
- - `./logs` - Application logs
63
+ - `./models` - Downloaded model weights (YOLO, Re-ID; Hugging Face downloads land in `models/huggingface` via `HF_HOME`). Without this volume every new container downloads them again.
64
+ - `./logs` - `pynode.log`, rotated at 10 MB with 5 backups (`PYNODE_LOG_FILE`).
65
+ - `./workflows` - Saved workflows, plus the credential store (`credentials.json`) and, unless `PYNODE_CREDENTIAL_KEY` or `PYNODE_CREDENTIAL_KEY_FILE` is set, its generated key (`.credential_key`). Back up both files together. Stop PyNode before restoring `credentials.json` from a backup: a running instance keeps the store in memory and would overwrite the restored file on its next change.
65
66
 
66
67
  ## Building
67
68
 
@@ -1,9 +1,11 @@
1
- FROM docker.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04
1
+ FROM docker.io/nvidia/cuda:12.8.1-runtime-ubuntu22.04
2
2
 
3
3
  # Set environment variables
4
4
  ENV DEBIAN_FRONTEND=noninteractive
5
5
  ENV PYTHONUNBUFFERED=1
6
- ENV CUDA_VERSION=12.6
6
+ # cu128 PyTorch wheels are the first to ship Blackwell (sm_120, e.g. RTX 50xx)
7
+ # kernels; older cu126 builds raise "no kernel image is available".
8
+ ENV CUDA_VERSION=12.8
7
9
 
8
10
  # Install system dependencies
9
11
  RUN apt-get update && apt-get install -y \
@@ -74,7 +76,7 @@ EXPOSE 5000
74
76
 
75
77
  # Health check (uses the bundled Python interpreter; no extra tools required)
76
78
  HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
77
- CMD ["/app/appenv/bin/python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"]
79
+ CMD ["/app/appenv/bin/python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/healthz')"]
78
80
 
79
81
  ENTRYPOINT ["/app/docker-entrypoint.sh"]
80
82
  CMD ["/app/appenv/bin/python", "-m", "pynode", "--production", "--host", "0.0.0.0", "--port", "5000"]
@@ -56,7 +56,7 @@ EXPOSE 5000
56
56
 
57
57
  # Health check (uses the bundled Python interpreter; no extra tools required)
58
58
  HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
59
- CMD ["/app/appenv/bin/python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"]
59
+ CMD ["/app/appenv/bin/python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/healthz')"]
60
60
 
61
61
  ENTRYPOINT ["/app/docker-entrypoint.sh"]
62
62
  CMD ["/app/appenv/bin/python", "-m", "pynode", "--production", "--host", "0.0.0.0", "--port", "5000"]
@@ -32,7 +32,8 @@ Install only the node groups you need:
32
32
 
33
33
  | Extra | Installs | Nodes it enables |
34
34
  |-------|----------|------------------|
35
- | `vision` | ultralytics, torch, torchvision, supervision | UltralyticsNode, DrawPredictionsNode, the Supervision nodes, … |
35
+ | `vision` | ultralytics, torch, torchvision, supervision, trackers | UltralyticsNode, DrawPredictionsNode, the Supervision nodes (tracking, zones, annotators), … |
36
+ | `reid` | reid (+ `vision`) | SV Re-ID node, BoT-SORT appearance association in SV Tracker |
36
37
  | `mqtt` | paho-mqtt | MQTTNode |
37
38
  | `camera` | framesource[full] | FrameSourceNode |
38
39
  | `inference` | onnxruntime (+ `vision`) | InferenceNode |
@@ -102,6 +103,14 @@ pynode --host 0.0.0.0 --port 5000 --production
102
103
  - `--host`: Host address to bind to (default: 0.0.0.0)
103
104
  - `--port`: Port to bind to (default: 5000)
104
105
  - `--production`: Run in production mode using Waitress server
106
+ - `--threads`: Request threads in production mode (default: 32, or `PYNODE_THREADS`)
107
+ - `--api-key`: Require an API key on all `/api/` requests (or `PYNODE_API_KEY`)
108
+ - `--cors-origins`: Comma-separated allowed browser origins (or `PYNODE_CORS_ORIGINS`)
109
+ - `--data-dir` / `--models-dir`: Where workflows and model weights are stored
110
+ - `--log-level`: DEBUG, INFO, WARNING or ERROR (default: INFO)
111
+ - `--log-file`: Also log to a rotating file (or `PYNODE_LOG_FILE`)
112
+
113
+ See "Running in Production" in the README for health checks, shutdown behaviour and reverse-proxy notes.
105
114
 
106
115
  ## Installing Node Dependencies
107
116
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynode-flow
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Python based Node-RED-like visual workflow system
5
5
  Author: olkham
6
6
  License-Expression: MIT
@@ -26,11 +26,16 @@ Requires-Dist: flask-cors>=4.0
26
26
  Requires-Dist: waitress>=2.1
27
27
  Requires-Dist: numpy
28
28
  Requires-Dist: opencv-python
29
+ Requires-Dist: cryptography>=42
29
30
  Provides-Extra: vision
30
31
  Requires-Dist: ultralytics; extra == "vision"
31
32
  Requires-Dist: torch; extra == "vision"
32
33
  Requires-Dist: torchvision; extra == "vision"
33
- Requires-Dist: supervision; extra == "vision"
34
+ Requires-Dist: supervision>=0.31.0; extra == "vision"
35
+ Requires-Dist: trackers>=2.7.0; extra == "vision"
36
+ Provides-Extra: reid
37
+ Requires-Dist: pynode-flow[vision]; extra == "reid"
38
+ Requires-Dist: reid>=0.1.0; extra == "reid"
34
39
  Provides-Extra: mqtt
35
40
  Requires-Dist: paho-mqtt; extra == "mqtt"
36
41
  Provides-Extra: camera
@@ -53,6 +58,7 @@ Requires-Dist: pynode-flow[mqtt]; extra == "full"
53
58
  Requires-Dist: pynode-flow[camera]; extra == "full"
54
59
  Requires-Dist: pynode-flow[inference]; extra == "full"
55
60
  Requires-Dist: pynode-flow[vlm]; extra == "full"
61
+ Requires-Dist: pynode-flow[reid]; extra == "full"
56
62
  Requires-Dist: pynode-flow[upload]; extra == "full"
57
63
  Requires-Dist: pynode-flow[discovery]; extra == "full"
58
64
  Provides-Extra: all
@@ -67,7 +73,10 @@ Dynamic: license-file
67
73
 
68
74
  A Node-RED-like visual workflow editor with a Python backend. Create workflows by connecting Python nodes that process and route messages.
69
75
 
70
- https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2
76
+ <!-- https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2 -->
77
+ [![Watch the PyNode promo video on YouTube](docs/video_thumb_play.png)](https://www.youtube.com/watch?v=d-sQIWd4X9Q)
78
+
79
+ <p align="center"><a href="https://www.youtube.com/watch?v=d-sQIWd4X9Q">▶ Watch the promo video on YouTube</a></p>
71
80
 
72
81
  ## Features
73
82
 
@@ -94,8 +103,8 @@ carries its own documentation in the editor's **Information** panel (ℹ️ tab)
94
103
  | **Output** | Image Viewer, Image Writer, Video Writer, Message Writer, Roboflow Upload |
95
104
  | **Logic** | Switch (route on conditions), Gate, Sync, Auto Sync |
96
105
  | **Network** | MQTT In / Out, UDP In / Out, TCP In / Out, REST Endpoint, Webhook, mDNS Broadcast / Discovery |
97
- | **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
98
- | **Supervision** | SV Tracker (ByteTrack), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
106
+ | **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Class Map (relabel classes), Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
107
+ | **Supervision** | SV Tracker (ByteTrack / SORT / OC-SORT / BoT-SORT via Roboflow `trackers`), SV Re-ID (appearance identities that survive occlusion and carry across cameras), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
99
108
  | **Analysis** | BBox Metrics, Polygon Metrics, Point in Shape |
100
109
  | **OpenCV** | ~28 image-processing nodes: Blur, Threshold, Morphology, Edge Detector, Find Contours, Perspective, Resize, Rotate, Colormap, Histogram, FFT, Template Match, RealSense Depth, and more |
101
110
  | **Node Probes** | Rate Probe, Queue Length Probe, Counter |
@@ -256,8 +265,25 @@ order:
256
265
 
257
266
  - **API key**: start with `pynode --api-key <secret>` (or set the `PYNODE_API_KEY` env var). All `/api/` requests then require the key via the `X-API-Key` header or an `api_key` query parameter; the web UI prompts for it on first load and remembers it in the browser. Unset/empty = no authentication (the default).
258
267
  - **CORS**: restrict allowed browser origins with `pynode --cors-origins http://localhost:5000,https://myhost` (or the `PYNODE_CORS_ORIGINS` env var). Default is `*` (all origins).
268
+ - **Credentials**: nodes that need a secret (Webhook and REST Endpoint authentication, the Roboflow Upload API key, the Qwen3-VL HuggingFace token) and MQTT brokers (the password) reference a stored credential by ID instead of keeping the secret in the workflow or the broker settings. Secrets saved in plain text by earlier versions keep working until you select a credential in their place. Secrets are encrypted in `<data dir>/workflows/credentials.json` and are never sent back to the browser. The encryption key comes from `PYNODE_CREDENTIAL_KEY` (a Fernet key, e.g. from `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`) or from the file named by `PYNODE_CREDENTIAL_KEY_FILE`. With neither set, PyNode generates `<data dir>/workflows/.credential_key` on first use and logs its location. Back up the key together with `credentials.json`, because the secrets cannot be recovered without it. If the data directory may be copied elsewhere, supply the key from outside it. Credentials prevent accidental exposure, not a malicious flow editor: anyone who can edit and deploy flows (e.g. with a Function node) can use any stored credential.
259
269
  - **Bind locally**: when you don't need network access, run `pynode --host 127.0.0.1` so the server is only reachable from the local machine.
260
270
 
271
+ PyNode logs a warning at startup when it is listening on a network interface without an API key.
272
+
273
+ ## Running in Production
274
+
275
+ ```bash
276
+ pynode --production --host 0.0.0.0 --api-key "$PYNODE_API_KEY" --log-file /var/log/pynode/pynode.log
277
+ ```
278
+
279
+ - **Server**: `--production` serves through Waitress. `--threads N` (or `PYNODE_THREADS`, default 32) sets the request threads. Every open editor tab holds one for its event stream and every live MJPEG viewer holds another, so raise it if many people watch at once.
280
+ - **Health checks**: `GET /healthz` is an unauthenticated liveness probe for Docker, Kubernetes or a load balancer. `GET /api/health` (API key required) returns the version, uptime and the run state of every workflow, for monitoring and alerting.
281
+ - **Graceful shutdown**: on `SIGTERM` (`docker stop`, systemd, Kubernetes) and Ctrl+C, PyNode stops every workflow before exiting, so video writers finalise their files, detection logs are flushed and cameras are released. Give the supervisor a stop timeout of 10 seconds or more.
282
+ - **Logs**: `--log-file <path>` (or `PYNODE_LOG_FILE`) adds a rotating log file (10 MB, 5 backups) next to console output.
283
+ - **Upload limit**: request bodies are capped at 4096 MB; change it with `PYNODE_MAX_UPLOAD_MB` (`0` = unlimited).
284
+ - **TLS**: PyNode speaks plain HTTP. Terminate TLS in a reverse proxy (nginx, Caddy, Traefik) and disable response buffering for `/api/debug/stream` and the MJPEG routes, which are long-lived streams.
285
+ - **Persistent data**: keep the data directory (`--data-dir`, workflows and credentials) and the models directory (`--models-dir`, downloaded weights) on persistent storage. See [DOCKER.md](DOCKER.md) for the container setup.
286
+
261
287
  ## Docker Setup
262
288
 
263
289
  PyNode can be run in a Docker container with GPU support (CUDA 12.6).
@@ -2,7 +2,10 @@
2
2
 
3
3
  A Node-RED-like visual workflow editor with a Python backend. Create workflows by connecting Python nodes that process and route messages.
4
4
 
5
- https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2
5
+ <!-- https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2 -->
6
+ [![Watch the PyNode promo video on YouTube](docs/video_thumb_play.png)](https://www.youtube.com/watch?v=d-sQIWd4X9Q)
7
+
8
+ <p align="center"><a href="https://www.youtube.com/watch?v=d-sQIWd4X9Q">▶ Watch the promo video on YouTube</a></p>
6
9
 
7
10
  ## Features
8
11
 
@@ -29,8 +32,8 @@ carries its own documentation in the editor's **Information** panel (ℹ️ tab)
29
32
  | **Output** | Image Viewer, Image Writer, Video Writer, Message Writer, Roboflow Upload |
30
33
  | **Logic** | Switch (route on conditions), Gate, Sync, Auto Sync |
31
34
  | **Network** | MQTT In / Out, UDP In / Out, TCP In / Out, REST Endpoint, Webhook, mDNS Broadcast / Discovery |
32
- | **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
33
- | **Supervision** | SV Tracker (ByteTrack), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
35
+ | **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Class Map (relabel classes), Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
36
+ | **Supervision** | SV Tracker (ByteTrack / SORT / OC-SORT / BoT-SORT via Roboflow `trackers`), SV Re-ID (appearance identities that survive occlusion and carry across cameras), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
34
37
  | **Analysis** | BBox Metrics, Polygon Metrics, Point in Shape |
35
38
  | **OpenCV** | ~28 image-processing nodes: Blur, Threshold, Morphology, Edge Detector, Find Contours, Perspective, Resize, Rotate, Colormap, Histogram, FFT, Template Match, RealSense Depth, and more |
36
39
  | **Node Probes** | Rate Probe, Queue Length Probe, Counter |
@@ -191,8 +194,25 @@ order:
191
194
 
192
195
  - **API key**: start with `pynode --api-key <secret>` (or set the `PYNODE_API_KEY` env var). All `/api/` requests then require the key via the `X-API-Key` header or an `api_key` query parameter; the web UI prompts for it on first load and remembers it in the browser. Unset/empty = no authentication (the default).
193
196
  - **CORS**: restrict allowed browser origins with `pynode --cors-origins http://localhost:5000,https://myhost` (or the `PYNODE_CORS_ORIGINS` env var). Default is `*` (all origins).
197
+ - **Credentials**: nodes that need a secret (Webhook and REST Endpoint authentication, the Roboflow Upload API key, the Qwen3-VL HuggingFace token) and MQTT brokers (the password) reference a stored credential by ID instead of keeping the secret in the workflow or the broker settings. Secrets saved in plain text by earlier versions keep working until you select a credential in their place. Secrets are encrypted in `<data dir>/workflows/credentials.json` and are never sent back to the browser. The encryption key comes from `PYNODE_CREDENTIAL_KEY` (a Fernet key, e.g. from `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`) or from the file named by `PYNODE_CREDENTIAL_KEY_FILE`. With neither set, PyNode generates `<data dir>/workflows/.credential_key` on first use and logs its location. Back up the key together with `credentials.json`, because the secrets cannot be recovered without it. If the data directory may be copied elsewhere, supply the key from outside it. Credentials prevent accidental exposure, not a malicious flow editor: anyone who can edit and deploy flows (e.g. with a Function node) can use any stored credential.
194
198
  - **Bind locally**: when you don't need network access, run `pynode --host 127.0.0.1` so the server is only reachable from the local machine.
195
199
 
200
+ PyNode logs a warning at startup when it is listening on a network interface without an API key.
201
+
202
+ ## Running in Production
203
+
204
+ ```bash
205
+ pynode --production --host 0.0.0.0 --api-key "$PYNODE_API_KEY" --log-file /var/log/pynode/pynode.log
206
+ ```
207
+
208
+ - **Server**: `--production` serves through Waitress. `--threads N` (or `PYNODE_THREADS`, default 32) sets the request threads. Every open editor tab holds one for its event stream and every live MJPEG viewer holds another, so raise it if many people watch at once.
209
+ - **Health checks**: `GET /healthz` is an unauthenticated liveness probe for Docker, Kubernetes or a load balancer. `GET /api/health` (API key required) returns the version, uptime and the run state of every workflow, for monitoring and alerting.
210
+ - **Graceful shutdown**: on `SIGTERM` (`docker stop`, systemd, Kubernetes) and Ctrl+C, PyNode stops every workflow before exiting, so video writers finalise their files, detection logs are flushed and cameras are released. Give the supervisor a stop timeout of 10 seconds or more.
211
+ - **Logs**: `--log-file <path>` (or `PYNODE_LOG_FILE`) adds a rotating log file (10 MB, 5 backups) next to console output.
212
+ - **Upload limit**: request bodies are capped at 4096 MB; change it with `PYNODE_MAX_UPLOAD_MB` (`0` = unlimited).
213
+ - **TLS**: PyNode speaks plain HTTP. Terminate TLS in a reverse proxy (nginx, Caddy, Traefik) and disable response buffering for `/api/debug/stream` and the MJPEG routes, which are long-lived streams.
214
+ - **Persistent data**: keep the data directory (`--data-dir`, workflows and credentials) and the models directory (`--models-dir`, downloaded weights) on persistent storage. See [DOCKER.md](DOCKER.md) for the container setup.
215
+
196
216
  ## Docker Setup
197
217
 
198
218
  PyNode can be run in a Docker container with GPU support (CUDA 12.6).
@@ -14,11 +14,19 @@ services:
14
14
  volumes:
15
15
  # Mount persistent data
16
16
  - ./workflows:/app/workflows
17
+ - ./models:/app/models
17
18
  - ./logs:/app/logs
18
19
  environment:
19
20
  - PYTHONUNBUFFERED=1
21
+ - PYNODE_LOG_FILE=/app/logs/pynode.log
22
+ # Keep Hugging Face downloads (Re-ID / VLM weights) on the models volume
23
+ - HF_HOME=/app/models/huggingface
24
+ # Strongly recommended whenever the port is reachable from other machines:
25
+ - PYNODE_API_KEY=${PYNODE_API_KEY:-}
20
26
  - HOST_IP=${HOST_IP:-}
21
27
  restart: unless-stopped
28
+ # PyNode stops every workflow on SIGTERM so recordings and logs are finalised.
29
+ stop_grace_period: 30s
22
30
  # Rotate container logs so they cannot fill the host disk
23
31
  logging:
24
32
  driver: json-file
@@ -3,7 +3,7 @@ set -e
3
3
 
4
4
  # Ensure bind-mounted directories are writable by appuser (uid 1000).
5
5
  # docker-compose mounts host directories that may be owned by root.
6
- for dir in /app/workflows /app/workflows/services /app/logs; do
6
+ for dir in /app/workflows /app/workflows/services /app/logs /app/models; do
7
7
  mkdir -p "$dir"
8
8
  chown -R 1000:1000 "$dir" 2>/dev/null || true
9
9
  done
@@ -246,6 +246,45 @@ Properties define the configuration UI shown when a node is selected.
246
246
  }
247
247
  ```
248
248
 
249
+ ### Credential (stored secret)
250
+
251
+ Never put a secret (token, password, API key) in a `text` property. Node config is saved as plain text in `workflow.json`, its backups and exports, and is sent to the editor. Use a `credential` property instead: the config then holds only a credential ID, while the secret is stored encrypted in the credential store and never sent back to the browser.
252
+
253
+ ```python
254
+ {
255
+ 'name': 'credential',
256
+ 'label': 'Credential',
257
+ 'type': 'credential',
258
+ 'credentialType': 'secret' # core type: one secret value
259
+ }
260
+ ```
261
+
262
+ Resolve it when you need the value, and keep the result in memory only (never in `self.config`, a message or a log line):
263
+
264
+ ```python
265
+ from pynode.nodes.base_node import CredentialError
266
+
267
+ try:
268
+ token = self.get_credential(self.config.get('credential'))['value']
269
+ except CredentialError as e:
270
+ self.report_error(str(e)) # not found, store locked, or wrong key
271
+ return
272
+ ```
273
+
274
+ For the core `secret` type, `self.get_secret()` does the lookup for you and raises `CredentialError` if the credential is of another type. Give it the name of a legacy plain-text property when you are converting an existing node, so flows saved before the change keep working. A selected credential that cannot be resolved raises and never falls back to the legacy value:
275
+
276
+ ```python
277
+ try:
278
+ token = self.get_secret('credential', legacy_key='api_key') # '' when neither is set
279
+ except CredentialError as e:
280
+ self.report_error(str(e))
281
+ return
282
+ ```
283
+
284
+ Libraries often quote a secret in their error messages (for example in a request URL), so remove it from any exception text before you report or log it.
285
+
286
+ Node packages can register their own credential types (for example a username plus a password) with `pynode.credential_store.register_credential_type()`.
287
+
249
288
  ### Conditional Visibility (`showIf`)
250
289
 
251
290
  Show a property only when another property has a specific value:
@@ -689,6 +728,8 @@ class InternalNode(BaseNode):
689
728
  4. **Using hardcoded strings** - Use `MessageKeys.PAYLOAD` not `'payload'`, etc.
690
729
  5. **Not handling `None` payloads** - Always provide defaults: `msg.get(MessageKeys.PAYLOAD, default)`.
691
730
  6. **Blocking in `on_input`** - Long operations block the worker thread. For CPU-bound work, consider additional threads.
731
+ 7. **Doing heavy work in a sink's `on_input_direct`** - It runs on the *sending* node's thread for every message, so slow work there (encoding, disk or network I/O) caps the frame rate of the node feeding it. Keep it to storing a reference and do the work on your own thread or when the result is requested.
732
+ 8. **Setting `shares_input = True` on a node that modifies or forwards its input** - That flag lets `send()` skip the fan-out copy for read-only nodes. A node that mutates the payload or sends it on must leave it `False`.
692
733
  7. **Putting the class in the wrong folder** - The folder must be directly under `pynode/nodes/`, not nested deeper.
693
734
  8. **Class name not ending in `Node`** - Auto-discovery filters by this suffix.
694
735
  9. **Circular imports** - Only import from `pynode.nodes.base_node`. Don't import other node types.
@@ -202,28 +202,31 @@ class BaseNode:
202
202
 
203
203
  ### Queue Management
204
204
 
205
- Messages are enqueued with optional dropping:
206
-
207
- ```python
208
- def receive(self, msg: Dict[str, Any], input_index: int = 0):
209
- """Receive a message on an input."""
210
- if not self.enabled:
211
- return
212
-
213
- # Add queue length metadata
214
- msg[MessageKeys.QUEUE_LENGTH] = self._message_queue.qsize()
215
-
216
- try:
217
- if self.drop_while_busy and self._processing:
218
- # Drop message if node is busy
219
- self.drop_count += 1
220
- return
221
-
222
- # Enqueue message (blocks if queue is full)
223
- self._message_queue.put((deepcopy(msg), input_index))
224
- except queue.Full:
225
- self.drop_count += 1
226
- ```
205
+ Delivery is decided per recipient inside `BaseNode.send()`:
206
+
207
+ | Recipient | What happens |
208
+ |---|---|
209
+ | Sink with `on_input_direct` (no outputs) | Called synchronously **on the sending node's thread**. |
210
+ | Idle node | Message is put on the node's queue; its worker thread wakes immediately. |
211
+ | Busy node, `drop_messages` on | Message is parked in a one-slot, latest-wins inbox (one slot per input port). A newer message replaces it and `drop_count` is incremented. When the node finishes, it continues straight away with the newest message. |
212
+ | Busy node, `drop_messages` off | Message is queued (up to 1000). |
213
+
214
+ Copies are made only where a branch could otherwise see another branch's changes:
215
+
216
+ - one recipient: no copy, the recipient takes over the sender's objects;
217
+ - several recipients: each gets its own `deepcopy`, except
218
+ - nodes that declare `shares_input = True` (they only read the message and never
219
+ forward its objects, e.g. Image Viewer and Rate Probe), which share the original, and
220
+ - busy nodes, whose copy is made later on their own worker thread and only for the
221
+ messages they actually process.
222
+
223
+ Two consequences for node authors:
224
+
225
+ - A sink's `on_input_direct` runs on the upstream node's thread for every message, so
226
+ anything slow there throttles the source. Store what you need and do the heavy work
227
+ elsewhere (the Image Viewer keeps a reference and encodes when the UI asks for a frame).
228
+ - Set `shares_input = True` only if the node never mutates anything reachable from the
229
+ incoming message and never passes those objects downstream.
227
230
 
228
231
  ### Connection Routing
229
232
 
@@ -4,13 +4,11 @@ This example runs without the UI - it creates nodes programmatically,
4
4
  connects them, and displays the output in an OpenCV window.
5
5
  """
6
6
 
7
- import sys
8
- import os
9
7
  import time
10
8
  import cv2
11
9
  import numpy as np
12
10
  import base64
13
- from typing import Dict, Any
11
+ from typing import Any
14
12
 
15
13
  from pynode.workflow_engine import WorkflowEngine
16
14
  from pynode.nodes.base_node import BaseNode, MessageKeys
@@ -47,7 +45,7 @@ class DisplayNode(BaseNode):
47
45
  cv2.destroyWindow(self.window_name)
48
46
  print(f"[{self.name}] Display window closed")
49
47
 
50
- def on_input(self, msg: Dict[str, Any], input_index: int = 0):
48
+ def on_input(self, msg: dict[str, Any], input_index: int = 0):
51
49
  """
52
50
  Process incoming image messages and display them.
53
51
  """
@@ -4,13 +4,11 @@ and displays the annotated results using OpenCV.
4
4
  This example runs without the UI - it creates nodes programmatically.
5
5
  """
6
6
 
7
- import sys
8
- import os
9
7
  import time
10
8
  import cv2
11
9
  import numpy as np
12
10
  import base64
13
- from typing import Dict, Any
11
+ from typing import Any
14
12
 
15
13
  from pynode.workflow_engine import WorkflowEngine
16
14
  from pynode.nodes.base_node import BaseNode, MessageKeys
@@ -48,7 +46,7 @@ class DisplayNode(BaseNode):
48
46
  cv2.destroyWindow(self.window_name)
49
47
  print(f"[{self.name}] Display window closed")
50
48
 
51
- def on_input(self, msg: Dict[str, Any], input_index: int = 0):
49
+ def on_input(self, msg: dict[str, Any], input_index: int = 0):
52
50
  """
53
51
  Process incoming image messages and display them.
54
52
  """
@@ -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.7'
22
- __version_tuple__ = version_tuple = (0, 2, 7)
21
+ __version__ = version = '0.2.9'
22
+ __version_tuple__ = version_tuple = (0, 2, 9)
23
23
 
24
- __commit_id__ = commit_id = 'g6f8c52570'
24
+ __commit_id__ = commit_id = 'gdd99cab55'
@@ -1,12 +1,15 @@
1
1
  """API blueprints for the PyNode Flask server."""
2
2
 
3
+ from pynode.api.credentials import credentials_bp
4
+ from pynode.api.health import health_bp
3
5
  from pynode.api.nodes import nodes_bp
4
6
  from pynode.api.services import services_bp
5
7
  from pynode.api.sse import sse_bp
6
8
  from pynode.api.uploads import uploads_bp
7
9
  from pynode.api.workflows import workflows_bp
8
10
 
9
- ALL_BLUEPRINTS = (workflows_bp, nodes_bp, services_bp, uploads_bp, sse_bp)
11
+ ALL_BLUEPRINTS = (workflows_bp, nodes_bp, services_bp, credentials_bp, uploads_bp, sse_bp,
12
+ health_bp)
10
13
 
11
14
 
12
15
  def register_blueprints(app):