pynode-flow 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynode_flow-0.2.3/.claude/skills/pynode-workflows/SKILL.md +185 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/.gitignore +2 -0
- pynode_flow-0.2.3/DOCKER.md +189 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/INSTALL.md +10 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/PKG-INFO +36 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/README.md +35 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/docker-compose.yml +6 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/examples/README.md +5 -0
- pynode_flow-0.2.3/plan-pynodeReview-2026-07-18.prompt.md +135 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/_version.py +3 -3
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/helpers.py +9 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/nodes.py +22 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/services.py +40 -52
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/workflows.py +54 -16
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/config.py +42 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/main.py +9 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/camera_node.py +120 -45
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/debug_node.py +9 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DelayNode/delay_node.py +75 -37
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/image_viewer_node.py +16 -9
- pynode_flow-0.2.3/pynode/nodes/LinkNode/__init__.py +13 -0
- pynode_flow-0.2.3/pynode/nodes/LinkNode/link_bus.py +110 -0
- pynode_flow-0.2.3/pynode/nodes/LinkNode/link_in_node.py +104 -0
- pynode_flow-0.2.3/pynode/nodes/LinkNode/link_out_node.py +88 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_service.py +94 -30
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/omron_camera_node.py +13 -3
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/rate_probe_node.py +37 -29
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SwitchNode/switch_node.py +8 -2
- pynode_flow-0.2.3/pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode_flow-0.2.3/pynode/nodes/UltralyticsNode/__init__.py +11 -0
- pynode_flow-0.2.3/pynode/nodes/UltralyticsNode/model_paths.py +115 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/UltralyticsNode/ultralytics_node.py +15 -9
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/VideoReaderNode/video_reader_node.py +43 -22
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/__init__.py +6 -5
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/base_node.py +160 -52
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_manager.py +28 -3
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/messages.py +1 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/server.py +17 -0
- pynode_flow-0.2.3/pynode/static/examples/01-hello-world.json +33 -0
- pynode_flow-0.2.3/pynode/static/examples/02-function-transform.json +45 -0
- pynode_flow-0.2.3/pynode/static/examples/03-switch-routing.json +80 -0
- pynode_flow-0.2.3/pynode/static/examples/04-flow-control.json +76 -0
- pynode_flow-0.2.3/pynode/static/examples/05-webcam-viewer.json +35 -0
- pynode_flow-0.2.3/pynode/static/examples/06-webcam-yolo.json +75 -0
- pynode_flow-0.2.3/pynode/static/examples/07-blur-people.json +95 -0
- pynode_flow-0.2.3/pynode/static/examples/08-video-yolo-recorder.json +73 -0
- pynode_flow-0.2.3/pynode/static/examples/09-mqtt-pub-sub.json +56 -0
- pynode_flow-0.2.3/pynode/static/examples/10-person-alert-mqtt.json +134 -0
- pynode_flow-0.2.3/pynode/static/examples/README.md +65 -0
- pynode_flow-0.2.3/pynode/static/examples/manifest.json +74 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/index.html +80 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/debug.js +280 -58
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/events.js +72 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/main.js +5 -1
- pynode_flow-0.2.3/pynode/static/js/mqtt-services.js +346 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/nodes.js +9 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/properties.js +90 -283
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/state.js +13 -4
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/workflow.js +121 -60
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/workflows.js +192 -89
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/style.css +227 -32
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/workflow_engine.py +7 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/workflow_manager.py +4 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/PKG-INFO +36 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/SOURCES.txt +28 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/scm_file_list.json +28 -0
- pynode_flow-0.2.3/pynode_flow.egg-info/scm_version.json +8 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pyproject.toml +7 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_api_nodes.py +49 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_api_workflows.py +83 -0
- pynode_flow-0.2.3/tests/test_camera_node_backend.py +119 -0
- pynode_flow-0.2.3/tests/test_debug_workflow_scope.py +122 -0
- pynode_flow-0.2.3/tests/test_delay_rate_limit.py +296 -0
- pynode_flow-0.2.3/tests/test_link_nodes.py +208 -0
- pynode_flow-0.2.3/tests/test_models_dir.py +230 -0
- pynode_flow-0.2.3/tests/test_mqtt_services.py +158 -0
- pynode_flow-0.2.3/tests/test_send_copy_semantics.py +226 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_video_reader_node.py +62 -6
- pynode_flow-0.2.1/DOCKER.md +0 -113
- pynode_flow-0.2.1/pynode/nodes/UltralyticsNode/__init__.py +0 -4
- pynode_flow-0.2.1/pynode_flow.egg-info/scm_version.json +0 -8
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/.dockerignore +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/Dockerfile +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/Dockerfile.cpu +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/LICENSE +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/MANIFEST.in +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/install_nodes.bat +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/install_nodes.sh +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/plan-pynodeRepoReview.prompt.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/sse.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/node_registry.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/change_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CropNode/crop_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DelayNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ErrorNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/function_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/GateNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/inference_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/InjectNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1/pynode/nodes/RangeNode → pynode_flow-0.2.3/pynode/nodes/LinkNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/colormap_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/fft_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/histogram_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/in_range_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/perspective_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/range_node.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/RateProbeNode → pynode_flow-0.2.3/pynode/nodes/RangeNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/SplitNode → pynode_flow-0.2.3/pynode/nodes/RateProbeNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/SwitchNode → pynode_flow-0.2.3/pynode/nodes/SplitNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/split_node.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/TemplateNode → pynode_flow-0.2.3/pynode/nodes/SwitchNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/image_utils.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/info.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/config.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/ui-utils.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/setup.bat +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/setup.cfg +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/setup.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/setup.sh +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/conftest.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_config.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_device_detection.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_node_types_api.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.3}/tests/test_workflow_roundtrip.py +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pynode-workflows
|
|
3
|
+
description: Build, edit and validate PyNode workflow JSON files (nodes + connections) that users can import via the GUI or REST API. Use when asked to create example flows, generate a workflow, wire nodes together, or debug a workflow definition.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building PyNode Workflows
|
|
7
|
+
|
|
8
|
+
A PyNode workflow is a JSON document describing nodes and the connections
|
|
9
|
+
between them. The GUI's **Import** button (hamburger menu) accepts a flat
|
|
10
|
+
workflow file and loads it into a **new tab** named after the file. The same
|
|
11
|
+
document can be POSTed to `/api/workflow`.
|
|
12
|
+
|
|
13
|
+
## Workflow file format
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"nodes": [
|
|
18
|
+
{
|
|
19
|
+
"id": "inject_1",
|
|
20
|
+
"type": "InjectNode",
|
|
21
|
+
"name": "Every 2s",
|
|
22
|
+
"config": { "props": [{"property": "payload", "value": "hi", "valueType": "str"}], "repeat": "2" },
|
|
23
|
+
"x": 80, "y": 80
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"connections": [
|
|
27
|
+
{ "source": "inject_1", "target": "debug_1", "sourceOutput": 0, "targetInput": 0 }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Rules:
|
|
33
|
+
- `type` is the **Python class name** of the node (e.g. `InjectNode`, `MqttOutNode`).
|
|
34
|
+
Unknown types import as disabled placeholder nodes — always validate.
|
|
35
|
+
- `id` is any unique string. Use readable slugs (`camera_1`, `yolo_1`).
|
|
36
|
+
- `enabled` is optional (default `true`). `x`/`y` are canvas pixels; space
|
|
37
|
+
columns ~260px apart (80, 340, 600, 860, 1120), rows ~140px apart.
|
|
38
|
+
- `sourceOutput`/`targetInput` are 0-based port indexes.
|
|
39
|
+
- Config values are lenient: many nodes read numbers from strings
|
|
40
|
+
(`"confidence": "0.25"`) — copy the conventions below exactly.
|
|
41
|
+
- The on-disk `workflows/workflow.json` uses a different multi-workflow
|
|
42
|
+
wrapper (`{version, activeWorkflow, workflows: [...]}`); **never** write that
|
|
43
|
+
file directly — import through the GUI or API instead.
|
|
44
|
+
|
|
45
|
+
## Message conventions
|
|
46
|
+
|
|
47
|
+
Messages are dicts in Node-RED style: `payload`, `topic`, `_msgid`, plus extras.
|
|
48
|
+
- Images travel at `msg.payload.image` as a numpy BGR array (or JPEG bytes if a
|
|
49
|
+
source has `encode_jpeg: true`). Downstream nodes locate it via their
|
|
50
|
+
`image_path` config, default `payload.image`.
|
|
51
|
+
- Detections travel at `msg.payload.detections`: a list of
|
|
52
|
+
`{"bbox": [x1,y1,x2,y2], "bbox_format": "xyxy", "class_name": str,
|
|
53
|
+
"class_id": int, "confidence": float}`, with `payload.detection_count` and
|
|
54
|
+
`payload.bbox_format` alongside.
|
|
55
|
+
- `MqttOutNode` publishes `msg.payload`; **`msg.topic` overrides the node's
|
|
56
|
+
configured topic** — don't set a `topic` in InjectNode when a downstream
|
|
57
|
+
MQTT-out node should control the topic.
|
|
58
|
+
|
|
59
|
+
## Node catalog (config keys verified against source)
|
|
60
|
+
|
|
61
|
+
### Core / logic
|
|
62
|
+
| Type | Ports in→out | Config (defaults) |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `InjectNode` | 0→1 | `props`: list of `{property: "payload", value, valueType: str\|num\|bool\|json\|date\|env}` (dot paths OK, e.g. `payload.count`); `topic`: str; `once`: seconds (str, empty=off); `repeat`: seconds (str, empty=off). Manual trigger button built in. |
|
|
65
|
+
| `DebugNode` | 1→0 | `console`: bool; `complete`: `"payload"` (default) or `"msg"` for whole message; `drop_messages`: bool |
|
|
66
|
+
| `FunctionNode` | 1→N | `func`: Python source (body of a function receiving `msg`, `node`, `time`; must `return msg` / a list for multiple outputs / `None` to drop); `outputs`: int (default 1). Full builtins and `import` are available inside the body. |
|
|
67
|
+
| `SwitchNode` | 1→N | `property`: dot path (`payload`, `payload.count`); `checkall`: bool; `rules`: list of `{operator, value, valueType}` — **rule index = output index**. Operators: `eq neq lt lte gt gte between contains matches true false null nnull else`. |
|
|
68
|
+
| `ChangeNode` | 1→1 | `rules`: list of `{type: set\|delete\|change\|move, path: "msg.payload...", value, valueType}` |
|
|
69
|
+
| `GateNode` | 1→1 | `{}` — passes messages only while enabled; toggled live from its card |
|
|
70
|
+
| `LinkOutNode` | 1→0 | `channel`: str — publishes each incoming msg to the named channel on a process-wide bus. Reaches every `LinkInNode` with the same channel, **including in other workflows**. Empty channel = disabled. `drop_messages` defaults `"false"`. |
|
|
71
|
+
| `LinkInNode` | 0→1 | `channel`: str — emits every msg published on the named channel (by any `LinkOutNode`, cross-flow). Deep-copied on delivery (flow isolation). Only receives while its flow is deployed/running and the node is enabled. Empty channel = disabled. |
|
|
72
|
+
| `DelayNode` | 1→1 | `mode`: `delay`\|`delay_count`\|`rate`; `timeout`: secs (delay mode); `rate` + `rate_time`: msgs per secs; `rate_drop`: `drop`\|`queue` |
|
|
73
|
+
| `CounterNode` | 1→1 | `initial_value`: "0"; `increment`: "1"; `retain_payload`: "false" |
|
|
74
|
+
| `RateProbeNode` | 1→1 | `window_size`: seconds (float) — passes msgs through, shows msg/s |
|
|
75
|
+
|
|
76
|
+
### Vision
|
|
77
|
+
| Type | Ports | Config |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `CameraNode` | 0→1 | `device_index`: 0; `fps`: 30; `width`: 640; `height`: 480; `encode_jpeg`: false; `jpeg_quality`: 75 |
|
|
80
|
+
| `VideoReaderNode` | 0→1 | `source`: path (upload via node properties in GUI); `fps`: 0 (=native); `loop`: bool; `jpeg_quality`: 80 |
|
|
81
|
+
| `UltralyticsNode` | 1→1 | `model`: "yolo11n.pt" (auto-downloads); `confidence`: "0.25"; `iou`: "0.45"; `draw_results`: "true"/"false"; `max_det`: "300"; `include_image`: true; `include_predictions`: true; `drop_messages`: "true" (keep on!); `device`: "cpu" (portable) or `cuda:0`/`intel:gpu` |
|
|
82
|
+
| `LabelFilterNode` | 1→2 | `labels`: "person, car" (comma list); `match_mode`: `any`\|`all`; `case_sensitive`: false; `filter_detections`: true (strips non-matching detections); `detections_path`: "payload.detections". **Output 0 = matched, output 1 = unmatched.** |
|
|
83
|
+
| `ConfidenceFilterNode` | 1→2 | `threshold_source`: `manual`\|`msg`; `threshold`: 0.5; `detections_path`: "payload.detections"; `confidence_field`: "confidence". **Output 0 = ≥ threshold, output 1 = < threshold.** |
|
|
84
|
+
| `ImageViewerNode` | 1→0 | `width`: 320; `height`: 240; `image_path`: "payload.image"; `drop_messages`: true |
|
|
85
|
+
| `VideoWriterNode` | 1→0 | `path`: "./output"; `filename`: "video_{counter}"; `codec`: `mp4v`\|`avc1`\|`xvid`...; `framerate`: 30.0; `width`/`height`; `auto_resolution`: bool (match input); `clip_length`: 0 (=unlimited); `naming_mode`: "counter"; `counter_digits`: 4 |
|
|
86
|
+
|
|
87
|
+
Other vision nodes: `DrawPredictionsNode`, `CropNode`, `TrackerNode`,
|
|
88
|
+
`ImageFormatNode`, `SliceNode`, `BBoxMetricsNode`, `PolygonMetricsNode`,
|
|
89
|
+
`InferenceNode` (multi-backend), `FrameSourceNode` (multi-source camera lib) —
|
|
90
|
+
read their `DEFAULT_CONFIG`/`properties` in `pynode/nodes/<Name>/` before use.
|
|
91
|
+
|
|
92
|
+
### Network
|
|
93
|
+
| Type | Ports | Config |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `MqttInNode` | 0→1 | `serviceId`: broker id from mqtt_services.json; `topic`: subscribe topic (wildcards `#`/`+` OK); `qos`: "0" |
|
|
96
|
+
| `MqttOutNode` | 1→0 | `serviceId`; `topic`: publish topic; `qos`: "0"; `retain`: "false" |
|
|
97
|
+
|
|
98
|
+
MQTT brokers are **services** stored in `mqtt_services.json` next to the
|
|
99
|
+
workflow data (managed in the GUI via the node's Broker dropdown → add). A
|
|
100
|
+
workflow referencing an unknown `serviceId` imports fine but the node reports
|
|
101
|
+
"No MQTT broker configured" until the user picks a broker.
|
|
102
|
+
|
|
103
|
+
Also available: `WebhookNode`, `RESTEndpointNode`, `mDNSNode`.
|
|
104
|
+
|
|
105
|
+
## FunctionNode authoring rules
|
|
106
|
+
|
|
107
|
+
The `func` string is the **body** of `def user_function(msg, node, time)` —
|
|
108
|
+
top-level `return` is required to emit. `msg` is a deep copy. Imports work
|
|
109
|
+
inside the body (`import cv2`, `import numpy as np`, `import random`).
|
|
110
|
+
Return a list to fan out across `outputs`; `None` drops the message.
|
|
111
|
+
|
|
112
|
+
Example — blur every detection bbox in-place:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import cv2
|
|
116
|
+
import numpy as np
|
|
117
|
+
p = msg.get('payload') or {}
|
|
118
|
+
img = p.get('image')
|
|
119
|
+
dets = p.get('detections') or []
|
|
120
|
+
if not isinstance(img, np.ndarray):
|
|
121
|
+
return msg
|
|
122
|
+
h, w = img.shape[:2]
|
|
123
|
+
for d in dets:
|
|
124
|
+
bbox = d.get('bbox') or []
|
|
125
|
+
if len(bbox) != 4:
|
|
126
|
+
continue
|
|
127
|
+
x1, y1, x2, y2 = [int(round(v)) for v in bbox]
|
|
128
|
+
x1 = max(0, min(x1, w - 1)); x2 = max(x1 + 1, min(x2, w))
|
|
129
|
+
y1 = max(0, min(y1, h - 1)); y2 = max(y1 + 1, min(y2, h))
|
|
130
|
+
k = max(9, ((x2 - x1) // 4)) | 1
|
|
131
|
+
p['image'][y1:y2, x1:x2] = cv2.GaussianBlur(img[y1:y2, x1:x2], (k, k), 0)
|
|
132
|
+
return msg
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When embedding code in workflow JSON, join lines with `\n` and keep
|
|
136
|
+
indentation exact.
|
|
137
|
+
|
|
138
|
+
## Loading a workflow
|
|
139
|
+
|
|
140
|
+
- **GUI (import)**: hamburger menu → *Import* → choose the `.json` file → it
|
|
141
|
+
opens as a new tab → press *Deploy* to activate.
|
|
142
|
+
- **GUI (bundled examples)**: hamburger menu → *Examples* → pick one. These are
|
|
143
|
+
the files in `pynode/static/examples/`, served statically and listed via
|
|
144
|
+
`pynode/static/examples/manifest.json`. **When adding a new bundled example,
|
|
145
|
+
drop the `.json` in that folder and add an entry to `manifest.json`** (id,
|
|
146
|
+
file, title, description, requires) or it won't appear in the menu.
|
|
147
|
+
- **API**: `POST /api/workflow` with the JSON body imports into the active
|
|
148
|
+
workflow (add `?workflow=<id>` to target another; `X-API-Key` header if the
|
|
149
|
+
server has an API key). `GET /api/workflow` exports the current one —
|
|
150
|
+
building a flow in the GUI and exporting it is the best way to learn configs.
|
|
151
|
+
|
|
152
|
+
## Validating a workflow file (do this for every file you produce)
|
|
153
|
+
|
|
154
|
+
Run a sandboxed import — never against the real server or the repo's
|
|
155
|
+
`workflows/` dir (a leaked test once destroyed real data; see
|
|
156
|
+
tests/conftest.py):
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import json, tempfile, os
|
|
160
|
+
from pynode.server import create_app # use a venv with the vision extras
|
|
161
|
+
|
|
162
|
+
td = tempfile.mkdtemp()
|
|
163
|
+
app = create_app({'WORKFLOWS_DIR': os.path.join(td, 'wf'),
|
|
164
|
+
'WORKFLOW_FILE': os.path.join(td, 'wf', 'workflow.json'),
|
|
165
|
+
'UPLOAD_BASE_DIR': os.path.join(td, 'up'), 'TESTING': True})
|
|
166
|
+
client = app.test_client()
|
|
167
|
+
data = json.load(open('pynode/static/examples/01-hello-world.json'))
|
|
168
|
+
r = client.post('/api/workflow', json=data)
|
|
169
|
+
assert r.status_code == 201, r.get_json()
|
|
170
|
+
manager = app.extensions['workflow_manager']
|
|
171
|
+
with manager.state_lock:
|
|
172
|
+
eng = next(iter(manager.working_engines.values()))
|
|
173
|
+
unknown = [n.type for n in eng.nodes.values() if getattr(n, '_is_unknown_node', False)]
|
|
174
|
+
assert not unknown, f"unknown node types: {unknown}"
|
|
175
|
+
manager.shutdown()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Checklist:
|
|
179
|
+
- every connection's `source`/`target` id exists; port indexes < the node's
|
|
180
|
+
`input_count`/`output_count`
|
|
181
|
+
- node `type` strings match class names exactly (case-sensitive)
|
|
182
|
+
- vision chains keep `drop_messages` on for slow consumers (YOLO, viewers)
|
|
183
|
+
- prefer `device: "cpu"` and auto-downloading models (`yolo11n.pt`) so flows
|
|
184
|
+
run anywhere; note anything machine-specific (camera index, broker IP) in an
|
|
185
|
+
accompanying README
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Docker Setup for PyNode
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Docker Engine with GPU support (NVIDIA Container Toolkit)
|
|
6
|
+
- Docker Compose v1.28+ (for GPU support)
|
|
7
|
+
- NVIDIA GPU with CUDA support
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### Build and Run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
docker-compose up --build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Run in Background
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
docker-compose up -d
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### View Logs
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker-compose logs -f
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Stop the Application
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker-compose down
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
### Ports
|
|
38
|
+
|
|
39
|
+
The application runs on port `5000` by default. To change the port, edit `docker-compose.yml`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
ports:
|
|
43
|
+
- "8080:5000" # Change 8080 to your desired port
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### GPU Support
|
|
47
|
+
|
|
48
|
+
The Docker Compose file is configured to use all available NVIDIA GPUs. To limit GPU access:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
deploy:
|
|
52
|
+
resources:
|
|
53
|
+
reservations:
|
|
54
|
+
devices:
|
|
55
|
+
- driver: nvidia
|
|
56
|
+
device_ids: ['0'] # Use only GPU 0
|
|
57
|
+
capabilities: [gpu]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Persistent Storage
|
|
61
|
+
|
|
62
|
+
The following directories are mounted for persistence:
|
|
63
|
+
- `./workflows` - Saved workflows
|
|
64
|
+
- `./logs` - Application logs
|
|
65
|
+
|
|
66
|
+
## Building
|
|
67
|
+
|
|
68
|
+
### Custom CUDA Version
|
|
69
|
+
|
|
70
|
+
To use a different CUDA version, edit the `Dockerfile`:
|
|
71
|
+
|
|
72
|
+
```dockerfile
|
|
73
|
+
FROM nvidia/cuda:12.8.0-runtime-ubuntu22.04 # Change CUDA version
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Without GPU Support
|
|
77
|
+
|
|
78
|
+
For CPU-only deployment, use the CPU Dockerfile:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
docker build -f Dockerfile.cpu -t pynode-cpu .
|
|
82
|
+
docker run -p 5000:5000 pynode-cpu
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Accessing the Application
|
|
86
|
+
|
|
87
|
+
Once running, access the web interface at:
|
|
88
|
+
- http://localhost:5000
|
|
89
|
+
|
|
90
|
+
## mDNS / Service Discovery
|
|
91
|
+
|
|
92
|
+
### The Limitation
|
|
93
|
+
|
|
94
|
+
The default `docker-compose.yml` uses Docker's standard bridge network. mDNS
|
|
95
|
+
(multicast DNS) relies on link-local multicast traffic (UDP `224.0.0.251:5353`),
|
|
96
|
+
which the bridge network's NAT does not forward in either direction. With the
|
|
97
|
+
default compose file, PyNode's mDNS nodes can broadcast and listen *inside*
|
|
98
|
+
the container, but that traffic never reaches the LAN — so other machines on
|
|
99
|
+
your network cannot discover the container, and the container cannot discover
|
|
100
|
+
services running elsewhere.
|
|
101
|
+
|
|
102
|
+
### `HOST_IP`
|
|
103
|
+
|
|
104
|
+
The `HOST_IP` environment variable (see `docker-compose.yml`) only controls
|
|
105
|
+
the IP address **advertised** in mDNS service records - it does *not* make
|
|
106
|
+
multicast traffic reach the LAN. Setting `HOST_IP` without one of the fixes
|
|
107
|
+
below just means the (unreachable) mDNS records will advertise the right
|
|
108
|
+
address instead of the container's internal one; it does not fix discovery.
|
|
109
|
+
|
|
110
|
+
### Fixing It
|
|
111
|
+
|
|
112
|
+
**Option 1: `network_mode: host` (Linux hosts only, full fix)**
|
|
113
|
+
|
|
114
|
+
On Linux, host networking removes the network isolation between the
|
|
115
|
+
container and the host, so multicast works exactly as it would for a
|
|
116
|
+
process running directly on the host:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
services:
|
|
120
|
+
pynode:
|
|
121
|
+
network_mode: host
|
|
122
|
+
# ports: mapping is ignored in host mode - the container shares the
|
|
123
|
+
# host's network stack directly, so remove/comment out `ports:` above.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This does **not** work on Docker Desktop for Windows/macOS - Docker Desktop
|
|
127
|
+
runs containers inside a lightweight VM, and `network_mode: host` there still
|
|
128
|
+
does not bridge multicast across the VM boundary.
|
|
129
|
+
|
|
130
|
+
**Option 2: macvlan network**
|
|
131
|
+
|
|
132
|
+
A macvlan network gives the container its own IP address directly on the LAN
|
|
133
|
+
(as if it were a separate physical device), which also allows multicast to
|
|
134
|
+
work normally. This works on Docker Desktop as well as Linux, but requires
|
|
135
|
+
more setup (a dedicated IP, and typically a way for the host itself to reach
|
|
136
|
+
the container since macvlan interfaces are not reachable from the host by
|
|
137
|
+
default). See the
|
|
138
|
+
[Docker macvlan network documentation](https://docs.docker.com/network/drivers/macvlan/)
|
|
139
|
+
for setup details.
|
|
140
|
+
|
|
141
|
+
**Option 3: mDNS reflector on the host**
|
|
142
|
+
|
|
143
|
+
When host networking isn't available (e.g. Docker Desktop), run an mDNS
|
|
144
|
+
reflector on the host to bridge multicast between the host's network and the
|
|
145
|
+
container's bridge network:
|
|
146
|
+
|
|
147
|
+
- **avahi-daemon** (Linux) - enable reflection in `/etc/avahi/avahi-daemon.conf`:
|
|
148
|
+
```ini
|
|
149
|
+
[reflector]
|
|
150
|
+
enable-reflector=yes
|
|
151
|
+
```
|
|
152
|
+
- **mdns-repeater** - run as a sidecar/host process that repeats mDNS packets
|
|
153
|
+
between interfaces.
|
|
154
|
+
|
|
155
|
+
This is the option that works when host networking isn't an option, at the
|
|
156
|
+
cost of an extra moving part to run and maintain.
|
|
157
|
+
|
|
158
|
+
### Port Matching
|
|
159
|
+
|
|
160
|
+
Whichever option you use, the port PyNode advertises in its mDNS records
|
|
161
|
+
must match the port other machines can actually reach it on (the *published*
|
|
162
|
+
port), not just the container's internal port. If you remap ports (e.g.
|
|
163
|
+
`"8080:5000"`), make sure discovery consumers connect using the published
|
|
164
|
+
port (`8080`), not the internal one.
|
|
165
|
+
|
|
166
|
+
## Troubleshooting
|
|
167
|
+
|
|
168
|
+
### GPU Not Detected
|
|
169
|
+
|
|
170
|
+
Verify NVIDIA Container Toolkit is installed:
|
|
171
|
+
```bash
|
|
172
|
+
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Permission Issues
|
|
176
|
+
|
|
177
|
+
Ensure the mounted directories have proper permissions:
|
|
178
|
+
```bash
|
|
179
|
+
mkdir -p workflows logs
|
|
180
|
+
chmod 777 workflows logs
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Rebuild from Scratch
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
docker-compose down
|
|
187
|
+
docker-compose build --no-cache
|
|
188
|
+
docker-compose up
|
|
189
|
+
```
|
|
@@ -16,6 +16,16 @@ pip install "pynode-flow[full]"
|
|
|
16
16
|
Nodes whose optional dependencies are missing are simply skipped at startup, so
|
|
17
17
|
a core-only install still runs fine.
|
|
18
18
|
|
|
19
|
+
> **Warning — conflicting `pynode` package:** an unrelated project on PyPI is
|
|
20
|
+
> named `pynode`, and it also installs a top-level `pynode` package. If it is
|
|
21
|
+
> present in the same environment the two clobber each other (symptoms include
|
|
22
|
+
> `ImportError: cannot import name '__version__' from 'pynode' (unknown
|
|
23
|
+
> location)`). Remove it before or after installing:
|
|
24
|
+
>
|
|
25
|
+
> ```bash
|
|
26
|
+
> pip uninstall pynode
|
|
27
|
+
> ```
|
|
28
|
+
|
|
19
29
|
### Optional extras
|
|
20
30
|
|
|
21
31
|
Install only the node groups you need:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pynode-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python based Node-RED-like visual workflow system
|
|
5
5
|
Author: olkham
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,6 +84,7 @@ https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2
|
|
|
84
84
|
- **SwitchNode**: Route messages based on conditions
|
|
85
85
|
- **DelayNode**: Delay message delivery
|
|
86
86
|
- **GateNode**: Control message flow with real-time toggle
|
|
87
|
+
- **LinkNode**: Pass messages between flows via named channels (Link In / Link Out)
|
|
87
88
|
- **RateProbeNode**: Monitor message throughput
|
|
88
89
|
- **QueueLengthProbeNode**: Monitor queue lengths
|
|
89
90
|
- **CounterNode**: Count messages
|
|
@@ -112,6 +113,12 @@ pip install "pynode-flow[full]"
|
|
|
112
113
|
pynode
|
|
113
114
|
```
|
|
114
115
|
|
|
116
|
+
or specify a port:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pynode --port 500
|
|
120
|
+
```
|
|
121
|
+
|
|
115
122
|
See [INSTALL.md](INSTALL.md) for the full list of extras (`vision`, `mqtt`,
|
|
116
123
|
`camera`, `inference`, `vlm`, `upload`, `discovery`, `full`) and how to install
|
|
117
124
|
per-node dependencies with `pynode-install-nodes`.
|
|
@@ -187,6 +194,15 @@ python -m pynode
|
|
|
187
194
|
|
|
188
195
|
Navigate to `http://localhost:5000`
|
|
189
196
|
|
|
197
|
+
### Load an Example Workflow
|
|
198
|
+
|
|
199
|
+
Ten ready-made learning workflows ship with PyNode — from a two-node hello
|
|
200
|
+
world up to a camera → YOLO → filter → MQTT alert pipeline (including a "blur
|
|
201
|
+
every detected person" privacy flow). Load any of them straight from the
|
|
202
|
+
editor: **☰ menu → Examples → pick one**, then press **Deploy**. See
|
|
203
|
+
[pynode/static/examples/README.md](pynode/static/examples/README.md) for the
|
|
204
|
+
guided tour and per-example prerequisites.
|
|
205
|
+
|
|
190
206
|
### Data Directory
|
|
191
207
|
|
|
192
208
|
PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus timestamped backups in `_backups/`). The data directory is resolved in this order:
|
|
@@ -198,6 +214,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
|
|
|
198
214
|
|
|
199
215
|
The resolved location is logged at startup (`Workflow data directory: ...`).
|
|
200
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
|
+
|
|
201
236
|
## Securing PyNode
|
|
202
237
|
|
|
203
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:
|
|
@@ -19,6 +19,7 @@ https://github.com/user-attachments/assets/0b53085a-2cc6-4c26-bd43-e0de1e0716a2
|
|
|
19
19
|
- **SwitchNode**: Route messages based on conditions
|
|
20
20
|
- **DelayNode**: Delay message delivery
|
|
21
21
|
- **GateNode**: Control message flow with real-time toggle
|
|
22
|
+
- **LinkNode**: Pass messages between flows via named channels (Link In / Link Out)
|
|
22
23
|
- **RateProbeNode**: Monitor message throughput
|
|
23
24
|
- **QueueLengthProbeNode**: Monitor queue lengths
|
|
24
25
|
- **CounterNode**: Count messages
|
|
@@ -47,6 +48,12 @@ pip install "pynode-flow[full]"
|
|
|
47
48
|
pynode
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
or specify a port:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pynode --port 500
|
|
55
|
+
```
|
|
56
|
+
|
|
50
57
|
See [INSTALL.md](INSTALL.md) for the full list of extras (`vision`, `mqtt`,
|
|
51
58
|
`camera`, `inference`, `vlm`, `upload`, `discovery`, `full`) and how to install
|
|
52
59
|
per-node dependencies with `pynode-install-nodes`.
|
|
@@ -122,6 +129,15 @@ python -m pynode
|
|
|
122
129
|
|
|
123
130
|
Navigate to `http://localhost:5000`
|
|
124
131
|
|
|
132
|
+
### Load an Example Workflow
|
|
133
|
+
|
|
134
|
+
Ten ready-made learning workflows ship with PyNode — from a two-node hello
|
|
135
|
+
world up to a camera → YOLO → filter → MQTT alert pipeline (including a "blur
|
|
136
|
+
every detected person" privacy flow). Load any of them straight from the
|
|
137
|
+
editor: **☰ menu → Examples → pick one**, then press **Deploy**. See
|
|
138
|
+
[pynode/static/examples/README.md](pynode/static/examples/README.md) for the
|
|
139
|
+
guided tour and per-example prerequisites.
|
|
140
|
+
|
|
125
141
|
### Data Directory
|
|
126
142
|
|
|
127
143
|
PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus timestamped backups in `_backups/`). The data directory is resolved in this order:
|
|
@@ -133,6 +149,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
|
|
|
133
149
|
|
|
134
150
|
The resolved location is logged at startup (`Workflow data directory: ...`).
|
|
135
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
|
+
|
|
136
171
|
## Securing PyNode
|
|
137
172
|
|
|
138
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
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This directory contains example Python scripts that demonstrate how to use PyNode workflows programmatically without the UI.
|
|
4
4
|
|
|
5
|
+
> Looking for **GUI-loadable** examples instead? PyNode ships ten example
|
|
6
|
+
> workflows you can load from the editor's **☰ menu → Examples**. Their source
|
|
7
|
+
> and a guided tour live in
|
|
8
|
+
> [pynode/static/examples/](../pynode/static/examples/README.md).
|
|
9
|
+
|
|
5
10
|
## Examples
|
|
6
11
|
|
|
7
12
|
### 1. `camera_workflow.py` - Simple Camera Display
|