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.
Files changed (191) hide show
  1. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/PKG-INFO +2 -1
  2. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/pyproject.toml +6 -0
  3. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/tui_subscriber.py +18 -2
  4. horus_runtime-0.3.0/src/horus_builtin/executor/_remote_function_call.py +117 -0
  5. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/python_exec.py +12 -0
  6. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/python_fn.py +49 -23
  7. horus_runtime-0.3.0/src/horus_builtin/executor/python_fn_external.py +233 -0
  8. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/command.py +3 -1
  9. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/python.py +114 -3
  10. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/target/local.py +13 -0
  11. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/task/function.py +9 -1
  12. horus_runtime-0.3.0/src/horus_builtin/task/horus_task.py +220 -0
  13. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/cli.py +8 -0
  14. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/base.py +7 -5
  15. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/store.py +26 -0
  16. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/executor/base.py +18 -0
  17. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/resources.py +44 -5
  18. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/base.py +48 -0
  19. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/channel.py +17 -1
  20. horus_runtime-0.3.0/tests/unit/core/test_resource_scope.py +262 -0
  21. horus_runtime-0.3.0/tests/unit/executor/test_python_fn_external.py +188 -0
  22. horus_runtime-0.3.0/tests/unit/runtime/test_python_function_reference.py +173 -0
  23. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/test_builtin_task.py +93 -0
  24. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_builtin_workflow.py +3 -0
  25. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_conditions.py +3 -0
  26. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_map.py +30 -22
  27. horus_runtime-0.2.2/src/horus_builtin/task/horus_task.py +0 -116
  28. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/.gitignore +0 -0
  29. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/LICENSE +0 -0
  30. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/__init__.py +0 -0
  31. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/__init__.py +0 -0
  32. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/file.py +0 -0
  33. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/folder.py +0 -0
  34. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/json.py +0 -0
  35. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/artifact/pickle.py +0 -0
  36. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/__init__.py +0 -0
  37. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/artifact_event.py +0 -0
  38. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/log_subscriber.py +0 -0
  39. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/task_event.py +0 -0
  40. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/event/workflow_event.py +0 -0
  41. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/__init__.py +0 -0
  42. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/_cwd_lock.py +0 -0
  43. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/executor/shell.py +0 -0
  44. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/__init__.py +0 -0
  45. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/cli.py +0 -0
  46. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/__init__.py +0 -0
  47. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/confirm.py +0 -0
  48. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/dropdown.py +0 -0
  49. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/file.py +0 -0
  50. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/interaction/common/string.py +0 -0
  51. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/middleware/task_log_file.py +0 -0
  52. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/middleware/task_time.py +0 -0
  53. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/middleware/workflow_time.py +0 -0
  54. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/py.typed +0 -0
  55. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/__init__.py +0 -0
  56. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/python_script.py +0 -0
  57. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/python_string.py +0 -0
  58. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/runtime/substitution.py +0 -0
  59. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/target/__init__.py +0 -0
  60. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/task/__init__.py +0 -0
  61. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/transfer/__init__.py +0 -0
  62. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/transfer/local_noop.py +0 -0
  63. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/tui.py +0 -0
  64. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/__init__.py +0 -0
  65. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/branch.py +0 -0
  66. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/condition.py +0 -0
  67. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/dag.py +0 -0
  68. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/horus_workflow.py +0 -0
  69. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/loop.py +0 -0
  70. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/map.py +0 -0
  71. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/scheduler.py +0 -0
  72. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/__init__.py +0 -0
  73. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/errors.py +0 -0
  74. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/expander.py +0 -0
  75. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/lowering.py +0 -0
  76. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_builtin/workflow/subworkflow/ports.py +0 -0
  77. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/__init__.py +0 -0
  78. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/context.py +0 -0
  79. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/__init__.py +0 -0
  80. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/__init__.py +0 -0
  81. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/artifact/exceptions.py +0 -0
  82. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/executor/__init__.py +0 -0
  83. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/executor/exceptions.py +0 -0
  84. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/__init__.py +0 -0
  85. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/base.py +0 -0
  86. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/exceptions.py +0 -0
  87. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/renderer.py +0 -0
  88. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/interaction/transport.py +0 -0
  89. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/placement.py +0 -0
  90. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/runtime/__init__.py +0 -0
  91. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/runtime/base.py +0 -0
  92. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/runtime/events.py +0 -0
  93. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/__init__.py +0 -0
  94. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/target/exceptions.py +0 -0
  95. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/__init__.py +0 -0
  96. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/base.py +0 -0
  97. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/exceptions.py +0 -0
  98. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/task/status.py +0 -0
  99. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/__init__.py +0 -0
  100. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/exceptions.py +0 -0
  101. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/generic.py +0 -0
  102. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/transfer/strategy.py +0 -0
  103. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/__init__.py +0 -0
  104. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/base.py +0 -0
  105. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/condition.py +0 -0
  106. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/edge.py +0 -0
  107. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/exceptions.py +0 -0
  108. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/core/workflow/status.py +0 -0
  109. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/__init__.py +0 -0
  110. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/async_loop.py +0 -0
  111. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/base.py +0 -0
  112. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/bus.py +0 -0
  113. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/subscriber.py +0 -0
  114. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/event/transport.py +0 -0
  115. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/i18n.py +0 -0
  116. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/locale/es/LC_MESSAGES/horus_runtime.mo +0 -0
  117. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/locale/es/LC_MESSAGES/horus_runtime.po +0 -0
  118. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/locale/messages.pot +0 -0
  119. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/logging.py +0 -0
  120. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/__init__.py +0 -0
  121. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/auto_middleware.py +0 -0
  122. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/executor.py +0 -0
  123. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/interaction.py +0 -0
  124. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/runtime.py +0 -0
  125. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/target.py +0 -0
  126. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/target_command.py +0 -0
  127. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/task.py +0 -0
  128. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/transfer.py +0 -0
  129. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/middleware/workflow.py +0 -0
  130. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/packaging.py +0 -0
  131. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/py.typed +0 -0
  132. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/__init__.py +0 -0
  133. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/auto_registry.py +0 -0
  134. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/auto_registry_product.py +0 -0
  135. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/registry/exceptions.py +0 -0
  136. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/sanitize.py +0 -0
  137. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/settings.py +0 -0
  138. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/src/horus_runtime/version.py +0 -0
  139. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/__init__.py +0 -0
  140. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/conftest.py +0 -0
  141. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/integration/test_runtime_integration.py +0 -0
  142. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/artifact/test_artifact_base.py +0 -0
  143. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/artifact/test_builtin_artifact.py +0 -0
  144. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/artifact/test_store.py +0 -0
  145. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/boot/test_boot.py +0 -0
  146. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/cli/test_cli.py +0 -0
  147. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/cli/test_run.py +0 -0
  148. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/core/test_placement.py +0 -0
  149. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/core/test_resources.py +0 -0
  150. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/common.py +0 -0
  151. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/test_builtin_log_subscriber.py +0 -0
  152. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/test_event_system.py +0 -0
  153. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/event/test_tui_subscriber.py +0 -0
  154. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/executor/test_builtin_executor.py +0 -0
  155. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/executor/test_collect_side_artifacts.py +0 -0
  156. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/executor/test_executor_base.py +0 -0
  157. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/i18n/test_i18n.py +0 -0
  158. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/interaction/test_builtin_interactions.py +0 -0
  159. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/interaction/test_interaction.py +0 -0
  160. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/middleware/test_middleware.py +0 -0
  161. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/middleware/test_target_command.py +0 -0
  162. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/middleware/test_task_log_file.py +0 -0
  163. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/registry/test_auto_registry.py +0 -0
  164. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/registry/test_schema_generation.py +0 -0
  165. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_anchor_local_paths.py +0 -0
  166. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_builtin_runtime.py +0 -0
  167. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_python_script.py +0 -0
  168. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_python_string.py +0 -0
  169. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_runtime_base.py +0 -0
  170. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/runtime/test_substitution.py +0 -0
  171. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_channel.py +0 -0
  172. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_detach.py +0 -0
  173. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_local_target.py +0 -0
  174. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/target/test_target_base.py +0 -0
  175. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/conftest.py +0 -0
  176. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/test_builtin_function_task.py +0 -0
  177. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/task/test_task_base.py +0 -0
  178. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_logging.py +0 -0
  179. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_main.py +0 -0
  180. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_packaging.py +0 -0
  181. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/test_sanitize.py +0 -0
  182. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/transfer/test_builtin_transfer.py +0 -0
  183. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/transfer/test_transfer.py +0 -0
  184. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_branch_router.py +0 -0
  185. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_dynamic_mutation.py +0 -0
  186. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_edges.py +0 -0
  187. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_loops.py +0 -0
  188. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_run_layout.py +0 -0
  189. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_scheduler.py +0 -0
  190. {horus_runtime-0.2.2 → horus_runtime-0.3.0}/tests/unit/workflow/test_subworkflow.py +0 -0
  191. {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.2.2
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
- if isawaitable(result):
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
  """