horus-runtime 0.2.2__tar.gz → 0.3.0__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.
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/PKG-INFO +2 -1
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/pyproject.toml +6 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/tui_subscriber.py +18 -2
- horus_runtime-0.3.0/src/horus_builtin/executor/_remote_function_call.py +117 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/python_exec.py +12 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/python_fn.py +49 -23
- horus_runtime-0.3.0/src/horus_builtin/executor/python_fn_external.py +233 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/command.py +3 -1
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/python.py +114 -3
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/target/local.py +13 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/task/function.py +9 -1
- horus_runtime-0.3.0/src/horus_builtin/task/horus_task.py +220 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/cli.py +8 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/base.py +7 -5
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/store.py +26 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/executor/base.py +18 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/resources.py +44 -5
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/base.py +48 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/channel.py +17 -1
- horus_runtime-0.3.0/tests/unit/core/test_resource_scope.py +262 -0
- horus_runtime-0.3.0/tests/unit/executor/test_python_fn_external.py +188 -0
- horus_runtime-0.3.0/tests/unit/runtime/test_python_function_reference.py +173 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/test_builtin_task.py +93 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_builtin_workflow.py +3 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_conditions.py +3 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_map.py +30 -22
- horus_runtime-0.2.2/src/horus_builtin/task/horus_task.py +0 -116
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/.gitignore +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/LICENSE +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/file.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/folder.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/json.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/pickle.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/artifact_event.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/log_subscriber.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/task_event.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/workflow_event.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/_cwd_lock.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/shell.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/cli.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/confirm.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/dropdown.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/file.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/string.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/middleware/task_log_file.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/middleware/task_time.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/middleware/workflow_time.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/py.typed +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/python_script.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/python_string.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/substitution.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/target/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/task/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/transfer/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/transfer/local_noop.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/tui.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/branch.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/condition.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/dag.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/horus_workflow.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/loop.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/map.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/scheduler.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/errors.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/expander.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/lowering.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/ports.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/context.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/executor/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/executor/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/renderer.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/transport.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/placement.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/runtime/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/runtime/base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/runtime/events.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/status.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/generic.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/strategy.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/condition.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/edge.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/status.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/async_loop.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/bus.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/subscriber.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/transport.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/i18n.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/locale/es/LC_MESSAGES/horus_runtime.mo +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/locale/es/LC_MESSAGES/horus_runtime.po +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/locale/messages.pot +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/logging.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/auto_middleware.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/executor.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/interaction.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/runtime.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/target.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/target_command.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/task.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/transfer.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/workflow.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/packaging.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/py.typed +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/auto_registry.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/auto_registry_product.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/exceptions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/sanitize.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/settings.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/version.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/__init__.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/conftest.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/integration/test_runtime_integration.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/artifact/test_artifact_base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/artifact/test_builtin_artifact.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/artifact/test_store.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/boot/test_boot.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/cli/test_cli.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/cli/test_run.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/core/test_placement.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/core/test_resources.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/common.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/test_builtin_log_subscriber.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/test_event_system.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/test_tui_subscriber.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/executor/test_builtin_executor.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/executor/test_collect_side_artifacts.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/executor/test_executor_base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/i18n/test_i18n.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/interaction/test_builtin_interactions.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/interaction/test_interaction.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/middleware/test_middleware.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/middleware/test_target_command.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/middleware/test_task_log_file.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/registry/test_auto_registry.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/registry/test_schema_generation.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_anchor_local_paths.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_builtin_runtime.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_python_script.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_python_string.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_runtime_base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_substitution.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_channel.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_detach.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_local_target.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_target_base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/conftest.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/test_builtin_function_task.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/test_task_base.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_logging.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_main.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_packaging.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_sanitize.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/transfer/test_builtin_transfer.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/transfer/test_transfer.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_branch_router.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_dynamic_mutation.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_edges.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_loops.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_run_layout.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_scheduler.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_subworkflow.py +0 -0
- {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_workflow.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: horus-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The Horus workflow manager runtime. Execute advanced workflows with multi-environment support, HPC integration, automatic artifact management, and more.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Requires-Dist: click>=8.3.3
|
|
10
|
+
Requires-Dist: cloudpickle>=3.0
|
|
10
11
|
Requires-Dist: loguru~=0.7
|
|
11
12
|
Requires-Dist: pydantic-settings~=2.0
|
|
12
13
|
Requires-Dist: pydantic~=2.0
|
|
@@ -11,6 +11,7 @@ classifiers = [
|
|
|
11
11
|
]
|
|
12
12
|
dependencies = [
|
|
13
13
|
"click>=8.3.3",
|
|
14
|
+
"cloudpickle>=3.0",
|
|
14
15
|
"loguru~=0.7",
|
|
15
16
|
"pydantic~=2.0",
|
|
16
17
|
"pydantic_settings~=2.0",
|
|
@@ -34,6 +35,7 @@ pickle = "horus_builtin.artifact.pickle"
|
|
|
34
35
|
shell = "horus_builtin.executor.shell"
|
|
35
36
|
python_exec = "horus_builtin.executor.python_exec"
|
|
36
37
|
python_fn = "horus_builtin.executor.python_fn"
|
|
38
|
+
python_fn_external = "horus_builtin.executor.python_fn_external"
|
|
37
39
|
|
|
38
40
|
[project.entry-points."horus.interaction"]
|
|
39
41
|
common = "horus_builtin.interaction.common"
|
|
@@ -110,6 +112,10 @@ strict = true
|
|
|
110
112
|
exclude = ["build", "dist"]
|
|
111
113
|
plugins = ["pydantic.mypy"]
|
|
112
114
|
|
|
115
|
+
[[tool.mypy.overrides]]
|
|
116
|
+
module = ["cloudpickle.*"]
|
|
117
|
+
ignore_missing_imports = true
|
|
118
|
+
|
|
113
119
|
[tool.pydantic-mypy]
|
|
114
120
|
init_typed = true
|
|
115
121
|
init_forbid_extra = true
|
|
@@ -29,7 +29,7 @@ import sys
|
|
|
29
29
|
import time
|
|
30
30
|
from collections import deque
|
|
31
31
|
from collections.abc import Iterator
|
|
32
|
-
from contextlib import contextmanager
|
|
32
|
+
from contextlib import contextmanager, suppress
|
|
33
33
|
from typing import TYPE_CHECKING, ClassVar, NamedTuple
|
|
34
34
|
|
|
35
35
|
from pydantic import PrivateAttr
|
|
@@ -258,6 +258,8 @@ class WorkflowTUISubscriber(BaseEventSubscriber):
|
|
|
258
258
|
_last_transfer: tuple[str, float] | None = PrivateAttr(default=None)
|
|
259
259
|
_error: tuple[str, str] | None = PrivateAttr(default=None)
|
|
260
260
|
_paused: bool = PrivateAttr(default=False)
|
|
261
|
+
_finished: bool = PrivateAttr(default=False)
|
|
262
|
+
_finished_at: float | None = PrivateAttr(default=None)
|
|
261
263
|
|
|
262
264
|
def setup(self) -> None:
|
|
263
265
|
"""No startup work needed."""
|
|
@@ -328,6 +330,16 @@ class WorkflowTUISubscriber(BaseEventSubscriber):
|
|
|
328
330
|
self._capture_error(exc)
|
|
329
331
|
raise
|
|
330
332
|
finally:
|
|
333
|
+
# Leave the DAG/table/log view on screen instead of
|
|
334
|
+
# tearing it down the instant the run returns, so the
|
|
335
|
+
# user can still see final task state. Only wait on an
|
|
336
|
+
# interactive TTY, non-interactive runs (CI, pipes)
|
|
337
|
+
# would otherwise hang forever on this.
|
|
338
|
+
self._finished = True
|
|
339
|
+
self._finished_at = time.monotonic()
|
|
340
|
+
if sys.stdin.isatty():
|
|
341
|
+
with suppress(EOFError, KeyboardInterrupt):
|
|
342
|
+
input()
|
|
331
343
|
self._live = None
|
|
332
344
|
finally:
|
|
333
345
|
horus_logger.restore_terminal()
|
|
@@ -459,6 +471,10 @@ class WorkflowTUISubscriber(BaseEventSubscriber):
|
|
|
459
471
|
]
|
|
460
472
|
if self._error is not None:
|
|
461
473
|
sections.append(self._render_error())
|
|
474
|
+
if self._finished:
|
|
475
|
+
sections.append(
|
|
476
|
+
Text(_("Workflow finished, press Enter to close"), style="dim")
|
|
477
|
+
)
|
|
462
478
|
return Group(*sections)
|
|
463
479
|
|
|
464
480
|
def _render_header(self, workflow: BaseWorkflow) -> RenderableType:
|
|
@@ -467,7 +483,7 @@ class WorkflowTUISubscriber(BaseEventSubscriber):
|
|
|
467
483
|
elapsed = (
|
|
468
484
|
None
|
|
469
485
|
if self._started_at is None
|
|
470
|
-
else time.monotonic() - self._started_at
|
|
486
|
+
else (self._finished_at or time.monotonic()) - self._started_at
|
|
471
487
|
)
|
|
472
488
|
line = Text.assemble(
|
|
473
489
|
(workflow.name, "bold"),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#
|
|
2
|
+
# horus-runtime
|
|
3
|
+
# Copyright (C) 2026 Temple Compute
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU Affero General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
"""
|
|
19
|
+
Launcher shipped to the target by
|
|
20
|
+
:class:`~horus_builtin.executor.python_fn_external.
|
|
21
|
+
ExternalPythonFunctionExecutor`.
|
|
22
|
+
|
|
23
|
+
It is copied to the task working directory and run there by the target's
|
|
24
|
+
interpreter, so it is deliberately a *script*: it must import nothing from
|
|
25
|
+
horus-runtime at module level (the payload may pull horus classes in while
|
|
26
|
+
unpickling, but the launcher itself must start on any interpreter that has
|
|
27
|
+
cloudpickle).
|
|
28
|
+
|
|
29
|
+
Protocol, both halves of which are hard-coded here and in the executor:
|
|
30
|
+
|
|
31
|
+
- ``argv[1]``: cloudpickled ``(func, kwargs)`` payload to read.
|
|
32
|
+
- ``argv[2]``: where to write the cloudpickled outcome.
|
|
33
|
+
- outcome: ``{"ok": bool, "value": ..., "traceback": str | None,
|
|
34
|
+
"exception": BaseException | None}``.
|
|
35
|
+
|
|
36
|
+
The outcome file, not the exit code, is what the executor believes: an exit
|
|
37
|
+
code can only say *that* something failed, and "subprocess exited 1" is a
|
|
38
|
+
useless thing to hand a scientist whose function raised on line 40.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
import inspect
|
|
43
|
+
import sys
|
|
44
|
+
import traceback
|
|
45
|
+
from collections.abc import Awaitable
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
import cloudpickle
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _resolve(awaitable: Awaitable[Any]) -> Any:
|
|
52
|
+
"""Await *awaitable*, the entry point ``asyncio.run`` needs."""
|
|
53
|
+
return await awaitable
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _write_outcome(result_path: str, outcome: dict[str, Any]) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Write *outcome* to *result_path*, degrading to text if it will not pickle.
|
|
59
|
+
|
|
60
|
+
A return value or an exception can be unpicklable (an open file handle
|
|
61
|
+
captured in ``__init__``, say). Losing the object is survivable; losing
|
|
62
|
+
the traceback that explains the run is not, so the fallback keeps the
|
|
63
|
+
text.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
data = cloudpickle.dumps(outcome)
|
|
67
|
+
except Exception:
|
|
68
|
+
data = cloudpickle.dumps(
|
|
69
|
+
{
|
|
70
|
+
"ok": False,
|
|
71
|
+
"value": None,
|
|
72
|
+
"exception": None,
|
|
73
|
+
"traceback": (
|
|
74
|
+
f"{outcome.get('traceback') or ''}\n"
|
|
75
|
+
f"Could not send the result back to the orchestrator:\n"
|
|
76
|
+
f"{traceback.format_exc()}"
|
|
77
|
+
).strip(),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
with open(result_path, "wb") as fh:
|
|
81
|
+
fh.write(data)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(payload_path: str, result_path: str) -> int:
|
|
85
|
+
"""
|
|
86
|
+
Call the pickled function and report the outcome. Returns an exit code.
|
|
87
|
+
"""
|
|
88
|
+
with open(payload_path, "rb") as fh:
|
|
89
|
+
func, kwargs = cloudpickle.load(fh)
|
|
90
|
+
|
|
91
|
+
outcome: dict[str, Any]
|
|
92
|
+
try:
|
|
93
|
+
value = func(**kwargs)
|
|
94
|
+
# The in-process executor awaits coroutine functions, so an async task
|
|
95
|
+
# body must keep working here.
|
|
96
|
+
if inspect.isawaitable(value):
|
|
97
|
+
value = asyncio.run(_resolve(value))
|
|
98
|
+
outcome = {
|
|
99
|
+
"ok": True,
|
|
100
|
+
"value": value,
|
|
101
|
+
"exception": None,
|
|
102
|
+
"traceback": None,
|
|
103
|
+
}
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
outcome = {
|
|
106
|
+
"ok": False,
|
|
107
|
+
"value": None,
|
|
108
|
+
"exception": exc,
|
|
109
|
+
"traceback": traceback.format_exc(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_write_outcome(result_path, outcome)
|
|
113
|
+
return 0 if outcome["ok"] else 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
sys.exit(main(sys.argv[1], sys.argv[2]))
|
|
@@ -20,12 +20,15 @@ Defines the PythonExecExecutor class, which represents an executor that runs a
|
|
|
20
20
|
a Python code task in-process in the Horus runtime.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
import os
|
|
23
24
|
from typing import TYPE_CHECKING, ClassVar
|
|
24
25
|
|
|
25
26
|
from horus_builtin.executor._cwd_lock import chdir_locked
|
|
26
27
|
from horus_builtin.runtime.python_string import PythonCodeStringRuntime
|
|
27
28
|
from horus_runtime.context import HorusContext
|
|
28
29
|
from horus_runtime.core.executor.base import BaseExecutor, RuntimeFilterType
|
|
30
|
+
from horus_runtime.core.resources import InProcessScope, ResourceScope
|
|
31
|
+
from horus_runtime.core.target.channel import ChannelProcess
|
|
29
32
|
from horus_runtime.i18n import tr as _
|
|
30
33
|
from horus_runtime.settings import runtime_settings
|
|
31
34
|
|
|
@@ -46,6 +49,15 @@ class PythonExecExecutor(BaseExecutor):
|
|
|
46
49
|
|
|
47
50
|
runtimes: ClassVar[RuntimeFilterType] = (PythonCodeStringRuntime,)
|
|
48
51
|
|
|
52
|
+
async def resource_scope(
|
|
53
|
+
self, task: "BaseTask", process: ChannelProcess | None = None
|
|
54
|
+
) -> ResourceScope:
|
|
55
|
+
"""
|
|
56
|
+
Declare that the work runs inside the orchestrator process.
|
|
57
|
+
"""
|
|
58
|
+
del task, process
|
|
59
|
+
return InProcessScope(pid=os.getpid())
|
|
60
|
+
|
|
49
61
|
async def _execute(self, task: "BaseTask") -> None:
|
|
50
62
|
"""
|
|
51
63
|
Runs the task in-process by executing the Python code specified in the
|
|
@@ -20,6 +20,7 @@ Python executor for in-memory workflows in horus-runtime. (Function
|
|
|
20
20
|
executor).
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
import os
|
|
23
24
|
from inspect import isawaitable
|
|
24
25
|
from typing import ClassVar
|
|
25
26
|
|
|
@@ -30,11 +31,49 @@ from horus_builtin.runtime.python import (
|
|
|
30
31
|
)
|
|
31
32
|
from horus_runtime.core.artifact.base import BaseArtifact
|
|
32
33
|
from horus_runtime.core.executor.base import BaseExecutor, RuntimeFilterType
|
|
34
|
+
from horus_runtime.core.resources import InProcessScope, ResourceScope
|
|
35
|
+
from horus_runtime.core.target.channel import ChannelProcess
|
|
33
36
|
from horus_runtime.core.task.base import BaseTask
|
|
34
37
|
from horus_runtime.i18n import tr as _
|
|
35
38
|
from horus_runtime.logging import horus_logger
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
async def parse_result_artifacts(
|
|
42
|
+
task: BaseTask, result: PythonFunctionReturnType
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Parse the result of a Python function execution to extract any declared
|
|
46
|
+
side-product artifacts.
|
|
47
|
+
|
|
48
|
+
Module-level so the out-of-process executor applies the exact same rules
|
|
49
|
+
to a result that came back over a pipe as the in-process one does to a
|
|
50
|
+
live return value.
|
|
51
|
+
"""
|
|
52
|
+
if isawaitable(result):
|
|
53
|
+
result = await result
|
|
54
|
+
|
|
55
|
+
if result is None:
|
|
56
|
+
return
|
|
57
|
+
if isinstance(result, BaseArtifact):
|
|
58
|
+
task.side_artifacts = [result]
|
|
59
|
+
return
|
|
60
|
+
if isinstance(result, list) and all(
|
|
61
|
+
isinstance(r, BaseArtifact) for r in result
|
|
62
|
+
):
|
|
63
|
+
task.side_artifacts = result
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
horus_logger.log.warning(
|
|
67
|
+
_(
|
|
68
|
+
"Task %(task_id)s returned an unexpected value from its "
|
|
69
|
+
"Python function runtime. Expected BaseArtifact, "
|
|
70
|
+
"list[BaseArtifact], or None; got: %(result)s. Skipping side "
|
|
71
|
+
"artifact handling."
|
|
72
|
+
)
|
|
73
|
+
% {"task_id": task.id, "result": result}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
38
77
|
class PythonFunctionExecutor(BaseExecutor):
|
|
39
78
|
"""
|
|
40
79
|
Executor for running Python functions in-memory.
|
|
@@ -48,6 +87,15 @@ class PythonFunctionExecutor(BaseExecutor):
|
|
|
48
87
|
|
|
49
88
|
runtimes: ClassVar[RuntimeFilterType] = (PythonFunctionRuntime,)
|
|
50
89
|
|
|
90
|
+
async def resource_scope(
|
|
91
|
+
self, task: "BaseTask", process: ChannelProcess | None = None
|
|
92
|
+
) -> ResourceScope:
|
|
93
|
+
"""
|
|
94
|
+
Declare that the work runs inside the orchestrator process.
|
|
95
|
+
"""
|
|
96
|
+
del task, process
|
|
97
|
+
return InProcessScope(pid=os.getpid())
|
|
98
|
+
|
|
51
99
|
async def _execute(self, task: "BaseTask") -> None:
|
|
52
100
|
"""
|
|
53
101
|
Executes the Python function specified in the task's runtime.
|
|
@@ -78,26 +126,4 @@ class PythonFunctionExecutor(BaseExecutor):
|
|
|
78
126
|
Parse the result of a Python function execution to extract any declared
|
|
79
127
|
side-product artifacts.
|
|
80
128
|
"""
|
|
81
|
-
|
|
82
|
-
result = await result
|
|
83
|
-
|
|
84
|
-
if result is None:
|
|
85
|
-
return
|
|
86
|
-
if isinstance(result, BaseArtifact):
|
|
87
|
-
task.side_artifacts = [result]
|
|
88
|
-
return
|
|
89
|
-
if isinstance(result, list) and all(
|
|
90
|
-
isinstance(r, BaseArtifact) for r in result
|
|
91
|
-
):
|
|
92
|
-
task.side_artifacts = result
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
horus_logger.log.warning(
|
|
96
|
-
_(
|
|
97
|
-
"Task %(task_id)s returned an unexpected value from its "
|
|
98
|
-
"Python function runtime. Expected BaseArtifact, "
|
|
99
|
-
"list[BaseArtifact], or None; got: %(result)s. Skipping side "
|
|
100
|
-
"artifact handling."
|
|
101
|
-
)
|
|
102
|
-
% {"task_id": task.id, "result": result}
|
|
103
|
-
)
|
|
129
|
+
await parse_result_artifacts(task, result)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#
|
|
2
|
+
# horus-runtime
|
|
3
|
+
# Copyright (C) 2026 Temple Compute
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU Affero General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
"""
|
|
19
|
+
Run a ``python_function`` runtime in a subprocess **on the task's target**
|
|
20
|
+
instead of inside the orchestrator.
|
|
21
|
+
|
|
22
|
+
Why this exists, given ``python_fn`` already runs functions:
|
|
23
|
+
|
|
24
|
+
- ``PythonFunctionExecutor`` ignores ``task.target``. A function task with an
|
|
25
|
+
SSH target runs on the orchestrator, which is a correctness bug and not
|
|
26
|
+
merely a metrics gap. This executor honours the target like every other
|
|
27
|
+
executor does, by going through ``target.run_command``.
|
|
28
|
+
- Because the work is a real process, it inherits the default
|
|
29
|
+
:meth:`BaseExecutor.resource_scope` (a ``ProcessTreeScope`` with a live
|
|
30
|
+
pid) and becomes measurable with no extra code here.
|
|
31
|
+
|
|
32
|
+
**Limitation: live DAG mutation does not work here.** A task body may call
|
|
33
|
+
``workflow.add_task()`` / ``add_edge()`` on the running workflow (see
|
|
34
|
+
``core/workflow/base.py``, "Safe to call from inside a running task"). That
|
|
35
|
+
depends on sharing memory with the orchestrator and cannot cross a process
|
|
36
|
+
boundary, so tasks that mutate the DAG must keep using the in-process
|
|
37
|
+
``python_function`` executor. The same reasoning applies to any function that
|
|
38
|
+
takes the live ``task``; rather than hand it a lookalike copy whose mutations
|
|
39
|
+
would silently go nowhere, this executor refuses such a function up front.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import asyncio
|
|
43
|
+
import shlex
|
|
44
|
+
import sys
|
|
45
|
+
from contextlib import aclosing
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
48
|
+
|
|
49
|
+
import cloudpickle
|
|
50
|
+
|
|
51
|
+
from horus_builtin.executor.python_fn import parse_result_artifacts
|
|
52
|
+
from horus_builtin.runtime.python import PythonFunctionRuntime
|
|
53
|
+
from horus_runtime.core.executor.base import BaseExecutor, RuntimeFilterType
|
|
54
|
+
from horus_runtime.core.task.exceptions import TaskExecutionError
|
|
55
|
+
from horus_runtime.i18n import tr as _
|
|
56
|
+
from horus_runtime.logging import horus_logger
|
|
57
|
+
from horus_runtime.settings import runtime_settings
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
from horus_runtime.core.task.base import BaseTask
|
|
61
|
+
|
|
62
|
+
_LAUNCHER_SOURCE = Path(__file__).with_name("_remote_function_call.py")
|
|
63
|
+
_LAUNCHER_NAME = ".horus_function_call.py"
|
|
64
|
+
_PAYLOAD_NAME = ".horus_function_payload.pkl"
|
|
65
|
+
_RESULT_NAME = ".horus_function_result.pkl"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ExternalPythonFunctionExecutor(BaseExecutor):
|
|
69
|
+
"""
|
|
70
|
+
Execute a Python function in a subprocess on the task's target.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
kind: str = "python_function_external"
|
|
74
|
+
kind_name: ClassVar[str] = "External Python Function Executor"
|
|
75
|
+
kind_description: ClassVar[str] = _(
|
|
76
|
+
"Executes a Python function in a subprocess on the task's target."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
runtimes: ClassVar[RuntimeFilterType] = (PythonFunctionRuntime,)
|
|
80
|
+
|
|
81
|
+
python: str | None = None
|
|
82
|
+
"""
|
|
83
|
+
Interpreter used on the target. It needs ``cloudpickle`` importable, plus
|
|
84
|
+
whatever the function itself imports, including ``horus_runtime`` if the
|
|
85
|
+
function takes or returns artifacts.
|
|
86
|
+
|
|
87
|
+
``None`` (the default) means: the orchestrator's own interpreter when the
|
|
88
|
+
target is co-located with it, otherwise plain ``python``. The first is the
|
|
89
|
+
one interpreter that provably satisfies those requirements, since it is
|
|
90
|
+
running this code; the second is the only thing that can be assumed about
|
|
91
|
+
a machine we have never seen.
|
|
92
|
+
|
|
93
|
+
Environment management is deliberately *not* reimplemented here. The
|
|
94
|
+
``horus-environments`` executors already build conda/uv/virtualenv
|
|
95
|
+
environments on the target and put the interpreter at a known path, so
|
|
96
|
+
composing with them is a matter of pointing this field at one, e.g.
|
|
97
|
+
``python: .horus_python_environment/bin/python``. Relative paths resolve
|
|
98
|
+
against the task working directory, which is the command's cwd.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def _interpreter(self, task: "BaseTask") -> str:
|
|
102
|
+
"""Return the interpreter command to invoke on the target."""
|
|
103
|
+
if self.python is not None:
|
|
104
|
+
return self.python
|
|
105
|
+
if task.target.is_orchestrator_local:
|
|
106
|
+
return sys.executable
|
|
107
|
+
return "python"
|
|
108
|
+
|
|
109
|
+
async def _execute(self, task: "BaseTask") -> None:
|
|
110
|
+
"""
|
|
111
|
+
Ship the function to the target, run it there, bring the result back.
|
|
112
|
+
"""
|
|
113
|
+
assert isinstance(task.runtime, PythonFunctionRuntime)
|
|
114
|
+
func, kwargs = await task.runtime.setup_runtime(task)
|
|
115
|
+
|
|
116
|
+
if "task" in kwargs:
|
|
117
|
+
# PythonFunctionRuntime injects the live task whenever the
|
|
118
|
+
# signature asks for it. A copy of it on the far side of a pipe
|
|
119
|
+
# would look right and do nothing (mutations, DAG edits and
|
|
120
|
+
# interactions all die with the subprocess), so fail loudly here
|
|
121
|
+
# instead of subtly there.
|
|
122
|
+
raise TaskExecutionError(
|
|
123
|
+
_(
|
|
124
|
+
"Task %(task_id)s uses the '%(kind)s' executor but its "
|
|
125
|
+
"function accepts a 'task' argument. The live task object "
|
|
126
|
+
"cannot cross a process boundary; use the in-process "
|
|
127
|
+
"'python_function' executor, or drop the 'task' parameter "
|
|
128
|
+
"(and any **kwargs that would receive it)."
|
|
129
|
+
)
|
|
130
|
+
% {"task_id": task.id, "kind": self.kind}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await task.target.mkdir(task.working_dir)
|
|
134
|
+
|
|
135
|
+
# cloudpickle rather than pickle: closing over local state is a
|
|
136
|
+
# supported way to build a workflow programmatically, and plain pickle
|
|
137
|
+
# cannot represent a closure. It also already prefers a by-reference
|
|
138
|
+
# pickle for anything importable, so the cheap payload is the default
|
|
139
|
+
# and the expensive one only appears when it is the only option.
|
|
140
|
+
payload = f"{task.working_dir}/{_PAYLOAD_NAME}"
|
|
141
|
+
result_path = f"{task.working_dir}/{_RESULT_NAME}"
|
|
142
|
+
launcher = f"{task.working_dir}/{_LAUNCHER_NAME}"
|
|
143
|
+
await task.target.put_file(cloudpickle.dumps((func, kwargs)), payload)
|
|
144
|
+
await task.target.put_file(_LAUNCHER_SOURCE, launcher)
|
|
145
|
+
|
|
146
|
+
command = (
|
|
147
|
+
f"{self._interpreter(task)} {shlex.quote(launcher)} "
|
|
148
|
+
f"{shlex.quote(payload)} {shlex.quote(result_path)}"
|
|
149
|
+
)
|
|
150
|
+
horus_logger.log.debug(
|
|
151
|
+
_("Executing function for task %(task_id)s: %(command)s")
|
|
152
|
+
% {"task_id": task.id, "command": command}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
proc = await task.target.run_command(
|
|
156
|
+
command,
|
|
157
|
+
cwd=task.working_dir,
|
|
158
|
+
env={
|
|
159
|
+
runtime_settings.SIDE_ARTIFACTS_DIR_ENV: str(
|
|
160
|
+
task.side_artifacts_dir
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
try:
|
|
165
|
+
async with aclosing(proc.stream()) as stream:
|
|
166
|
+
async for stream_name, line in stream:
|
|
167
|
+
text = line.decode("utf-8", errors="replace").rstrip()
|
|
168
|
+
if stream_name == "stdout":
|
|
169
|
+
horus_logger.log.info(text)
|
|
170
|
+
else:
|
|
171
|
+
horus_logger.log.warning(text)
|
|
172
|
+
except asyncio.CancelledError:
|
|
173
|
+
proc.kill()
|
|
174
|
+
await proc.wait()
|
|
175
|
+
raise
|
|
176
|
+
await proc.wait()
|
|
177
|
+
|
|
178
|
+
outcome = await self._read_outcome(task, result_path, proc.returncode)
|
|
179
|
+
await parse_result_artifacts(task, outcome["value"])
|
|
180
|
+
|
|
181
|
+
async def _read_outcome(
|
|
182
|
+
self, task: "BaseTask", result_path: str, returncode: int | None
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
"""
|
|
185
|
+
Read the launcher's outcome file and re-raise a remote failure here.
|
|
186
|
+
|
|
187
|
+
The exit code is only a fallback: what the caller needs is the
|
|
188
|
+
exception the function raised, with its own traceback. When it made it
|
|
189
|
+
back it is re-raised as itself, so ``except MyError`` around a workflow
|
|
190
|
+
run still catches what it always caught, with the target-side frames
|
|
191
|
+
attached as a note.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
raw = await task.target.get_file(result_path)
|
|
195
|
+
outcome: dict[str, Any] = cloudpickle.loads(raw)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
# No outcome file at all: the interpreter never got far enough to
|
|
198
|
+
# write one (missing python, missing cloudpickle, killed job). The
|
|
199
|
+
# streamed stderr above is the real diagnosis; point at it.
|
|
200
|
+
raise TaskExecutionError(
|
|
201
|
+
_(
|
|
202
|
+
"Function subprocess for task %(task_id)s produced no "
|
|
203
|
+
"result (exit code %(code)s); see the task log for the "
|
|
204
|
+
"interpreter's own output. Cause: %(err)s"
|
|
205
|
+
)
|
|
206
|
+
% {
|
|
207
|
+
"task_id": task.id,
|
|
208
|
+
"code": returncode,
|
|
209
|
+
"err": exc,
|
|
210
|
+
}
|
|
211
|
+
) from exc
|
|
212
|
+
|
|
213
|
+
if outcome["ok"]:
|
|
214
|
+
return outcome
|
|
215
|
+
|
|
216
|
+
remote_traceback = outcome["traceback"] or ""
|
|
217
|
+
exception = outcome["exception"]
|
|
218
|
+
if isinstance(exception, BaseException):
|
|
219
|
+
exception.add_note(
|
|
220
|
+
_("Raised on target %(target)s:\n%(traceback)s")
|
|
221
|
+
% {
|
|
222
|
+
"target": task.target.location_id,
|
|
223
|
+
"traceback": remote_traceback,
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
raise exception
|
|
227
|
+
raise TaskExecutionError(
|
|
228
|
+
_("Function failed on target %(target)s:\n%(traceback)s")
|
|
229
|
+
% {
|
|
230
|
+
"target": task.target.location_id,
|
|
231
|
+
"traceback": remote_traceback,
|
|
232
|
+
}
|
|
233
|
+
)
|
|
@@ -21,6 +21,8 @@ Command implementation for the runtime.
|
|
|
21
21
|
|
|
22
22
|
from typing import TYPE_CHECKING, ClassVar
|
|
23
23
|
|
|
24
|
+
from pydantic import Field
|
|
25
|
+
|
|
24
26
|
from horus_builtin.runtime.substitution import substitute
|
|
25
27
|
from horus_runtime.context import HorusContext
|
|
26
28
|
from horus_runtime.core.runtime.base import BaseRuntime
|
|
@@ -51,7 +53,7 @@ class CommandRuntime(BaseRuntime[str]):
|
|
|
51
53
|
through untouched.
|
|
52
54
|
"""
|
|
53
55
|
|
|
54
|
-
formatted_command: str = ""
|
|
56
|
+
formatted_command: str = Field(default="", exclude=True)
|
|
55
57
|
"""
|
|
56
58
|
The formatted command after processing any placeholders.
|
|
57
59
|
"""
|