pynode-flow 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynode_flow-0.2.2/.claude/skills/pynode-workflows/SKILL.md +185 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/.gitignore +2 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/INSTALL.md +10 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/PKG-INFO +17 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/README.md +16 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/examples/README.md +5 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/_version.py +3 -3
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/helpers.py +9 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/nodes.py +22 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/services.py +40 -52
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/workflows.py +54 -16
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CameraNode/camera_node.py +120 -45
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DebugNode/debug_node.py +9 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DelayNode/delay_node.py +75 -37
- pynode_flow-0.2.2/pynode/nodes/LinkNode/__init__.py +13 -0
- pynode_flow-0.2.2/pynode/nodes/LinkNode/link_bus.py +104 -0
- pynode_flow-0.2.2/pynode/nodes/LinkNode/link_in_node.py +104 -0
- pynode_flow-0.2.2/pynode/nodes/LinkNode/link_out_node.py +88 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/mqtt_service.py +94 -30
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RateProbeNode/rate_probe_node.py +37 -29
- pynode_flow-0.2.2/pynode/nodes/TemplateNode/requirements.txt +1 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/VideoReaderNode/video_reader_node.py +43 -22
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/messages.py +1 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/server.py +17 -0
- pynode_flow-0.2.2/pynode/static/examples/01-hello-world.json +33 -0
- pynode_flow-0.2.2/pynode/static/examples/02-function-transform.json +45 -0
- pynode_flow-0.2.2/pynode/static/examples/03-switch-routing.json +80 -0
- pynode_flow-0.2.2/pynode/static/examples/04-flow-control.json +76 -0
- pynode_flow-0.2.2/pynode/static/examples/05-webcam-viewer.json +35 -0
- pynode_flow-0.2.2/pynode/static/examples/06-webcam-yolo.json +75 -0
- pynode_flow-0.2.2/pynode/static/examples/07-blur-people.json +95 -0
- pynode_flow-0.2.2/pynode/static/examples/08-video-yolo-recorder.json +73 -0
- pynode_flow-0.2.2/pynode/static/examples/09-mqtt-pub-sub.json +56 -0
- pynode_flow-0.2.2/pynode/static/examples/10-person-alert-mqtt.json +134 -0
- pynode_flow-0.2.2/pynode/static/examples/README.md +65 -0
- pynode_flow-0.2.2/pynode/static/examples/manifest.json +74 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/index.html +80 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/debug.js +280 -58
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/events.js +72 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/main.js +5 -1
- pynode_flow-0.2.2/pynode/static/js/mqtt-services.js +346 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/nodes.js +9 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/properties.js +90 -283
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/state.js +13 -4
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/workflow.js +121 -60
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/workflows.js +192 -89
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/style.css +227 -32
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/workflow_engine.py +7 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/workflow_manager.py +4 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/PKG-INFO +17 -1
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/SOURCES.txt +24 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/scm_file_list.json +24 -0
- pynode_flow-0.2.2/pynode_flow.egg-info/scm_version.json +8 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_api_nodes.py +49 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_api_workflows.py +83 -0
- pynode_flow-0.2.2/tests/test_camera_node_backend.py +119 -0
- pynode_flow-0.2.2/tests/test_debug_workflow_scope.py +122 -0
- pynode_flow-0.2.2/tests/test_delay_rate_limit.py +296 -0
- pynode_flow-0.2.2/tests/test_link_nodes.py +208 -0
- pynode_flow-0.2.2/tests/test_mqtt_services.py +158 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_video_reader_node.py +62 -6
- pynode_flow-0.2.1/pynode_flow.egg-info/scm_version.json +0 -8
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/.dockerignore +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/DOCKER.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/Dockerfile +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/Dockerfile.cpu +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/LICENSE +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/MANIFEST.in +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/docker-compose.yml +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/install_nodes.bat +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/install_nodes.sh +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/plan-pynodeRepoReview.prompt.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/sse.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/config.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/main.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/node_registry.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ChangeNode/change_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CropNode/crop_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DelayNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ErrorNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FunctionNode/function_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/FunctionNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/GateNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/inference_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InferenceNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/InjectNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/JoinNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.1/pynode/nodes/RangeNode → pynode_flow-0.2.2/pynode/nodes/LinkNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/colormap_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/fft_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/histogram_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/in_range_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/perspective_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RangeNode/range_node.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/RateProbeNode → pynode_flow-0.2.2/pynode/nodes/RangeNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/SplitNode → pynode_flow-0.2.2/pynode/nodes/RateProbeNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/SwitchNode → pynode_flow-0.2.2/pynode/nodes/SplitNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SplitNode/split_node.py +0 -0
- {pynode_flow-0.2.1/pynode/nodes/TemplateNode → pynode_flow-0.2.2/pynode/nodes/SwitchNode}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SwitchNode/switch_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/TrackerNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/UltralyticsNode/ultralytics_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/base_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/image_utils.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/info.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/config.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/ui-utils.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/pyproject.toml +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/requirements.txt +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/setup.bat +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/setup.cfg +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/setup.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/setup.sh +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/__init__.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/conftest.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_config.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_device_detection.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_node_types_api.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.1 → pynode_flow-0.2.2}/tests/test_workflow_roundtrip.py +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pynode-workflows
|
|
3
|
+
description: Build, edit and validate PyNode workflow JSON files (nodes + connections) that users can import via the GUI or REST API. Use when asked to create example flows, generate a workflow, wire nodes together, or debug a workflow definition.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building PyNode Workflows
|
|
7
|
+
|
|
8
|
+
A PyNode workflow is a JSON document describing nodes and the connections
|
|
9
|
+
between them. The GUI's **Import** button (hamburger menu) accepts a flat
|
|
10
|
+
workflow file and loads it into a **new tab** named after the file. The same
|
|
11
|
+
document can be POSTed to `/api/workflow`.
|
|
12
|
+
|
|
13
|
+
## Workflow file format
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"nodes": [
|
|
18
|
+
{
|
|
19
|
+
"id": "inject_1",
|
|
20
|
+
"type": "InjectNode",
|
|
21
|
+
"name": "Every 2s",
|
|
22
|
+
"config": { "props": [{"property": "payload", "value": "hi", "valueType": "str"}], "repeat": "2" },
|
|
23
|
+
"x": 80, "y": 80
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"connections": [
|
|
27
|
+
{ "source": "inject_1", "target": "debug_1", "sourceOutput": 0, "targetInput": 0 }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Rules:
|
|
33
|
+
- `type` is the **Python class name** of the node (e.g. `InjectNode`, `MqttOutNode`).
|
|
34
|
+
Unknown types import as disabled placeholder nodes — always validate.
|
|
35
|
+
- `id` is any unique string. Use readable slugs (`camera_1`, `yolo_1`).
|
|
36
|
+
- `enabled` is optional (default `true`). `x`/`y` are canvas pixels; space
|
|
37
|
+
columns ~260px apart (80, 340, 600, 860, 1120), rows ~140px apart.
|
|
38
|
+
- `sourceOutput`/`targetInput` are 0-based port indexes.
|
|
39
|
+
- Config values are lenient: many nodes read numbers from strings
|
|
40
|
+
(`"confidence": "0.25"`) — copy the conventions below exactly.
|
|
41
|
+
- The on-disk `workflows/workflow.json` uses a different multi-workflow
|
|
42
|
+
wrapper (`{version, activeWorkflow, workflows: [...]}`); **never** write that
|
|
43
|
+
file directly — import through the GUI or API instead.
|
|
44
|
+
|
|
45
|
+
## Message conventions
|
|
46
|
+
|
|
47
|
+
Messages are dicts in Node-RED style: `payload`, `topic`, `_msgid`, plus extras.
|
|
48
|
+
- Images travel at `msg.payload.image` as a numpy BGR array (or JPEG bytes if a
|
|
49
|
+
source has `encode_jpeg: true`). Downstream nodes locate it via their
|
|
50
|
+
`image_path` config, default `payload.image`.
|
|
51
|
+
- Detections travel at `msg.payload.detections`: a list of
|
|
52
|
+
`{"bbox": [x1,y1,x2,y2], "bbox_format": "xyxy", "class_name": str,
|
|
53
|
+
"class_id": int, "confidence": float}`, with `payload.detection_count` and
|
|
54
|
+
`payload.bbox_format` alongside.
|
|
55
|
+
- `MqttOutNode` publishes `msg.payload`; **`msg.topic` overrides the node's
|
|
56
|
+
configured topic** — don't set a `topic` in InjectNode when a downstream
|
|
57
|
+
MQTT-out node should control the topic.
|
|
58
|
+
|
|
59
|
+
## Node catalog (config keys verified against source)
|
|
60
|
+
|
|
61
|
+
### Core / logic
|
|
62
|
+
| Type | Ports in→out | Config (defaults) |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `InjectNode` | 0→1 | `props`: list of `{property: "payload", value, valueType: str\|num\|bool\|json\|date\|env}` (dot paths OK, e.g. `payload.count`); `topic`: str; `once`: seconds (str, empty=off); `repeat`: seconds (str, empty=off). Manual trigger button built in. |
|
|
65
|
+
| `DebugNode` | 1→0 | `console`: bool; `complete`: `"payload"` (default) or `"msg"` for whole message; `drop_messages`: bool |
|
|
66
|
+
| `FunctionNode` | 1→N | `func`: Python source (body of a function receiving `msg`, `node`, `time`; must `return msg` / a list for multiple outputs / `None` to drop); `outputs`: int (default 1). Full builtins and `import` are available inside the body. |
|
|
67
|
+
| `SwitchNode` | 1→N | `property`: dot path (`payload`, `payload.count`); `checkall`: bool; `rules`: list of `{operator, value, valueType}` — **rule index = output index**. Operators: `eq neq lt lte gt gte between contains matches true false null nnull else`. |
|
|
68
|
+
| `ChangeNode` | 1→1 | `rules`: list of `{type: set\|delete\|change\|move, path: "msg.payload...", value, valueType}` |
|
|
69
|
+
| `GateNode` | 1→1 | `{}` — passes messages only while enabled; toggled live from its card |
|
|
70
|
+
| `LinkOutNode` | 1→0 | `channel`: str — publishes each incoming msg to the named channel on a process-wide bus. Reaches every `LinkInNode` with the same channel, **including in other workflows**. Empty channel = disabled. `drop_messages` defaults `"false"`. |
|
|
71
|
+
| `LinkInNode` | 0→1 | `channel`: str — emits every msg published on the named channel (by any `LinkOutNode`, cross-flow). Deep-copied on delivery (flow isolation). Only receives while its flow is deployed/running and the node is enabled. Empty channel = disabled. |
|
|
72
|
+
| `DelayNode` | 1→1 | `mode`: `delay`\|`delay_count`\|`rate`; `timeout`: secs (delay mode); `rate` + `rate_time`: msgs per secs; `rate_drop`: `drop`\|`queue` |
|
|
73
|
+
| `CounterNode` | 1→1 | `initial_value`: "0"; `increment`: "1"; `retain_payload`: "false" |
|
|
74
|
+
| `RateProbeNode` | 1→1 | `window_size`: seconds (float) — passes msgs through, shows msg/s |
|
|
75
|
+
|
|
76
|
+
### Vision
|
|
77
|
+
| Type | Ports | Config |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `CameraNode` | 0→1 | `device_index`: 0; `fps`: 30; `width`: 640; `height`: 480; `encode_jpeg`: false; `jpeg_quality`: 75 |
|
|
80
|
+
| `VideoReaderNode` | 0→1 | `source`: path (upload via node properties in GUI); `fps`: 0 (=native); `loop`: bool; `jpeg_quality`: 80 |
|
|
81
|
+
| `UltralyticsNode` | 1→1 | `model`: "yolo11n.pt" (auto-downloads); `confidence`: "0.25"; `iou`: "0.45"; `draw_results`: "true"/"false"; `max_det`: "300"; `include_image`: true; `include_predictions`: true; `drop_messages`: "true" (keep on!); `device`: "cpu" (portable) or `cuda:0`/`intel:gpu` |
|
|
82
|
+
| `LabelFilterNode` | 1→2 | `labels`: "person, car" (comma list); `match_mode`: `any`\|`all`; `case_sensitive`: false; `filter_detections`: true (strips non-matching detections); `detections_path`: "payload.detections". **Output 0 = matched, output 1 = unmatched.** |
|
|
83
|
+
| `ConfidenceFilterNode` | 1→2 | `threshold_source`: `manual`\|`msg`; `threshold`: 0.5; `detections_path`: "payload.detections"; `confidence_field`: "confidence". **Output 0 = ≥ threshold, output 1 = < threshold.** |
|
|
84
|
+
| `ImageViewerNode` | 1→0 | `width`: 320; `height`: 240; `image_path`: "payload.image"; `drop_messages`: true |
|
|
85
|
+
| `VideoWriterNode` | 1→0 | `path`: "./output"; `filename`: "video_{counter}"; `codec`: `mp4v`\|`avc1`\|`xvid`...; `framerate`: 30.0; `width`/`height`; `auto_resolution`: bool (match input); `clip_length`: 0 (=unlimited); `naming_mode`: "counter"; `counter_digits`: 4 |
|
|
86
|
+
|
|
87
|
+
Other vision nodes: `DrawPredictionsNode`, `CropNode`, `TrackerNode`,
|
|
88
|
+
`ImageFormatNode`, `SliceNode`, `BBoxMetricsNode`, `PolygonMetricsNode`,
|
|
89
|
+
`InferenceNode` (multi-backend), `FrameSourceNode` (multi-source camera lib) —
|
|
90
|
+
read their `DEFAULT_CONFIG`/`properties` in `pynode/nodes/<Name>/` before use.
|
|
91
|
+
|
|
92
|
+
### Network
|
|
93
|
+
| Type | Ports | Config |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `MqttInNode` | 0→1 | `serviceId`: broker id from mqtt_services.json; `topic`: subscribe topic (wildcards `#`/`+` OK); `qos`: "0" |
|
|
96
|
+
| `MqttOutNode` | 1→0 | `serviceId`; `topic`: publish topic; `qos`: "0"; `retain`: "false" |
|
|
97
|
+
|
|
98
|
+
MQTT brokers are **services** stored in `mqtt_services.json` next to the
|
|
99
|
+
workflow data (managed in the GUI via the node's Broker dropdown → add). A
|
|
100
|
+
workflow referencing an unknown `serviceId` imports fine but the node reports
|
|
101
|
+
"No MQTT broker configured" until the user picks a broker.
|
|
102
|
+
|
|
103
|
+
Also available: `WebhookNode`, `RESTEndpointNode`, `mDNSNode`.
|
|
104
|
+
|
|
105
|
+
## FunctionNode authoring rules
|
|
106
|
+
|
|
107
|
+
The `func` string is the **body** of `def user_function(msg, node, time)` —
|
|
108
|
+
top-level `return` is required to emit. `msg` is a deep copy. Imports work
|
|
109
|
+
inside the body (`import cv2`, `import numpy as np`, `import random`).
|
|
110
|
+
Return a list to fan out across `outputs`; `None` drops the message.
|
|
111
|
+
|
|
112
|
+
Example — blur every detection bbox in-place:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import cv2
|
|
116
|
+
import numpy as np
|
|
117
|
+
p = msg.get('payload') or {}
|
|
118
|
+
img = p.get('image')
|
|
119
|
+
dets = p.get('detections') or []
|
|
120
|
+
if not isinstance(img, np.ndarray):
|
|
121
|
+
return msg
|
|
122
|
+
h, w = img.shape[:2]
|
|
123
|
+
for d in dets:
|
|
124
|
+
bbox = d.get('bbox') or []
|
|
125
|
+
if len(bbox) != 4:
|
|
126
|
+
continue
|
|
127
|
+
x1, y1, x2, y2 = [int(round(v)) for v in bbox]
|
|
128
|
+
x1 = max(0, min(x1, w - 1)); x2 = max(x1 + 1, min(x2, w))
|
|
129
|
+
y1 = max(0, min(y1, h - 1)); y2 = max(y1 + 1, min(y2, h))
|
|
130
|
+
k = max(9, ((x2 - x1) // 4)) | 1
|
|
131
|
+
p['image'][y1:y2, x1:x2] = cv2.GaussianBlur(img[y1:y2, x1:x2], (k, k), 0)
|
|
132
|
+
return msg
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When embedding code in workflow JSON, join lines with `\n` and keep
|
|
136
|
+
indentation exact.
|
|
137
|
+
|
|
138
|
+
## Loading a workflow
|
|
139
|
+
|
|
140
|
+
- **GUI (import)**: hamburger menu → *Import* → choose the `.json` file → it
|
|
141
|
+
opens as a new tab → press *Deploy* to activate.
|
|
142
|
+
- **GUI (bundled examples)**: hamburger menu → *Examples* → pick one. These are
|
|
143
|
+
the files in `pynode/static/examples/`, served statically and listed via
|
|
144
|
+
`pynode/static/examples/manifest.json`. **When adding a new bundled example,
|
|
145
|
+
drop the `.json` in that folder and add an entry to `manifest.json`** (id,
|
|
146
|
+
file, title, description, requires) or it won't appear in the menu.
|
|
147
|
+
- **API**: `POST /api/workflow` with the JSON body imports into the active
|
|
148
|
+
workflow (add `?workflow=<id>` to target another; `X-API-Key` header if the
|
|
149
|
+
server has an API key). `GET /api/workflow` exports the current one —
|
|
150
|
+
building a flow in the GUI and exporting it is the best way to learn configs.
|
|
151
|
+
|
|
152
|
+
## Validating a workflow file (do this for every file you produce)
|
|
153
|
+
|
|
154
|
+
Run a sandboxed import — never against the real server or the repo's
|
|
155
|
+
`workflows/` dir (a leaked test once destroyed real data; see
|
|
156
|
+
tests/conftest.py):
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import json, tempfile, os
|
|
160
|
+
from pynode.server import create_app # use a venv with the vision extras
|
|
161
|
+
|
|
162
|
+
td = tempfile.mkdtemp()
|
|
163
|
+
app = create_app({'WORKFLOWS_DIR': os.path.join(td, 'wf'),
|
|
164
|
+
'WORKFLOW_FILE': os.path.join(td, 'wf', 'workflow.json'),
|
|
165
|
+
'UPLOAD_BASE_DIR': os.path.join(td, 'up'), 'TESTING': True})
|
|
166
|
+
client = app.test_client()
|
|
167
|
+
data = json.load(open('pynode/static/examples/01-hello-world.json'))
|
|
168
|
+
r = client.post('/api/workflow', json=data)
|
|
169
|
+
assert r.status_code == 201, r.get_json()
|
|
170
|
+
manager = app.extensions['workflow_manager']
|
|
171
|
+
with manager.state_lock:
|
|
172
|
+
eng = next(iter(manager.working_engines.values()))
|
|
173
|
+
unknown = [n.type for n in eng.nodes.values() if getattr(n, '_is_unknown_node', False)]
|
|
174
|
+
assert not unknown, f"unknown node types: {unknown}"
|
|
175
|
+
manager.shutdown()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Checklist:
|
|
179
|
+
- every connection's `source`/`target` id exists; port indexes < the node's
|
|
180
|
+
`input_count`/`output_count`
|
|
181
|
+
- node `type` strings match class names exactly (case-sensitive)
|
|
182
|
+
- vision chains keep `drop_messages` on for slow consumers (YOLO, viewers)
|
|
183
|
+
- prefer `device: "cpu"` and auto-downloading models (`yolo11n.pt`) so flows
|
|
184
|
+
run anywhere; note anything machine-specific (camera index, broker IP) in an
|
|
185
|
+
accompanying README
|
|
@@ -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.2
|
|
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:
|
|
@@ -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:
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This directory contains example Python scripts that demonstrate how to use PyNode workflows programmatically without the UI.
|
|
4
4
|
|
|
5
|
+
> Looking for **GUI-loadable** examples instead? PyNode ships ten example
|
|
6
|
+
> workflows you can load from the editor's **☰ menu → Examples**. Their source
|
|
7
|
+
> and a guided tour live in
|
|
8
|
+
> [pynode/static/examples/](../pynode/static/examples/README.md).
|
|
9
|
+
|
|
5
10
|
## Examples
|
|
6
11
|
|
|
7
12
|
### 1. `camera_workflow.py` - Simple Camera Display
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g51b4c1354'
|
|
@@ -16,6 +16,15 @@ def _get_manager():
|
|
|
16
16
|
return current_app.extensions['workflow_manager']
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def _get_mqtt_manager():
|
|
20
|
+
"""Return the MQTTServiceManager of the app handling the current request.
|
|
21
|
+
|
|
22
|
+
Each app owns its own manager (isolated services file), so the sandboxed
|
|
23
|
+
test app never touches the real mqtt_services.json.
|
|
24
|
+
"""
|
|
25
|
+
return current_app.extensions['mqtt_manager']
|
|
26
|
+
|
|
27
|
+
|
|
19
28
|
def _json_error(message, status):
|
|
20
29
|
"""JSON error envelope: {'success': False, 'error': str}."""
|
|
21
30
|
return jsonify({'success': False, 'error': message}), status
|
|
@@ -29,6 +29,28 @@ def get_node_types():
|
|
|
29
29
|
return jsonify(node_registry.get_node_types())
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
@nodes_bp.route('/api/link-channels', methods=['GET'])
|
|
33
|
+
def get_link_channels():
|
|
34
|
+
"""List distinct, non-empty channel names already used by Link In/Out
|
|
35
|
+
nodes across every workflow's working engine.
|
|
36
|
+
|
|
37
|
+
Backs the properties panel's channel suggestion list, so users can pick
|
|
38
|
+
an existing channel instead of retyping it. Scans working engines (not
|
|
39
|
+
just deployed) so channels typed into not-yet-deployed nodes show up too.
|
|
40
|
+
"""
|
|
41
|
+
manager = _get_manager()
|
|
42
|
+
channels = set()
|
|
43
|
+
with manager.state_lock:
|
|
44
|
+
for engine in manager.working_engines.values():
|
|
45
|
+
for node in engine.nodes.values():
|
|
46
|
+
if node.type not in ('LinkInNode', 'LinkOutNode'):
|
|
47
|
+
continue
|
|
48
|
+
channel = str(node.config.get('channel', '') or '').strip()
|
|
49
|
+
if channel:
|
|
50
|
+
channels.add(channel)
|
|
51
|
+
return jsonify({'success': True, 'channels': sorted(channels)})
|
|
52
|
+
|
|
53
|
+
|
|
32
54
|
@nodes_bp.route('/api/nodes', methods=['GET'])
|
|
33
55
|
def get_nodes():
|
|
34
56
|
"""Get all nodes in the working workflow."""
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
"""MQTT service management API.
|
|
1
|
+
"""MQTT service management API.
|
|
2
|
+
|
|
3
|
+
All handlers resolve the MQTT service manager off the current app
|
|
4
|
+
(``_get_mqtt_manager``) so the sandboxed test app operates on its own
|
|
5
|
+
isolated services file, never the real mqtt_services.json.
|
|
6
|
+
"""
|
|
2
7
|
|
|
3
8
|
from flask import Blueprint, jsonify
|
|
4
9
|
|
|
5
|
-
from pynode.api.helpers import
|
|
10
|
+
from pynode.api.helpers import (
|
|
11
|
+
_INVALID_BODY_ERROR, _get_json_body, _get_mqtt_manager, _json_error)
|
|
6
12
|
|
|
7
13
|
services_bp = Blueprint('services', __name__)
|
|
8
14
|
|
|
@@ -11,8 +17,7 @@ services_bp = Blueprint('services', __name__)
|
|
|
11
17
|
def list_mqtt_services():
|
|
12
18
|
"""List all MQTT services."""
|
|
13
19
|
try:
|
|
14
|
-
|
|
15
|
-
services = mqtt_manager.list_services()
|
|
20
|
+
services = _get_mqtt_manager().list_services()
|
|
16
21
|
return jsonify({'success': True, 'services': services})
|
|
17
22
|
except Exception as e:
|
|
18
23
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
@@ -22,7 +27,6 @@ def list_mqtt_services():
|
|
|
22
27
|
def create_mqtt_service():
|
|
23
28
|
"""Create a new MQTT service."""
|
|
24
29
|
try:
|
|
25
|
-
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
26
30
|
data = _get_json_body()
|
|
27
31
|
if data is None:
|
|
28
32
|
return _json_error(_INVALID_BODY_ERROR, 400)
|
|
@@ -33,16 +37,32 @@ def create_mqtt_service():
|
|
|
33
37
|
if not data.get('broker'):
|
|
34
38
|
return jsonify({'success': False, 'error': 'Broker address is required'}), 400
|
|
35
39
|
|
|
36
|
-
service =
|
|
37
|
-
return jsonify({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
service = _get_mqtt_manager().create_service(data)
|
|
41
|
+
return jsonify({'success': True, 'service': service.to_dict()})
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@services_bp.route('/api/services/mqtt/test', methods=['POST'])
|
|
47
|
+
def test_mqtt_connection():
|
|
48
|
+
"""Test an MQTT connection using the SUBMITTED form values.
|
|
49
|
+
|
|
50
|
+
Does not create or modify any saved service and does not disturb live
|
|
51
|
+
connections - it spins up a short-lived throwaway client that is always
|
|
52
|
+
cleaned up. Returns ``{'success': bool, 'error': str|None}`` where
|
|
53
|
+
``success`` reflects whether the connection succeeded.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
from pynode.nodes.MQTTNode.mqtt_service import test_connection
|
|
57
|
+
|
|
58
|
+
data = _get_json_body()
|
|
59
|
+
if data is None:
|
|
60
|
+
return _json_error(_INVALID_BODY_ERROR, 400)
|
|
61
|
+
if not data.get('broker'):
|
|
62
|
+
return jsonify({'success': False, 'error': 'Broker address is required'}), 400
|
|
63
|
+
|
|
64
|
+
success, error = test_connection(data)
|
|
65
|
+
return jsonify({'success': success, 'error': error})
|
|
46
66
|
except Exception as e:
|
|
47
67
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
48
68
|
|
|
@@ -51,15 +71,11 @@ def create_mqtt_service():
|
|
|
51
71
|
def get_mqtt_service(service_id):
|
|
52
72
|
"""Get a specific MQTT service by ID."""
|
|
53
73
|
try:
|
|
54
|
-
|
|
55
|
-
service = mqtt_manager.get_service(service_id)
|
|
74
|
+
service = _get_mqtt_manager().get_service(service_id)
|
|
56
75
|
if not service:
|
|
57
76
|
return jsonify({'success': False, 'error': 'Service not found'}), 404
|
|
58
77
|
|
|
59
|
-
return jsonify({
|
|
60
|
-
'success': True,
|
|
61
|
-
'service': service.to_dict()
|
|
62
|
-
})
|
|
78
|
+
return jsonify({'success': True, 'service': service.to_dict()})
|
|
63
79
|
except Exception as e:
|
|
64
80
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
65
81
|
|
|
@@ -68,19 +84,15 @@ def get_mqtt_service(service_id):
|
|
|
68
84
|
def update_mqtt_service(service_id):
|
|
69
85
|
"""Update an existing MQTT service."""
|
|
70
86
|
try:
|
|
71
|
-
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
72
87
|
data = _get_json_body()
|
|
73
88
|
if data is None:
|
|
74
89
|
return _json_error(_INVALID_BODY_ERROR, 400)
|
|
75
90
|
|
|
76
|
-
service =
|
|
91
|
+
service = _get_mqtt_manager().update_service(service_id, data)
|
|
77
92
|
if not service:
|
|
78
93
|
return jsonify({'success': False, 'error': 'Service not found'}), 404
|
|
79
94
|
|
|
80
|
-
return jsonify({
|
|
81
|
-
'success': True,
|
|
82
|
-
'service': service.to_dict()
|
|
83
|
-
})
|
|
95
|
+
return jsonify({'success': True, 'service': service.to_dict()})
|
|
84
96
|
except Exception as e:
|
|
85
97
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
86
98
|
|
|
@@ -89,9 +101,7 @@ def update_mqtt_service(service_id):
|
|
|
89
101
|
def delete_mqtt_service(service_id):
|
|
90
102
|
"""Delete an MQTT service."""
|
|
91
103
|
try:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if not mqtt_manager.delete_service(service_id):
|
|
104
|
+
if not _get_mqtt_manager().delete_service(service_id):
|
|
95
105
|
return jsonify({
|
|
96
106
|
'success': False,
|
|
97
107
|
'error': 'Service not found or still in use'
|
|
@@ -100,25 +110,3 @@ def delete_mqtt_service(service_id):
|
|
|
100
110
|
return jsonify({'success': True})
|
|
101
111
|
except Exception as e:
|
|
102
112
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@services_bp.route('/api/services/mqtt/<service_id>/test', methods=['POST'])
|
|
106
|
-
def test_mqtt_service(service_id):
|
|
107
|
-
"""Test connection to an MQTT service."""
|
|
108
|
-
try:
|
|
109
|
-
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
110
|
-
|
|
111
|
-
service = mqtt_manager.get_service(service_id)
|
|
112
|
-
if not service:
|
|
113
|
-
return jsonify({'success': False, 'error': 'Service not found'}), 404
|
|
114
|
-
|
|
115
|
-
# Try to connect
|
|
116
|
-
connected = service.connect()
|
|
117
|
-
|
|
118
|
-
return jsonify({
|
|
119
|
-
'success': True,
|
|
120
|
-
'connected': connected,
|
|
121
|
-
'status': 'connected' if connected else 'failed'
|
|
122
|
-
})
|
|
123
|
-
except Exception as e:
|
|
124
|
-
return jsonify({'success': False, 'error': str(e)}), 500
|
|
@@ -4,7 +4,7 @@ full deploy (save), incremental deploy (deploy-changes), restart and stats.
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
-
from flask import Blueprint, jsonify
|
|
7
|
+
from flask import Blueprint, jsonify, request
|
|
8
8
|
|
|
9
9
|
from pynode.api.helpers import (
|
|
10
10
|
_INVALID_BODY_ERROR,
|
|
@@ -23,9 +23,24 @@ workflows_bp = Blueprint('workflows', __name__)
|
|
|
23
23
|
|
|
24
24
|
@workflows_bp.route('/api/version', methods=['GET'])
|
|
25
25
|
def get_version():
|
|
26
|
-
"""Return the running PyNode version (for display in the UI).
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
"""Return the running PyNode version (for display in the UI).
|
|
27
|
+
|
|
28
|
+
Defensive on purpose: if the top-level ``pynode`` package resolves to a
|
|
29
|
+
namespace package (e.g. a stale, conflicting ``pynode`` distribution left
|
|
30
|
+
an ``__init__.py``-less folder in site-packages), ``from pynode import
|
|
31
|
+
__version__`` raises ImportError. Fall back to the installed distribution
|
|
32
|
+
metadata rather than returning a 500 for a purely cosmetic endpoint.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
from pynode import __version__ as version
|
|
36
|
+
except Exception:
|
|
37
|
+
try:
|
|
38
|
+
from importlib.metadata import version as _dist_version
|
|
39
|
+
version = _dist_version('pynode-flow')
|
|
40
|
+
except Exception:
|
|
41
|
+
logger.warning("Could not determine PyNode version", exc_info=True)
|
|
42
|
+
version = 'unknown'
|
|
43
|
+
return jsonify({'success': True, 'version': version})
|
|
29
44
|
|
|
30
45
|
|
|
31
46
|
@workflows_bp.route('/api/workflows', methods=['GET'])
|
|
@@ -404,17 +419,29 @@ def deploy_changes():
|
|
|
404
419
|
|
|
405
420
|
@workflows_bp.route('/api/workflow/restart', methods=['POST'])
|
|
406
421
|
def restart_workflow():
|
|
407
|
-
"""Restart
|
|
422
|
+
"""Restart deployed workflow engine(s) without a full redeploy.
|
|
423
|
+
|
|
424
|
+
Restarts every deployed engine by default (used by e.g. bulk/maintenance
|
|
425
|
+
callers). Pass ``?workflow=<id>`` to scope the restart to a single
|
|
426
|
+
workflow - the Deploy button's "Restart" action does this so it only
|
|
427
|
+
affects the currently-viewed flow, not every open tab.
|
|
428
|
+
"""
|
|
408
429
|
manager = _get_manager()
|
|
430
|
+
wid = request.args.get('workflow')
|
|
409
431
|
try:
|
|
410
|
-
# Snapshot under the lock; engine stop/start happens outside it.
|
|
411
432
|
with manager.state_lock:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
433
|
+
if wid is not None:
|
|
434
|
+
if wid not in manager.workflows:
|
|
435
|
+
return jsonify({'success': False, 'error': 'Workflow not found'}), 404
|
|
436
|
+
snapshot = [(wid, manager.deployed_engines.get(wid), manager.workflows[wid]['enabled'])]
|
|
437
|
+
else:
|
|
438
|
+
# Snapshot under the lock; engine stop/start happens outside it.
|
|
439
|
+
snapshot = [
|
|
440
|
+
(w, manager.deployed_engines.get(w), meta['enabled'])
|
|
441
|
+
for w, meta in manager.workflows.items()
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
for w, deployed, enabled in snapshot:
|
|
418
445
|
if not deployed:
|
|
419
446
|
continue
|
|
420
447
|
workflow_data = deployed.export_workflow()
|
|
@@ -431,18 +458,29 @@ def restart_workflow():
|
|
|
431
458
|
|
|
432
459
|
@workflows_bp.route('/api/workflow/stop', methods=['POST'])
|
|
433
460
|
def stop_workflows():
|
|
434
|
-
"""Transiently stop
|
|
461
|
+
"""Transiently stop deployed workflow engine(s).
|
|
435
462
|
|
|
436
463
|
Unlike disabling a workflow, this does NOT change any workflow's
|
|
437
464
|
persisted 'enabled' flag and does NOT write to disk - the next Deploy
|
|
438
465
|
(full save or incremental deploy-changes) starts enabled workflows again.
|
|
466
|
+
|
|
467
|
+
Stops every deployed engine by default (used by e.g. bulk/maintenance
|
|
468
|
+
callers). Pass ``?workflow=<id>`` to scope the stop to a single
|
|
469
|
+
workflow - the Deploy button's "Stop" action does this so it only
|
|
470
|
+
affects the currently-viewed flow, not every open tab.
|
|
439
471
|
"""
|
|
440
472
|
manager = _get_manager()
|
|
473
|
+
wid = request.args.get('workflow')
|
|
441
474
|
try:
|
|
442
|
-
# Snapshot under the lock; engine stop happens outside it.
|
|
443
475
|
with manager.state_lock:
|
|
444
|
-
|
|
445
|
-
|
|
476
|
+
if wid is not None:
|
|
477
|
+
if wid not in manager.workflows:
|
|
478
|
+
return jsonify({'success': False, 'error': 'Workflow not found'}), 404
|
|
479
|
+
snapshot = [manager.deployed_engines.get(wid)]
|
|
480
|
+
else:
|
|
481
|
+
# Snapshot under the lock; engine stop happens outside it.
|
|
482
|
+
snapshot = [manager.deployed_engines.get(w)
|
|
483
|
+
for w in manager.workflows]
|
|
446
484
|
|
|
447
485
|
stopped = 0
|
|
448
486
|
for deployed in snapshot:
|