pynode-flow 0.2.2__tar.gz → 0.2.4__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.
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/.dockerignore +0 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/.gitignore +2 -0
- pynode_flow-0.2.4/DOCKER.md +196 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/Dockerfile +22 -8
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/Dockerfile.cpu +22 -9
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/PKG-INFO +20 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/README.md +19 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/docker-compose.yml +6 -1
- pynode_flow-0.2.4/docker-entrypoint.sh +12 -0
- pynode_flow-0.2.4/plan-pynodeReview-2026-07-18.prompt.md +135 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/__init__.py +5 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/_version.py +3 -3
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/nodes.py +5 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/config.py +42 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/main.py +9 -0
- pynode_flow-0.2.4/pynode/nodes/ControlSliderNode/control_slider_node.py +208 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CropNode/crop_node.py +141 -12
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DelayNode/delay_node.py +2 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FrameSourceNode/frame_source_node.py +1 -1
- pynode_flow-0.2.4/pynode/nodes/FunctionNode/function_node.py +309 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageFormatNode/image_format_node.py +2 -2
- pynode_flow-0.2.4/pynode/nodes/ImageUploadNode/image_upload_node.py +246 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageViewerNode/image_viewer_node.py +16 -9
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LinkNode/link_bus.py +7 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MQTTNode/mqtt_in_node.py +4 -3
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MQTTNode/mqtt_out_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MQTTNode/mqtt_service.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OmronCameraNode/omron_camera_node.py +13 -3
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/arithmetic_node.py +5 -5
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/blob_detector_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/colormap_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/fft_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/focus_estimation_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/histogram_node.py +2 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/hough_circles_node.py +2 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/in_range_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/perspective_node.py +6 -6
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/realsense_depth_node.py +2 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +1 -1
- pynode_flow-0.2.4/pynode/nodes/RangeNode/range_node.py +220 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +1 -1
- pynode_flow-0.2.4/pynode/nodes/SocketNode/__init__.py +28 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/interop/README.md +239 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/interop/nodered-example-flow.json +318 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/ndjson_protocol.py +176 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/nodered_snippets.py +147 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/requirements.txt +4 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/tcp_in_node.py +269 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/tcp_out_node.py +270 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/udp_in_node.py +257 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/udp_out_node.py +253 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/udp_probe.py +105 -0
- pynode_flow-0.2.4/pynode/nodes/SocketNode/udp_protocol.py +479 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SplitNode/split_node.py +2 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SwitchNode/switch_node.py +8 -2
- pynode_flow-0.2.4/pynode/nodes/UltralyticsNode/__init__.py +11 -0
- pynode_flow-0.2.4/pynode/nodes/UltralyticsNode/model_paths.py +115 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/UltralyticsNode/ultralytics_node.py +20 -14
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/WebhookNode/webhook_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/__init__.py +6 -5
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/base_node.py +161 -53
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/image_utils.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/info.py +36 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/mDNSNode/mdns_discovery_node.py +1 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/mDNSNode/mdns_manager.py +31 -6
- pynode_flow-0.2.4/pynode/static/examples/16-socket-bridge.json +75 -0
- pynode_flow-0.2.4/pynode/static/examples/17-slider-crop.json +105 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/events.js +35 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/main.js +5 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/nodes.js +56 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/properties.js +182 -1
- pynode_flow-0.2.4/pynode/static/js/ui-utils.js +82 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/style.css +226 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/workflow_engine.py +0 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/PKG-INFO +20 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/SOURCES.txt +26 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/scm_file_list.json +266 -240
- pynode_flow-0.2.4/pynode_flow.egg-info/scm_version.json +8 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pyproject.toml +10 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/setup.sh +4 -5
- pynode_flow-0.2.4/tests/test_control_slider_node.py +199 -0
- pynode_flow-0.2.4/tests/test_crop_node.py +138 -0
- pynode_flow-0.2.4/tests/test_function_node.py +200 -0
- pynode_flow-0.2.4/tests/test_image_upload_node.py +176 -0
- pynode_flow-0.2.4/tests/test_models_dir.py +230 -0
- pynode_flow-0.2.4/tests/test_range_node.py +180 -0
- pynode_flow-0.2.4/tests/test_send_copy_semantics.py +226 -0
- pynode_flow-0.2.4/tests/test_socket_nodes.py +1292 -0
- pynode_flow-0.2.2/DOCKER.md +0 -113
- pynode_flow-0.2.2/pynode/nodes/FunctionNode/function_node.py +0 -168
- pynode_flow-0.2.2/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -96
- pynode_flow-0.2.2/pynode/nodes/RangeNode/range_node.py +0 -115
- pynode_flow-0.2.2/pynode/nodes/UltralyticsNode/__init__.py +0 -4
- pynode_flow-0.2.2/pynode/static/js/ui-utils.js +0 -28
- pynode_flow-0.2.2/pynode_flow.egg-info/scm_version.json +0 -8
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/.claude/skills/pynode-workflows/SKILL.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/INSTALL.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/LICENSE +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/MANIFEST.in +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/examples/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/install_nodes.bat +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/install_nodes.sh +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/plan-pynodeRepoReview.prompt.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/helpers.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/services.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/sse.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/api/workflows.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/node_registry.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CameraNode/camera_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ChangeNode/change_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DebugNode/debug_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DelayNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ErrorNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/FunctionNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/GateNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/inference_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InferenceNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/InjectNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/JoinNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LinkNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LinkNode/link_in_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LinkNode/link_out_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/LinkNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RateProbeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SplitNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SwitchNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/TemplateNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/TrackerNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/VideoReaderNode/video_reader_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/nodes/messages.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/server.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/01-hello-world.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/02-function-transform.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/03-switch-routing.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/04-flow-control.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/05-webcam-viewer.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/06-webcam-yolo.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/07-blur-people.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/examples/manifest.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/index.html +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/config.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/debug.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/mqtt-services.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/state.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/workflow.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/static/js/workflows.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode/workflow_manager.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/setup.bat +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/setup.cfg +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/setup.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/conftest.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_api_nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_api_workflows.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_camera_node_backend.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_config.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_debug_workflow_scope.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_delay_rate_limit.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_device_detection.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_link_nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_mqtt_services.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_node_types_api.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_video_reader_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.4}/tests/test_workflow_roundtrip.py +0 -0
|
@@ -152,6 +152,8 @@ Thumbs.db
|
|
|
152
152
|
*.json
|
|
153
153
|
# ...but keep the bundled, GUI-loadable example workflows
|
|
154
154
|
!pynode/static/examples/*.json
|
|
155
|
+
# ...and the example Node-RED flow shipped with the SocketNode nodes
|
|
156
|
+
!pynode/nodes/SocketNode/interop/*.json
|
|
155
157
|
|
|
156
158
|
# PyTorch model files
|
|
157
159
|
*.pt
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Docker Setup for PyNode
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Docker Engine with GPU support (NVIDIA Container Toolkit)
|
|
6
|
+
- Docker Compose v1.28+ (for GPU support)
|
|
7
|
+
- NVIDIA GPU with CUDA support
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### Build and Run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
docker-compose up --build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Run in Background
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
docker-compose up -d
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### View Logs
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker-compose logs -f
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Stop the Application
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker-compose down
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
### Ports
|
|
38
|
+
|
|
39
|
+
The application runs on port `5000` by default. To change the port, edit `docker-compose.yml`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
ports:
|
|
43
|
+
- "8080:5000" # Change 8080 to your desired port
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### GPU Support
|
|
47
|
+
|
|
48
|
+
The Docker Compose file is configured to use all available NVIDIA GPUs. To limit GPU access:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
deploy:
|
|
52
|
+
resources:
|
|
53
|
+
reservations:
|
|
54
|
+
devices:
|
|
55
|
+
- driver: nvidia
|
|
56
|
+
device_ids: ['0'] # Use only GPU 0
|
|
57
|
+
capabilities: [gpu]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Persistent Storage
|
|
61
|
+
|
|
62
|
+
The following directories are mounted for persistence:
|
|
63
|
+
- `./workflows` - Saved workflows
|
|
64
|
+
- `./logs` - Application logs
|
|
65
|
+
|
|
66
|
+
## Building
|
|
67
|
+
|
|
68
|
+
### Version
|
|
69
|
+
|
|
70
|
+
The version shown in the UI is derived automatically from git history during
|
|
71
|
+
the Docker build — no extra flags or environment variables are needed. Tagged
|
|
72
|
+
commits display a clean version (e.g. `v0.2.3`); untagged commits include the
|
|
73
|
+
distance and short SHA (e.g. `v0.2.3-1-gcd272cf`).
|
|
74
|
+
|
|
75
|
+
### Custom CUDA Version
|
|
76
|
+
|
|
77
|
+
To use a different CUDA version, edit the `Dockerfile`:
|
|
78
|
+
|
|
79
|
+
```dockerfile
|
|
80
|
+
FROM nvidia/cuda:12.8.0-runtime-ubuntu22.04 # Change CUDA version
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Without GPU Support
|
|
84
|
+
|
|
85
|
+
For CPU-only deployment, use the CPU Dockerfile:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
docker build -f Dockerfile.cpu -t pynode-cpu .
|
|
89
|
+
docker run -p 5000:5000 pynode-cpu
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Accessing the Application
|
|
93
|
+
|
|
94
|
+
Once running, access the web interface at:
|
|
95
|
+
- http://localhost:5000
|
|
96
|
+
|
|
97
|
+
## mDNS / Service Discovery
|
|
98
|
+
|
|
99
|
+
### The Limitation
|
|
100
|
+
|
|
101
|
+
The default `docker-compose.yml` uses Docker's standard bridge network. mDNS
|
|
102
|
+
(multicast DNS) relies on link-local multicast traffic (UDP `224.0.0.251:5353`),
|
|
103
|
+
which the bridge network's NAT does not forward in either direction. With the
|
|
104
|
+
default compose file, PyNode's mDNS nodes can broadcast and listen *inside*
|
|
105
|
+
the container, but that traffic never reaches the LAN — so other machines on
|
|
106
|
+
your network cannot discover the container, and the container cannot discover
|
|
107
|
+
services running elsewhere.
|
|
108
|
+
|
|
109
|
+
### `HOST_IP`
|
|
110
|
+
|
|
111
|
+
The `HOST_IP` environment variable (see `docker-compose.yml`) only controls
|
|
112
|
+
the IP address **advertised** in mDNS service records - it does *not* make
|
|
113
|
+
multicast traffic reach the LAN. Setting `HOST_IP` without one of the fixes
|
|
114
|
+
below just means the (unreachable) mDNS records will advertise the right
|
|
115
|
+
address instead of the container's internal one; it does not fix discovery.
|
|
116
|
+
|
|
117
|
+
### Fixing It
|
|
118
|
+
|
|
119
|
+
**Option 1: `network_mode: host` (Linux hosts only, full fix)**
|
|
120
|
+
|
|
121
|
+
On Linux, host networking removes the network isolation between the
|
|
122
|
+
container and the host, so multicast works exactly as it would for a
|
|
123
|
+
process running directly on the host:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
services:
|
|
127
|
+
pynode:
|
|
128
|
+
network_mode: host
|
|
129
|
+
# ports: mapping is ignored in host mode - the container shares the
|
|
130
|
+
# host's network stack directly, so remove/comment out `ports:` above.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This does **not** work on Docker Desktop for Windows/macOS - Docker Desktop
|
|
134
|
+
runs containers inside a lightweight VM, and `network_mode: host` there still
|
|
135
|
+
does not bridge multicast across the VM boundary.
|
|
136
|
+
|
|
137
|
+
**Option 2: macvlan network**
|
|
138
|
+
|
|
139
|
+
A macvlan network gives the container its own IP address directly on the LAN
|
|
140
|
+
(as if it were a separate physical device), which also allows multicast to
|
|
141
|
+
work normally. This works on Docker Desktop as well as Linux, but requires
|
|
142
|
+
more setup (a dedicated IP, and typically a way for the host itself to reach
|
|
143
|
+
the container since macvlan interfaces are not reachable from the host by
|
|
144
|
+
default). See the
|
|
145
|
+
[Docker macvlan network documentation](https://docs.docker.com/network/drivers/macvlan/)
|
|
146
|
+
for setup details.
|
|
147
|
+
|
|
148
|
+
**Option 3: mDNS reflector on the host**
|
|
149
|
+
|
|
150
|
+
When host networking isn't available (e.g. Docker Desktop), run an mDNS
|
|
151
|
+
reflector on the host to bridge multicast between the host's network and the
|
|
152
|
+
container's bridge network:
|
|
153
|
+
|
|
154
|
+
- **avahi-daemon** (Linux) - enable reflection in `/etc/avahi/avahi-daemon.conf`:
|
|
155
|
+
```ini
|
|
156
|
+
[reflector]
|
|
157
|
+
enable-reflector=yes
|
|
158
|
+
```
|
|
159
|
+
- **mdns-repeater** - run as a sidecar/host process that repeats mDNS packets
|
|
160
|
+
between interfaces.
|
|
161
|
+
|
|
162
|
+
This is the option that works when host networking isn't an option, at the
|
|
163
|
+
cost of an extra moving part to run and maintain.
|
|
164
|
+
|
|
165
|
+
### Port Matching
|
|
166
|
+
|
|
167
|
+
Whichever option you use, the port PyNode advertises in its mDNS records
|
|
168
|
+
must match the port other machines can actually reach it on (the *published*
|
|
169
|
+
port), not just the container's internal port. If you remap ports (e.g.
|
|
170
|
+
`"8080:5000"`), make sure discovery consumers connect using the published
|
|
171
|
+
port (`8080`), not the internal one.
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
### GPU Not Detected
|
|
176
|
+
|
|
177
|
+
Verify NVIDIA Container Toolkit is installed:
|
|
178
|
+
```bash
|
|
179
|
+
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Permission Issues
|
|
183
|
+
|
|
184
|
+
Ensure the mounted directories have proper permissions:
|
|
185
|
+
```bash
|
|
186
|
+
mkdir -p workflows logs
|
|
187
|
+
chmod 777 workflows logs
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Rebuild from Scratch
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
docker-compose down
|
|
194
|
+
docker-compose build --no-cache
|
|
195
|
+
docker-compose up
|
|
196
|
+
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM docker.io/nvidia/cuda:12.6.0-
|
|
1
|
+
FROM docker.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04
|
|
2
2
|
|
|
3
3
|
# Set environment variables
|
|
4
4
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
@@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y \
|
|
|
12
12
|
python3-venv \
|
|
13
13
|
git \
|
|
14
14
|
wget \
|
|
15
|
+
gosu \
|
|
15
16
|
gfortran \
|
|
16
17
|
libgl1-mesa-glx \
|
|
17
18
|
libgtk-3-0 \
|
|
@@ -44,16 +45,29 @@ WORKDIR /app
|
|
|
44
45
|
# Copy application files
|
|
45
46
|
COPY . /app/
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
# Derive the display version from git history, then remove .git to save
|
|
49
|
+
# space. SETUPTOOLS_SCM_PRETEND_VERSION keeps pip install happy (PEP 440);
|
|
50
|
+
# the real version string is written to _version.py afterwards.
|
|
51
|
+
RUN chmod +x /app/setup.sh /app/install_nodes.sh && \
|
|
52
|
+
RAW=$(git -C /app describe --tags --always 2>/dev/null || true) && \
|
|
53
|
+
if echo "$RAW" | grep -qE '^v?[0-9]+\.[0-9]+'; then \
|
|
54
|
+
VERSION=$(echo "$RAW" | sed 's/^v//'); \
|
|
55
|
+
elif [ -n "$RAW" ]; then \
|
|
56
|
+
VERSION="0.0.0+g${RAW}"; \
|
|
57
|
+
else \
|
|
58
|
+
VERSION="0.0.0"; \
|
|
59
|
+
fi && \
|
|
60
|
+
rm -rf /app/.git && \
|
|
61
|
+
SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 bash -c "source /app/setup.sh < /dev/null" && \
|
|
62
|
+
printf '__version__ = "%s"\n' "$VERSION" > /app/pynode/_version.py
|
|
52
63
|
|
|
53
64
|
# Create a non-root user and give it ownership of the app directory
|
|
54
65
|
RUN useradd --create-home --uid 1000 appuser \
|
|
55
66
|
&& chown -R appuser:appuser /app
|
|
56
|
-
|
|
67
|
+
|
|
68
|
+
# Entrypoint ensures bind-mounted directories are writable by appuser
|
|
69
|
+
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
|
|
70
|
+
RUN chmod +x /app/docker-entrypoint.sh
|
|
57
71
|
|
|
58
72
|
# Expose port
|
|
59
73
|
EXPOSE 5000
|
|
@@ -62,5 +76,5 @@ EXPOSE 5000
|
|
|
62
76
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
|
63
77
|
CMD ["/app/appenv/bin/python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"]
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
|
66
80
|
CMD ["/app/appenv/bin/python", "-m", "pynode", "--production", "--host", "0.0.0.0", "--port", "5000"]
|
|
@@ -6,6 +6,7 @@ ENV PYTHONUNBUFFERED=1
|
|
|
6
6
|
|
|
7
7
|
# Install system dependencies
|
|
8
8
|
RUN apt-get update && apt-get install -y \
|
|
9
|
+
gosu \
|
|
9
10
|
libgl1-mesa-glx \
|
|
10
11
|
libglib2.0-0 \
|
|
11
12
|
libsm6 \
|
|
@@ -19,15 +20,24 @@ WORKDIR /app
|
|
|
19
20
|
# Copy application files
|
|
20
21
|
COPY . /app/
|
|
21
22
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
# Derive the display version from git history, then remove .git to save
|
|
24
|
+
# space. SETUPTOOLS_SCM_PRETEND_VERSION keeps pip install happy (PEP 440);
|
|
25
|
+
# the real version string is written to _version.py afterwards.
|
|
26
|
+
RUN RAW=$(git describe --tags --always 2>/dev/null || true) && \
|
|
27
|
+
if echo "$RAW" | grep -qE '^v?[0-9]+\.[0-9]+'; then \
|
|
28
|
+
VERSION=$(echo "$RAW" | sed 's/^v//'); \
|
|
29
|
+
elif [ -n "$RAW" ]; then \
|
|
30
|
+
VERSION="0.0.0+g${RAW}"; \
|
|
31
|
+
else \
|
|
32
|
+
VERSION="0.0.0"; \
|
|
33
|
+
fi && \
|
|
34
|
+
rm -rf /app/.git && \
|
|
35
|
+
python3 -m venv appenv && \
|
|
36
|
+
. appenv/bin/activate && \
|
|
28
37
|
pip install --upgrade pip && \
|
|
29
38
|
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu && \
|
|
30
|
-
pip install -e ".[vision,mqtt]"
|
|
39
|
+
SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 pip install -e ".[vision,mqtt]" && \
|
|
40
|
+
printf '__version__ = "%s"\n' "$VERSION" > /app/pynode/_version.py
|
|
31
41
|
|
|
32
42
|
# Install per-node dependencies (best-effort: a node whose vendor SDK is not
|
|
33
43
|
# on PyPI, e.g. OmronCameraNode's stapipy, is skipped without failing the build)
|
|
@@ -36,7 +46,10 @@ RUN . appenv/bin/activate && pynode-install-nodes || true
|
|
|
36
46
|
# Create a non-root user and give it ownership of the app directory
|
|
37
47
|
RUN useradd --create-home --uid 1000 appuser \
|
|
38
48
|
&& chown -R appuser:appuser /app
|
|
39
|
-
|
|
49
|
+
|
|
50
|
+
# Entrypoint ensures bind-mounted directories are writable by appuser
|
|
51
|
+
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
|
|
52
|
+
RUN chmod +x /app/docker-entrypoint.sh
|
|
40
53
|
|
|
41
54
|
# Expose port
|
|
42
55
|
EXPOSE 5000
|
|
@@ -45,5 +58,5 @@ EXPOSE 5000
|
|
|
45
58
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
|
46
59
|
CMD ["/app/appenv/bin/python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"]
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
|
49
62
|
CMD ["/app/appenv/bin/python", "-m", "pynode", "--production", "--host", "0.0.0.0", "--port", "5000"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pynode-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Python based Node-RED-like visual workflow system
|
|
5
5
|
Author: olkham
|
|
6
6
|
License-Expression: MIT
|
|
@@ -214,6 +214,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
|
|
|
214
214
|
|
|
215
215
|
The resolved location is logged at startup (`Workflow data directory: ...`).
|
|
216
216
|
|
|
217
|
+
### Models Directory
|
|
218
|
+
|
|
219
|
+
Nodes that download or generate model weights (e.g. the YOLO node's `.pt`
|
|
220
|
+
files and exported OpenVINO models) write them into a shared **models
|
|
221
|
+
directory** instead of the process working directory. It is resolved in this
|
|
222
|
+
order:
|
|
223
|
+
|
|
224
|
+
1. `pynode --models-dir <path>` CLI flag,
|
|
225
|
+
2. `PYNODE_MODELS_DIR` environment variable,
|
|
226
|
+
3. `<data dir>/models` otherwise — so `<repo>/models` for a source checkout and
|
|
227
|
+
`~/.pynode/models` for a regular `pip install`.
|
|
228
|
+
|
|
229
|
+
> **Upgrading:** older PyNode versions downloaded model files into whatever the
|
|
230
|
+
> working directory happened to be, so stray `.pt` files and
|
|
231
|
+
> `*_openvino_model/` folders may exist in the repo root, `pynode/models/` or
|
|
232
|
+
> `pynode/nodes/`. PyNode still reads models from those legacy locations, but
|
|
233
|
+
> nothing is migrated automatically — you can move them into the models
|
|
234
|
+
> directory manually when convenient.
|
|
235
|
+
|
|
217
236
|
## Securing PyNode
|
|
218
237
|
|
|
219
238
|
**PyNode executes arbitrary Python by design** (e.g. FunctionNode runs whatever code is in the workflow), so anyone who can reach the API can run code on the host. Authentication is the trust boundary — secure the server before exposing it beyond your own machine:
|
|
@@ -149,6 +149,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
|
|
|
149
149
|
|
|
150
150
|
The resolved location is logged at startup (`Workflow data directory: ...`).
|
|
151
151
|
|
|
152
|
+
### Models Directory
|
|
153
|
+
|
|
154
|
+
Nodes that download or generate model weights (e.g. the YOLO node's `.pt`
|
|
155
|
+
files and exported OpenVINO models) write them into a shared **models
|
|
156
|
+
directory** instead of the process working directory. It is resolved in this
|
|
157
|
+
order:
|
|
158
|
+
|
|
159
|
+
1. `pynode --models-dir <path>` CLI flag,
|
|
160
|
+
2. `PYNODE_MODELS_DIR` environment variable,
|
|
161
|
+
3. `<data dir>/models` otherwise — so `<repo>/models` for a source checkout and
|
|
162
|
+
`~/.pynode/models` for a regular `pip install`.
|
|
163
|
+
|
|
164
|
+
> **Upgrading:** older PyNode versions downloaded model files into whatever the
|
|
165
|
+
> working directory happened to be, so stray `.pt` files and
|
|
166
|
+
> `*_openvino_model/` folders may exist in the repo root, `pynode/models/` or
|
|
167
|
+
> `pynode/nodes/`. PyNode still reads models from those legacy locations, but
|
|
168
|
+
> nothing is migrated automatically — you can move them into the models
|
|
169
|
+
> directory manually when convenient.
|
|
170
|
+
|
|
152
171
|
## Securing PyNode
|
|
153
172
|
|
|
154
173
|
**PyNode executes arbitrary Python by design** (e.g. FunctionNode runs whatever code is in the workflow), so anyone who can reach the API can run code on the host. Authentication is the trust boundary — secure the server before exposing it beyond your own machine:
|
|
@@ -5,7 +5,12 @@ services:
|
|
|
5
5
|
dockerfile: Dockerfile
|
|
6
6
|
container_name: pynode
|
|
7
7
|
ports:
|
|
8
|
-
- "
|
|
8
|
+
- "5000:5000"
|
|
9
|
+
# Uncomment for mDNS support on Linux hosts (see DOCKER.md > mDNS /
|
|
10
|
+
# Service Discovery). Not effective on Docker Desktop for Windows/macOS.
|
|
11
|
+
# Remove the `ports:` mapping above when enabling this - it is ignored
|
|
12
|
+
# in host networking mode.
|
|
13
|
+
# network_mode: host
|
|
9
14
|
volumes:
|
|
10
15
|
# Mount persistent data
|
|
11
16
|
- ./workflows:/app/workflows
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Ensure bind-mounted directories are writable by appuser (uid 1000).
|
|
5
|
+
# docker-compose mounts host directories that may be owned by root.
|
|
6
|
+
for dir in /app/workflows /app/workflows/services /app/logs; do
|
|
7
|
+
mkdir -p "$dir"
|
|
8
|
+
chown -R 1000:1000 "$dir" 2>/dev/null || true
|
|
9
|
+
done
|
|
10
|
+
|
|
11
|
+
# Drop privileges and exec the CMD
|
|
12
|
+
exec gosu appuser "$@"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# PyNode Project Review Plan — 2026-07-18
|
|
2
|
+
|
|
3
|
+
Follow-up to the July 2026 overhaul (see `plan-pynodeRepoReview.prompt.md`). This plan captures
|
|
4
|
+
the full findings of the 2026-07-18 project review. Phases A–D are being executed now;
|
|
5
|
+
E–H are deferred and can be picked up in any later session.
|
|
6
|
+
|
|
7
|
+
Status legend: `[NOW]` in progress this session · `[LATER]` deferred · `[DECIDE]` needs a user decision.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Phase A [NOW] — Quick bug fixes + compose port
|
|
12
|
+
|
|
13
|
+
1. **Dead `include_predictions` toggle** — `pynode/nodes/UltralyticsNode/ultralytics_node.py:348`
|
|
14
|
+
reads `if include_predictions or True:` so the "Include Predictions in Output" checkbox does
|
|
15
|
+
nothing. Honor the flag (use `get_config_bool` for string-safety; same for `include_image`).
|
|
16
|
+
2. **docker-compose port typo** — `docker-compose.yml` publishes `"500:5000"`; DOCKER.md documents
|
|
17
|
+
port 5000. Change to `"5000:5000"`.
|
|
18
|
+
3. **Silent node-import failures** — `pynode/nodes/__init__.py` `_try_import_python_files` swallows
|
|
19
|
+
every exception with bare `continue`; a custom node with a syntax error vanishes from the palette
|
|
20
|
+
with no log output. Log warnings (missing optional deps may stay at debug level).
|
|
21
|
+
4. Minor cleanups: dead `system_error_node` variable in `workflow_engine.py:367`; ImageViewerNode
|
|
22
|
+
MJPEG generator loops forever after node stop (holds a waitress thread); ImageViewerNode's shared
|
|
23
|
+
`last_sent_timestamp` makes the SSE broadcaster and the `/frame` polling route steal frames from
|
|
24
|
+
each other; `socket.inet_ntoa` in `mdns_manager.py` throws on IPv6 service records.
|
|
25
|
+
|
|
26
|
+
Acceptance: full test suite passes; toggling "Include Predictions" actually changes output;
|
|
27
|
+
node import errors appear in the log.
|
|
28
|
+
|
|
29
|
+
## Phase B [NOW] — Model / node storage directories
|
|
30
|
+
|
|
31
|
+
Problem: `YOLO('yolo26n.pt')` downloads weights into the process CWD and `export(format='openvino')`
|
|
32
|
+
writes next to the source; strays currently exist in 4 places (repo root, `models/`,
|
|
33
|
+
`pynode/models/`, `pynode/nodes/`). A pip-installed PyNode launched from inside a venv would scatter
|
|
34
|
+
files there. Also `[tool.setuptools.package-data] nodes/**/*` would bake a stray `.pt` in
|
|
35
|
+
`pynode/nodes/` into a locally built wheel.
|
|
36
|
+
|
|
37
|
+
1. `pynode/config.py`: add `PYNODE_MODELS_DIR` env var + `resolve_models_dir()` → default
|
|
38
|
+
`<data_dir>/models/`; `--models-dir` CLI flag in `main.py` (mirror the `--data-dir` pattern).
|
|
39
|
+
2. `BaseNode.get_storage_dir(subdir=None)` → `<data_dir>/node_storage/<NodeType>/`, created lazily.
|
|
40
|
+
General rule going forward: nodes never write binaries outside their storage dir.
|
|
41
|
+
3. UltralyticsNode: resolve bare model names to an absolute path under the models dir before calling
|
|
42
|
+
`YOLO()` (Ultralytics downloads named assets to an absolute path; the OpenVINO export then lands
|
|
43
|
+
in the same folder). Check legacy locations first to avoid re-downloading; never move/delete
|
|
44
|
+
existing user files automatically (`pynode/models/` holds real user models).
|
|
45
|
+
4. Packaging: `[tool.setuptools.exclude-package-data]` for `*.pt` / `*_openvino_model` so strays
|
|
46
|
+
can't enter wheels.
|
|
47
|
+
5. Document (README/INSTALL): env var, CLI flag, where models live per install type.
|
|
48
|
+
|
|
49
|
+
Deferred idea: settings UI for the models folder.
|
|
50
|
+
|
|
51
|
+
## Phase C [NOW] — mDNS in Docker: fix + docs
|
|
52
|
+
|
|
53
|
+
Root cause: default bridge network; mDNS is link-local multicast (224.0.0.251:5353) which Docker's
|
|
54
|
+
bridge/NAT does not forward. `HOST_IP` only fixes the advertised address, not reachability.
|
|
55
|
+
|
|
56
|
+
1. DOCKER.md: new "mDNS / service discovery" section — explain the limitation; document `HOST_IP`
|
|
57
|
+
(currently undocumented); give options in order: `network_mode: host` (Linux; ports mapping
|
|
58
|
+
ignored; does NOT help on Docker Desktop Win/Mac), macvlan, host-side avahi reflector
|
|
59
|
+
(`enable-reflector=yes`) or mdns-repeater sidecar.
|
|
60
|
+
2. docker-compose.yml: commented-out `network_mode: host` alternative block with the caveats.
|
|
61
|
+
3. Advertised `service_port` must match the externally reachable port (relates to A2).
|
|
62
|
+
|
|
63
|
+
## Phase D [NOW] — send() copy semantics (biggest perf win)
|
|
64
|
+
|
|
65
|
+
`BaseNode.send()` deep-copies the message per recipient (`base_node.py:254`); raw numpy frames make
|
|
66
|
+
this ~6 MB memcpy per hop per recipient. Adopt Node-RED semantics: **last recipient gets the
|
|
67
|
+
original message, only additional wires get deep copies** — zero frame copies in the common
|
|
68
|
+
single-connection case. Contract change: *senders must not mutate a message after calling send().*
|
|
69
|
+
|
|
70
|
+
1. Audit every node for post-`send()` mutation and source nodes for numpy buffer reuse; fix
|
|
71
|
+
offenders to create fresh messages instead.
|
|
72
|
+
2. Implement last-recipient-no-copy in both the queued path and the sink "direct processing" path.
|
|
73
|
+
3. Update/adjust tests that assert sender-side isolation; document the contract in base_node.py.
|
|
74
|
+
|
|
75
|
+
## Phase E [LATER] — Remaining performance items
|
|
76
|
+
|
|
77
|
+
- Sink "direct processing" runs on the sender's thread (ImageViewer JPEG encode inside the camera
|
|
78
|
+
loop) and skips `drop_while_busy`; route sinks through the normal queue or apply drop checks.
|
|
79
|
+
- Idle worker threads busy-poll every 10–100 ms (`base_node.py:296`); switch to blocking
|
|
80
|
+
`queue.get()` with a `None` sentinel pushed in `on_stop()`.
|
|
81
|
+
- `sort_msg_keys` runs on every send per recipient; sort at display time in DebugNode instead.
|
|
82
|
+
- Waitress `threads=10` (`main.py:103`): each SSE client and MJPEG viewer permanently holds a
|
|
83
|
+
thread → silent API starvation. Make threads a CLI flag, default higher (~24), document.
|
|
84
|
+
- Dockerfile uses `cuda:12.6.0-devel` (~8 GB base); switch to `-runtime` or multi-stage.
|
|
85
|
+
|
|
86
|
+
## Phase F [LATER] — UI/UX
|
|
87
|
+
|
|
88
|
+
- Canvas node search (Ctrl+F: find by name/type, pan to node).
|
|
89
|
+
- Per-node runtime status text on canvas (Node-RED `status()` equivalent: "model loaded", fps,
|
|
90
|
+
drop count) — SSE plumbing (`sse_handlers`) already exists.
|
|
91
|
+
- Palette manager UI (falls out of Phase G).
|
|
92
|
+
- Light theme (style.css is dark-only; tokenize colors to CSS variables first).
|
|
93
|
+
- Groups/subflows — biggest structural gap vs Node-RED; affects workflow JSON schema, decide
|
|
94
|
+
deliberately before other schema work.
|
|
95
|
+
|
|
96
|
+
## Phase G [LATER] — Live node install from an approved registry
|
|
97
|
+
|
|
98
|
+
Constraints today: registries built once at import (`node_registry.py:154-156`); node `api_routes`
|
|
99
|
+
registered as real Flask routes at app creation (`api/nodes.py:418`, frozen after first request).
|
|
100
|
+
|
|
101
|
+
1. **Registry**: curated JSON index in a separate GitHub repo (`pynode-registry`), served raw:
|
|
102
|
+
name, description, pip requirement/git URL, version, author, category, `verified` flag.
|
|
103
|
+
Approval = PR review.
|
|
104
|
+
2. **External nodes dir**: also scan `<data_dir>/nodes/` (Node-RED `~/.node-red` pattern) —
|
|
105
|
+
cheap, enables "drop a folder in, restart". Later: `[project.entry-points."pynode.nodes"]`.
|
|
106
|
+
3. **API**: `GET /api/palette/catalog` (cached), `POST /api/palette/install` (pip via
|
|
107
|
+
`sys.executable -m pip`, reuse install_nodes.py pattern), `DELETE` for removal.
|
|
108
|
+
4. **Activation v1**: install + "restart to activate" banner.
|
|
109
|
+
**v2 (hot reload)**: make cache builders re-runnable, `palette-updated` SSE event, convert
|
|
110
|
+
per-node API routes to one catch-all `/api/nodes/<node_id>/<path:sub>` dispatching through the
|
|
111
|
+
registry dict at request time (removes the Flask route-freeze problem).
|
|
112
|
+
5. **Security**: only names from the curated index, pinned versions, behind the API key. Never
|
|
113
|
+
accept arbitrary pip specs from the UI.
|
|
114
|
+
|
|
115
|
+
## Phase H [DECIDE] — Naming
|
|
116
|
+
|
|
117
|
+
- **GitHub repo rename** `olkham/pynode` → `olkham/pynode-flow`: recommended; cheap, GitHub
|
|
118
|
+
redirects, removes PyPI/repo mismatch. Update `[project.urls]`, README.
|
|
119
|
+
- **Import package rename** (`pynode` → `pynode_flow`): argument for = the unrelated PyPI `pynode`
|
|
120
|
+
0.1.0 namespace-collision trap (see 2026-07-18 incident); argument against = breaks all imports
|
|
121
|
+
and custom nodes; mismatched dist/import names are normal (opencv-python/cv2).
|
|
122
|
+
Recommendation: keep `pynode` import; add a startup guard that detects the conflicting `pynode`
|
|
123
|
+
dist (importlib.metadata) and fails loudly with "uninstall the unrelated pynode package".
|
|
124
|
+
If ever renaming, the pre-1.0 window is the only sane time.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Execution notes (any session picking this up)
|
|
129
|
+
|
|
130
|
+
- User commits phases themselves; do not commit.
|
|
131
|
+
- Test safety (hard rules, see memory `pynode-test-safety-incident`): tests must build apps via
|
|
132
|
+
`create_app` with tmp paths (tests/conftest.py `api_client`); join every thread before fixture
|
|
133
|
+
teardown; after test work verify `git status --porcelain -- workflows/` is empty. A live PyNode
|
|
134
|
+
server may be running against this repo — its own saves to workflows/ are not test leakage.
|
|
135
|
+
- Suite size at time of writing: 306 tests.
|
|
@@ -21,7 +21,11 @@ def _resolve_version() -> str:
|
|
|
21
21
|
try:
|
|
22
22
|
from importlib.metadata import PackageNotFoundError, version
|
|
23
23
|
try:
|
|
24
|
-
|
|
24
|
+
v = version("pynode-flow")
|
|
25
|
+
# 0.0.0 is the placeholder used during Docker builds; skip it so
|
|
26
|
+
# the real version written to _version.py is preferred.
|
|
27
|
+
if v != "0.0.0":
|
|
28
|
+
return v
|
|
25
29
|
except PackageNotFoundError:
|
|
26
30
|
pass
|
|
27
31
|
except Exception:
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 4)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'ga1c7a9d87'
|
|
@@ -349,6 +349,7 @@ def _register_json_route(route, methods, handler_name, endpoint_name):
|
|
|
349
349
|
_, method, error = _resolve_node_handler(node_id, handler_name)
|
|
350
350
|
if error:
|
|
351
351
|
return error
|
|
352
|
+
assert method is not None
|
|
352
353
|
try:
|
|
353
354
|
result = method()
|
|
354
355
|
if result is None:
|
|
@@ -369,6 +370,8 @@ def _register_file_upload_route(route, methods, handler_name, endpoint_name, all
|
|
|
369
370
|
payload, status = error
|
|
370
371
|
return jsonify({'success': False, 'error': payload.get_json()['error']}), status
|
|
371
372
|
|
|
373
|
+
assert method is not None
|
|
374
|
+
|
|
372
375
|
if 'file' not in request.files:
|
|
373
376
|
return jsonify({'success': False, 'error': 'No file provided'}), 400
|
|
374
377
|
|
|
@@ -397,9 +400,10 @@ def _register_stream_route(route, handler_name, endpoint_name):
|
|
|
397
400
|
_, method, error = _resolve_node_handler(node_id, handler_name)
|
|
398
401
|
if error:
|
|
399
402
|
return error
|
|
403
|
+
assert method is not None
|
|
400
404
|
|
|
401
405
|
def generate():
|
|
402
|
-
for img_data in method():
|
|
406
|
+
for img_data in method(): # type: ignore[reportGeneralTypeIssues]
|
|
403
407
|
yield (b'--frame\r\n'
|
|
404
408
|
b'Content-Type: image/jpeg\r\n\r\n' + img_data + b'\r\n')
|
|
405
409
|
|