hypershell 2.7.0__tar.gz → 2.7.1__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 (176) hide show
  1. hypershell-2.7.1/.github/FUNDING.yml +12 -0
  2. hypershell-2.7.1/.github/workflows/publish.yml +19 -0
  3. hypershell-2.7.1/.github/workflows/tests.yml +24 -0
  4. {hypershell-2.7.0 → hypershell-2.7.1}/PKG-INFO +2 -1
  5. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/index.rst +1 -0
  6. {hypershell-2.7.0 → hypershell-2.7.1}/docs/conf.py +1 -1
  7. {hypershell-2.7.0 → hypershell-2.7.1}/docs/getting_started.rst +4 -4
  8. {hypershell-2.7.0 → hypershell-2.7.1}/pyproject.toml +12 -2
  9. {hypershell-2.7.0 → hypershell-2.7.1}/share/man/man1/hs.1 +1 -1
  10. {hypershell-2.7.0 → hypershell-2.7.1}/share/man/man1/hsx.1 +1 -1
  11. {hypershell-2.7.0 → hypershell-2.7.1}/share/man/man1/hyper-shell.1 +1 -1
  12. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/__init__.py +7 -1
  13. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/config.py +5 -5
  14. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/data/__init__.py +1 -1
  15. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/submit.py +5 -4
  16. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/task.py +8 -8
  17. hypershell-2.7.1/tests/__init__.py +75 -0
  18. hypershell-2.7.1/tests/conftest.py +27 -0
  19. hypershell-2.7.1/tests/test_cluster.py +184 -0
  20. hypershell-2.7.1/tests/test_initdb.py +82 -0
  21. hypershell-2.7.1/tests/test_main.py +179 -0
  22. hypershell-2.7.1/tests/test_server.py +102 -0
  23. hypershell-2.7.1/tests/test_submit.py +214 -0
  24. {hypershell-2.7.0 → hypershell-2.7.1}/uv.lock +25 -1
  25. hypershell-2.7.0/docs/_static/logo.svg +0 -19
  26. hypershell-2.7.0/tests/__init__.py +0 -4
  27. hypershell-2.7.0/tests/unit/__init__.py +0 -4
  28. hypershell-2.7.0/tests/unit/core/__init__.py +0 -2
  29. {hypershell-2.7.0 → hypershell-2.7.1}/.dockerignore +0 -0
  30. {hypershell-2.7.0 → hypershell-2.7.1}/.gitignore +0 -0
  31. {hypershell-2.7.0 → hypershell-2.7.1}/.readthedocs.yaml +0 -0
  32. {hypershell-2.7.0 → hypershell-2.7.1}/Apptainer +0 -0
  33. {hypershell-2.7.0 → hypershell-2.7.1}/CODE_OF_CONDUCT.md +0 -0
  34. {hypershell-2.7.0 → hypershell-2.7.1}/Dockerfile +0 -0
  35. {hypershell-2.7.0 → hypershell-2.7.1}/LICENSE +0 -0
  36. {hypershell-2.7.0 → hypershell-2.7.1}/README.rst +0 -0
  37. {hypershell-2.7.0 → hypershell-2.7.1}/docs/Makefile +0 -0
  38. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/citation.rst +0 -0
  39. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/client_desc.rst +0 -0
  40. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/client_help.rst +0 -0
  41. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/client_usage.rst +0 -0
  42. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/cluster_desc.rst +0 -0
  43. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/cluster_help.rst +0 -0
  44. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/cluster_usage.rst +0 -0
  45. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_edit_desc.rst +0 -0
  46. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_edit_help.rst +0 -0
  47. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_edit_usage.rst +0 -0
  48. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_get_desc.rst +0 -0
  49. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_get_help.rst +0 -0
  50. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_get_usage.rst +0 -0
  51. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_intro.rst +0 -0
  52. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_intro_alt.rst +0 -0
  53. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_param_ref.rst +0 -0
  54. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_pythonpath.rst +0 -0
  55. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_set_desc.rst +0 -0
  56. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_set_help.rst +0 -0
  57. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_set_usage.rst +0 -0
  58. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_site_vars.rst +0 -0
  59. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_task_env.rst +0 -0
  60. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_task_env_alt.rst +0 -0
  61. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_which_desc.rst +0 -0
  62. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_which_help.rst +0 -0
  63. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/config_which_usage.rst +0 -0
  64. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/database.rst +0 -0
  65. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/database_alt.rst +0 -0
  66. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/desc.rst +0 -0
  67. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/exit_status.rst +0 -0
  68. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/initdb_desc.rst +0 -0
  69. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/initdb_help.rst +0 -0
  70. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/initdb_usage.rst +0 -0
  71. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/license.rst +0 -0
  72. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/server_desc.rst +0 -0
  73. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/server_help.rst +0 -0
  74. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/server_usage.rst +0 -0
  75. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/signals.rst +0 -0
  76. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/submit_desc.rst +0 -0
  77. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/submit_help.rst +0 -0
  78. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/submit_usage.rst +0 -0
  79. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_info_desc.rst +0 -0
  80. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_info_help.rst +0 -0
  81. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_info_usage.rst +0 -0
  82. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_run_desc.rst +0 -0
  83. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_run_help.rst +0 -0
  84. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_run_usage.rst +0 -0
  85. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_search_desc.rst +0 -0
  86. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_search_help.rst +0 -0
  87. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_search_usage.rst +0 -0
  88. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_submit_desc.rst +0 -0
  89. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_submit_help.rst +0 -0
  90. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_submit_usage.rst +0 -0
  91. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_update_desc.rst +0 -0
  92. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_update_help.rst +0 -0
  93. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_update_usage.rst +0 -0
  94. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_wait_desc.rst +0 -0
  95. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_wait_help.rst +0 -0
  96. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/task_wait_usage.rst +0 -0
  97. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/templates.rst +0 -0
  98. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_include/templates_alt.rst +0 -0
  99. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_static/custom.css +0 -0
  100. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_static/logo-dark-mode.png +0 -0
  101. {hypershell-2.7.0 → hypershell-2.7.1}/docs/_static/logo-light-mode.png +0 -0
  102. {hypershell-2.7.0 → hypershell-2.7.1}/docs/api/client.rst +0 -0
  103. {hypershell-2.7.0 → hypershell-2.7.1}/docs/api/cluster.rst +0 -0
  104. {hypershell-2.7.0 → hypershell-2.7.1}/docs/api/data.rst +0 -0
  105. {hypershell-2.7.0 → hypershell-2.7.1}/docs/api/index.rst +0 -0
  106. {hypershell-2.7.0 → hypershell-2.7.1}/docs/api/server.rst +0 -0
  107. {hypershell-2.7.0 → hypershell-2.7.1}/docs/api/submit.rst +0 -0
  108. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20230329_2_2_0_release.rst +0 -0
  109. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20230413_2_3_0_release.rst +0 -0
  110. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20230602_2_4_0_release.rst +0 -0
  111. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20240518_2_5_0_release.rst +0 -0
  112. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20240706_2_5_2_release.rst +0 -0
  113. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20241115_2_6_0_release.rst +0 -0
  114. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20241115_announce_logo.rst +0 -0
  115. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20241231_2_6_1_release.rst +0 -0
  116. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20250215_2_6_5_release.rst +0 -0
  117. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20250405_2_6_6_release.rst +0 -0
  118. {hypershell-2.7.0 → hypershell-2.7.1}/docs/blog/20250504_2_7_0_release.rst +0 -0
  119. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/client.rst +0 -0
  120. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/cluster.rst +0 -0
  121. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/config_edit.rst +0 -0
  122. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/config_get.rst +0 -0
  123. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/config_set.rst +0 -0
  124. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/config_which.rst +0 -0
  125. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/index.rst +0 -0
  126. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/initdb.rst +0 -0
  127. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/server.rst +0 -0
  128. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/submit.rst +0 -0
  129. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/task_info.rst +0 -0
  130. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/task_run.rst +0 -0
  131. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/task_search.rst +0 -0
  132. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/task_submit.rst +0 -0
  133. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/task_update.rst +0 -0
  134. {hypershell-2.7.0 → hypershell-2.7.1}/docs/cli/task_wait.rst +0 -0
  135. {hypershell-2.7.0 → hypershell-2.7.1}/docs/config.rst +0 -0
  136. {hypershell-2.7.0 → hypershell-2.7.1}/docs/database.rst +0 -0
  137. {hypershell-2.7.0 → hypershell-2.7.1}/docs/index.rst +0 -0
  138. {hypershell-2.7.0 → hypershell-2.7.1}/docs/install.rst +0 -0
  139. {hypershell-2.7.0 → hypershell-2.7.1}/docs/logging.rst +0 -0
  140. {hypershell-2.7.0 → hypershell-2.7.1}/docs/manual.rst +0 -0
  141. {hypershell-2.7.0 → hypershell-2.7.1}/docs/roadmap.rst +0 -0
  142. {hypershell-2.7.0 → hypershell-2.7.1}/docs/templates.rst +0 -0
  143. {hypershell-2.7.0 → hypershell-2.7.1}/docs/tutorial/advanced.rst +0 -0
  144. {hypershell-2.7.0 → hypershell-2.7.1}/docs/tutorial/basic.rst +0 -0
  145. {hypershell-2.7.0 → hypershell-2.7.1}/docs/tutorial/distributed.rst +0 -0
  146. {hypershell-2.7.0 → hypershell-2.7.1}/docs/tutorial/hybrid.rst +0 -0
  147. {hypershell-2.7.0 → hypershell-2.7.1}/share/bash_completion.d/hs +0 -0
  148. {hypershell-2.7.0 → hypershell-2.7.1}/share/zsh/site-functions/_hs +0 -0
  149. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/client.py +0 -0
  150. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/cluster/__init__.py +0 -0
  151. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/cluster/local.py +0 -0
  152. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/cluster/remote.py +0 -0
  153. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/cluster/ssh.py +0 -0
  154. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/__init__.py +0 -0
  155. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/config.py +0 -0
  156. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/exceptions.py +0 -0
  157. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/fsm.py +0 -0
  158. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/heartbeat.py +0 -0
  159. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/logging.py +0 -0
  160. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/platform.py +0 -0
  161. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/pretty_print.py +0 -0
  162. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/queue.py +0 -0
  163. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/remote.py +0 -0
  164. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/signal.py +0 -0
  165. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/sys.py +0 -0
  166. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/tag.py +0 -0
  167. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/template.py +0 -0
  168. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/thread.py +0 -0
  169. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/types.py +0 -0
  170. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/core/uuid.py +0 -0
  171. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/data/core.py +0 -0
  172. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/data/model.py +0 -0
  173. {hypershell-2.7.0 → hypershell-2.7.1}/src/hypershell/server.py +0 -0
  174. {hypershell-2.7.0/tests/unit → hypershell-2.7.1/tests}/test_data/__init__.py +0 -0
  175. {hypershell-2.7.0/tests/unit → hypershell-2.7.1/tests}/test_data/test_core.py +0 -0
  176. {hypershell-2.7.0/tests/unit → hypershell-2.7.1/tests}/test_data/test_model.py +0 -0
@@ -0,0 +1,12 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: glentner
4
+ patreon: # Replace with a single Patreon username
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: # Replace with a single Ko-fi username
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ otechie: # Replace with a single Otechie username
12
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,19 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [created]
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+
12
+ - name: Install uv
13
+ uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ cache-dependency-glob: "uv.lock"
17
+
18
+ - name: Publish to PyPI
19
+ run: uv build && uv publish --token ${{ secrets.PYPI_TOKEN }}
@@ -0,0 +1,24 @@
1
+ name: Tests
2
+ on:
3
+ push:
4
+ branches: [ master, develop ]
5
+ pull_request:
6
+ branches: [ develop ]
7
+
8
+ jobs:
9
+ tests:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v5
19
+ with:
20
+ enable-cache: true
21
+ cache-dependency-glob: "uv.lock"
22
+
23
+ - name: Run tests
24
+ run: uv run --python ${{ matrix.python-version }} pytest -v
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypershell
3
- Version: 2.7.0
3
+ Version: 2.7.1
4
4
  Summary: A cross-platform, high-throughput computing utility for processing shell commands over a distributed, asynchronous queue.
5
5
  Author-email: Geoffrey Lentner <glentner@purdue.edu>
6
6
  License-Expression: Apache-2.0
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Topic :: Utilities
20
21
  Requires-Python: >=3.9
21
22
  Requires-Dist: cmdkit[toml]>=2.7.7
@@ -153,6 +153,7 @@ a few quality of life improvements, and a fix for SSH-mode.
153
153
  :hidden:
154
154
  :maxdepth: 1
155
155
 
156
+ 20250504_2_7_0_release
156
157
  20250405_2_6_6_release
157
158
  20250215_2_6_5_release
158
159
  20241231_2_6_1_release
@@ -72,7 +72,7 @@ html_css_files = [
72
72
  "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/solid.min.css",
73
73
  "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/brands.min.css",
74
74
  ]
75
- html_favicon = '_static/logo.svg'
75
+ html_favicon = '_static/logo-dark-mode.png'
76
76
  html_theme_options = {
77
77
  # 'announcement': 'See Installation page for not on PyPI package name issue!',
78
78
  'sidebar_hide_name': True,
@@ -56,7 +56,7 @@ will manage tasks entirely within memory.
56
56
 
57
57
  .. code-block:: shell
58
58
 
59
- seq 4 | hs cluster -t 'echo {}'
59
+ seq 4 | hsx -t 'echo {}'
60
60
 
61
61
  .. details:: Output
62
62
 
@@ -81,7 +81,7 @@ see additional detail about what is running, where, and when.
81
81
 
82
82
  .. code-block:: shell
83
83
 
84
- hs cluster tasks.in -N16 --ssh-group=xyz --capture
84
+ hsx tasks.in -N16 --ssh-group=xyz --capture
85
85
 
86
86
  .. details:: Logs
87
87
 
@@ -107,7 +107,7 @@ example we stagger the launch process over one minute.
107
107
 
108
108
  .. code-block:: shell
109
109
 
110
- hs cluster tasks.in -N128 -b128 --launcher=srun --max-retries=2 --delay-start=-60 >task.out
110
+ hsx tasks.in -N128 -b128 --launcher=srun --max-retries=2 --delay-start=-60 >task.out
111
111
 
112
112
  .. details:: Logs
113
113
 
@@ -175,7 +175,7 @@ expansion. Many meta-patterns are supported (see full overview of :ref:`template
175
175
 
176
176
  .. code-block:: shell
177
177
 
178
- hs cluster tasks.in -N12 -t './some_program.py {} >outputs/{/-}.out'
178
+ hsx tasks.in -N12 -t './some_program.py {} >outputs/{/-}.out'
179
179
 
180
180
  Capturing `stdout` and `stderr` is supported directly in fact with the ``--capture`` option.
181
181
  See the full documentation for environment variables under :ref:`configuration <config>`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hypershell"
3
- version = "2.7.0"
3
+ version = "2.7.1"
4
4
  description = "A cross-platform, high-throughput computing utility for processing shell commands over a distributed, asynchronous queue."
5
5
  readme = "README.rst"
6
6
  license = "Apache-2.0"
@@ -25,6 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.11",
26
26
  "Programming Language :: Python :: 3.12",
27
27
  "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
28
29
  "Operating System :: POSIX :: Linux",
29
30
  "Operating System :: MacOS",
30
31
  "Operating System :: Microsoft :: Windows",
@@ -54,7 +55,8 @@ dev = [
54
55
  "uuid-utils>=0.10.0",
55
56
  "pytest>=8.1.1",
56
57
  "hypothesis>=6.100.0",
57
- "sphinx-autobuild>=2024.2.4"
58
+ "sphinx-autobuild>=2024.2.4",
59
+ "pytest-xdist>=3.7.0",
58
60
  ]
59
61
  docs = [
60
62
  "sphinx>=6.0,<8.0",
@@ -81,3 +83,11 @@ hsx = "hypershell:main_x"
81
83
  [build-system]
82
84
  requires = ["hatchling"]
83
85
  build-backend = "hatchling.build"
86
+
87
+ [tool.pytest.ini_options]
88
+ addopts = ["--strict-markers", ]
89
+ markers = [
90
+ 'unit', # Unit tests are short, interface driven tests on discrete components.",
91
+ 'integration', # Integration tests are often longer and deal with the interaction between systems.",
92
+ 'parameterize', # Placeholder for parameterized tests (not a real type).",
93
+ ]
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
27
27
  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
28
28
  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
29
29
  ..
30
- .TH "HS" "1" "May 26, 2025" "2.7.0" "hypershell"
30
+ .TH "HS" "1" "Jun 07, 2025" "2.7.1" "hypershell"
31
31
  .SH NAME
32
32
  hs \- Process shell commands over a distributed, asynchronous queue
33
33
  .SH SYNOPSIS
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
27
27
  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
28
28
  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
29
29
  ..
30
- .TH "HS" "1" "May 26, 2025" "2.7.0" "hypershell"
30
+ .TH "HS" "1" "Jun 07, 2025" "2.7.1" "hypershell"
31
31
  .SH NAME
32
32
  hs \- Process shell commands over a distributed, asynchronous queue
33
33
  .SH SYNOPSIS
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
27
27
  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
28
28
  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
29
29
  ..
30
- .TH "HYPER-SHELL" "1" "May 26, 2025" "2.7.0" "hypershell"
30
+ .TH "HYPER-SHELL" "1" "Jun 07, 2025" "2.7.1" "hypershell"
31
31
  .SH NAME
32
32
  hyper-shell \- Process shell commands over a distributed, asynchronous queue
33
33
  .SH SYNOPSIS
@@ -3,6 +3,7 @@
3
3
 
4
4
  """Initialization and entry-point for console application."""
5
5
 
6
+
6
7
  # Type annotations
7
8
  from __future__ import annotations
8
9
  from typing import List
@@ -31,7 +32,12 @@ from hypershell.config import ConfigApp
31
32
  from hypershell.data import InitDBApp
32
33
 
33
34
  # Public interface
34
- __all__ = ['HyperShellApp', 'main', '__version__']
35
+ __all__ = [
36
+ 'HyperShellApp', 'main', '__version__', '__citation__',
37
+ 'APP_VERSION', 'APP_USAGE', 'APP_HELP',
38
+ 'SubmitApp', 'ServerApp', 'ClientApp', 'InitDBApp', 'ConfigApp',
39
+ 'TaskGroupApp', 'TaskInfoApp', 'TaskWaitApp', 'TaskRunApp', 'TaskSearchApp', 'TaskUpdateApp',
40
+ ]
35
41
 
36
42
  # project metadata
37
43
  __version__ = get_version('hypershell')
@@ -6,7 +6,7 @@
6
6
 
7
7
  # Type annotations
8
8
  from __future__ import annotations
9
- from typing import Any
9
+ from typing import Any, Final
10
10
 
11
11
  # Standard libs
12
12
  import os
@@ -386,16 +386,16 @@ class ConfigWhichApp(Application):
386
386
 
387
387
 
388
388
  if os.name == 'nt':
389
- CONFIG_PATH_INFO = f"""\
389
+ CONFIG_PATH_INFO: Final[str] = f"""\
390
390
  --system %ProgramData%\\HyperShell\\Config.toml
391
391
  --user %AppData%\\HyperShell\\Config.toml
392
- --local {path.local.config}
392
+ --local {path.local.config}\
393
393
  """
394
394
  else:
395
- CONFIG_PATH_INFO = f"""\
395
+ CONFIG_PATH_INFO: Final[str] = f"""\
396
396
  --system /etc/hypershell.toml
397
397
  --user ~/.hypershell/config.toml
398
- --local {path.local.config}
398
+ --local {path.local.config}\
399
399
  """
400
400
 
401
401
 
@@ -258,7 +258,7 @@ class InitDBApp(Application):
258
258
  raise ArgumentError('Can only rotate SQLite')
259
259
  if self.backup_path and os.path.exists(self.backup_path):
260
260
  raise RuntimeError(f'Backup path already exists ({self.backup_path})')
261
- if not sys.stdout.isatty():
261
+ if not sys.stdout.isatty() and not self.auto_confirm:
262
262
  raise RuntimeError('Non-interactive prompt cannot confirm (see --yes).')
263
263
 
264
264
  def confirm_action(self: InitDBApp, message: str) -> bool:
@@ -766,9 +766,8 @@ class SubmitApp(Application):
766
766
  source: IO | List[str] | None = None
767
767
  task_args: List[str] = []
768
768
  task_file: str = None
769
- input_interface = interface.add_mutually_exclusive_group()
770
- input_interface.add_argument('task_args', nargs='*')
771
- input_interface.add_argument('-f', '--task-file', default=task_file)
769
+ interface.add_argument('task_args', nargs='*')
770
+ interface.add_argument('-f', '--task-file', default=task_file)
772
771
 
773
772
  bundlesize: int = config.submit.bundlesize
774
773
  interface.add_argument('-b', '--bundlesize', type=int, default=bundlesize)
@@ -784,7 +783,7 @@ class SubmitApp(Application):
784
783
 
785
784
  tags: Dict[str, JSONValue] = {}
786
785
  taglist: List[str] = None
787
- interface.add_argument('-t', '--tag', nargs='*', default=[], dest='taglist')
786
+ interface.add_argument('-t', '--tag', nargs='+', default=[], dest='taglist')
788
787
 
789
788
  exceptions = {
790
789
  **get_shared_exception_mapping(__name__)
@@ -833,6 +832,8 @@ class SubmitApp(Application):
833
832
 
834
833
  def check_source(self: SubmitApp) -> None:
835
834
  """Determine task submission mode."""
835
+ if self.task_args and self.task_file:
836
+ raise ArgumentError(f'Cannot specify both -f/--task-file and positional arguments')
836
837
  if self.task_file == '-':
837
838
  log.debug(f'Submitted from <stdin> (explicit)')
838
839
  self.source = sys.stdin
@@ -1,7 +1,7 @@
1
1
  # SPDX-FileCopyrightText: 2025 Geoffrey Lentner
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- """Task based operations."""
4
+ """Task-based operations."""
5
5
 
6
6
 
7
7
  # Type annotations
@@ -85,7 +85,7 @@ class TaskSubmitApp(Application):
85
85
 
86
86
  tags: Dict[str, JSONValue] = {}
87
87
  taglist: List[str] = []
88
- interface.add_argument('-t', '--tag', nargs='*', dest='taglist')
88
+ interface.add_argument('-t', '--tag', nargs='+', dest='taglist')
89
89
 
90
90
  exceptions = {
91
91
  **get_shared_exception_mapping(__name__)
@@ -382,7 +382,7 @@ class TaskRunApp(Application):
382
382
  interface.add_argument('-n', '--interval', type=int, default=interval)
383
383
 
384
384
  taglist: List[str] = []
385
- interface.add_argument('-t', '--tag', nargs='*', dest='taglist')
385
+ interface.add_argument('-t', '--tag', nargs='+', dest='taglist')
386
386
 
387
387
  exceptions = {
388
388
  **get_shared_exception_mapping(__name__)
@@ -549,10 +549,10 @@ class TaskSearchApp(Application, SearchableMixin):
549
549
  interface.add_argument('field_names', nargs='*', default=field_names)
550
550
 
551
551
  where_clauses: List[str] = None
552
- interface.add_argument('-w', '--where', nargs='*', default=[], dest='where_clauses')
552
+ interface.add_argument('-w', '--where', nargs='+', default=[], dest='where_clauses')
553
553
 
554
554
  taglist: List[str] = None
555
- interface.add_argument('-t', '--with-tag', nargs='*', default=[], dest='taglist')
555
+ interface.add_argument('-t', '--with-tag', nargs='+', default=[], dest='taglist')
556
556
 
557
557
  order_by: str = None
558
558
  order_desc: bool = False
@@ -815,10 +815,10 @@ class TaskUpdateApp(Application, SearchableMixin):
815
815
  field_names: List[str] = []
816
816
 
817
817
  where_clauses: List[str] = None
818
- interface.add_argument('-w', '--where', nargs='*', default=[], dest='where_clauses')
818
+ interface.add_argument('-w', '--where', nargs='+', default=[], dest='where_clauses')
819
819
 
820
820
  taglist: List[str] = None
821
- interface.add_argument('-t', '--with-tag', nargs='*', default=[], dest='taglist')
821
+ interface.add_argument('-t', '--with-tag', nargs='+', default=[], dest='taglist')
822
822
 
823
823
  order_by: str = None
824
824
  order_desc: bool = False
@@ -847,7 +847,7 @@ class TaskUpdateApp(Application, SearchableMixin):
847
847
  action_interface.add_argument('--delete', action='store_true', dest='delete_mode')
848
848
 
849
849
  remove_tag: List[str] = None
850
- interface.add_argument('--remove-tag', nargs='*')
850
+ interface.add_argument('--remove-tag', nargs='+')
851
851
 
852
852
  no_confirm: bool = False
853
853
  interface.add_argument('-f', '--no-confirm', action='store_true')
@@ -0,0 +1,75 @@
1
+ # SPDX-FileCopyrightText: 2025 Geoffrey Lentner
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Automated tests for HyperShell."""
5
+
6
+
7
+ # Type annotations
8
+ from __future__ import annotations
9
+ from typing import Final, List, Tuple, Dict, Any
10
+
11
+ # Standard libs
12
+ import os
13
+ import re
14
+ from pathlib import Path
15
+ from subprocess import run, PIPE
16
+
17
+ # Public interface
18
+ __all__ = ['NO_OUTPUT', 'main', 'main_lines', 'assert_output',
19
+ 'create_taskfile', 'create_taskfile_echo',
20
+ 'UUID_PATTERN']
21
+
22
+
23
+ # Flag for empty output from `main_lines` function
24
+ NO_OUTPUT: Final[List[str]] = ['', ]
25
+
26
+
27
+ def main(argv: List[str]) -> Tuple[int, str, str]:
28
+ """Return stdout, stderr, and exit code of command-line interface."""
29
+ proc = run(argv, stdout=PIPE, stderr=PIPE, env=os.environ)
30
+ return (
31
+ proc.returncode,
32
+ proc.stdout.decode('utf-8').strip(),
33
+ proc.stderr.decode('utf-8').strip()
34
+ )
35
+
36
+
37
+ def main_lines(argv: List[str]) -> Tuple[int, List[str], List[str]]:
38
+ """Return stdout, stderr, and exit code of command-line interface with output as List[str]."""
39
+ rc, stdout, stderr = main(argv)
40
+ return (
41
+ rc,
42
+ [line.strip() for line in stdout.split('\n')],
43
+ [line.strip() for line in stderr.split('\n')]
44
+ )
45
+
46
+
47
+ def assert_output(pattern: str, output: str, count: int = 1, groups: Dict[str, str | re.Pattern] = None) -> None:
48
+ """Assert some count of lines in output match pattern, optionally with capture groups pattern matching."""
49
+ n = 0
50
+ for line in output.strip().splitlines():
51
+ if match := re.search(pattern, line):
52
+ n += 1
53
+ for key, value in (groups or {}).items():
54
+ assert re.match(value, match.group(key))
55
+ assert n == count
56
+
57
+
58
+ def create_taskfile(temp_site: Path, lines: List[str]) -> Path:
59
+ """Produce task input file for test, return path."""
60
+ taskfile = temp_site / 'task.in'
61
+ with open(taskfile, mode='w', encoding='utf-8') as stream:
62
+ for line in lines:
63
+ print(line, file=stream)
64
+ return taskfile
65
+
66
+
67
+ def create_taskfile_echo(temp_site: Path, count: int, tags: Dict[str, Any] = None) -> Path:
68
+ """Produce task input file with 'echo {}' lines, return path."""
69
+ tagline = ' '.join([f'{k}:{v}' for k, v in (tags or {}).items()])
70
+ return create_taskfile(temp_site, lines=[f'echo {n} # HYPERSHELL: {tagline} n:{n}' for n in range(count)])
71
+
72
+
73
+ UUID_PATTERN: re.Pattern = re.compile(
74
+ r'^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
75
+ )
@@ -0,0 +1,27 @@
1
+ # SPDX-FileCopyrightText: 2025 Geoffrey Lentner
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Fixtures for testing."""
5
+
6
+
7
+ # Standard libs
8
+ import os
9
+
10
+ # External libs
11
+ from pytest import fixture
12
+ from pathlib import Path
13
+
14
+
15
+ @fixture(scope="function")
16
+ def temp_site(tmpdir_factory) -> Path:
17
+ """Setup empty site and database path."""
18
+ for name in os.environ:
19
+ if name.startswith('HYPERSHELL_'):
20
+ os.environ.pop(name)
21
+ site = tmpdir_factory.mktemp('data')
22
+ db_path = site.join('local.db')
23
+ os.environ['HYPERSHELL_CONFIG_FILE'] = ''
24
+ os.environ['HYPERSHELL_SITE'] = str(site)
25
+ os.environ['HYPERSHELL_DATABASE_FILE'] = str(db_path)
26
+ os.environ['HYPERSHELL_LOGGING_LEVEL'] = 'DEBUG'
27
+ return site
@@ -0,0 +1,184 @@
1
+ # SPDX-FileCopyrightText: 2025 Geoffrey Lentner
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Test cluster operations."""
5
+
6
+
7
+ # Type annotations
8
+ from __future__ import annotations
9
+ from typing import Final, Dict
10
+
11
+ # Standard libs
12
+ from pathlib import Path
13
+
14
+ # External libs
15
+ from pytest import mark
16
+ from cmdkit.app import exit_status
17
+
18
+ # Internal libs
19
+ from tests import main, main_lines, NO_OUTPUT, create_taskfile_echo, assert_output
20
+ from hypershell.cluster import ClusterApp
21
+
22
+
23
+ @mark.integration
24
+ def test_cluster_usage() -> None:
25
+ """The 'hs cluster' command prints usage statement without arguments."""
26
+ rc, stdout, stderr = main(['hs', 'cluster', ])
27
+ assert rc == exit_status.usage
28
+ assert stdout == ClusterApp.interface.usage_text
29
+ assert stderr == ''
30
+
31
+
32
+ @mark.integration
33
+ def test_cluster_usage_hsx() -> None:
34
+ """The 'hsx' command prints usage statement without arguments."""
35
+ rc, stdout, stderr = main(['hsx', ])
36
+ assert rc == exit_status.usage
37
+ assert stdout == ClusterApp.interface.usage_text
38
+ assert stderr == ''
39
+
40
+
41
+ @mark.integration
42
+ @mark.parametrize('opt', ['-h', '--help'])
43
+ def test_cluster_help(opt: str) -> None:
44
+ """The 'hs cluster' command prints help statement with -h, --help option."""
45
+ rc, stdout, stderr = main(['hs', 'cluster', opt, ])
46
+ assert rc == exit_status.success
47
+ assert stdout == ClusterApp.interface.help_text
48
+ assert stderr == ''
49
+
50
+
51
+ @mark.integration
52
+ @mark.parametrize('opt', ['-h', '--help'])
53
+ def test_cluster_help_hsx(opt: str) -> None:
54
+ """The 'hsx' command prints help statement with -h, --help option."""
55
+ rc, stdout, stderr = main(['hsx', opt, ])
56
+ assert rc == exit_status.success
57
+ assert stdout == ClusterApp.interface.help_text
58
+ assert stderr == ''
59
+
60
+
61
+ @mark.integration
62
+ def test_cluster_mixed_modes() -> None:
63
+ """Cannot use multiple start methods."""
64
+ assert main_lines(['hs', 'cluster', '--forever', '--restart']) == (
65
+ exit_status.bad_argument, NO_OUTPUT, [
66
+ 'CRITICAL [hypershell] Using --forever with --restart is invalid',
67
+ ]
68
+ )
69
+
70
+
71
+ @mark.integration
72
+ @mark.parametrize('opt', ['--foo', '--bar', '--baz=7'])
73
+ def test_cluster_bad_options(opt: str) -> None:
74
+ """Error on invalid options."""
75
+ assert main_lines(['hs', 'cluster', opt]) == (
76
+ exit_status.bad_argument, NO_OUTPUT, [
77
+ f'CRITICAL [hypershell] unrecognized arguments: {opt}',
78
+ ]
79
+ )
80
+
81
+
82
+ @mark.integration
83
+ def test_cluster_missing_file(temp_site: Path) -> None:
84
+ """Error on missing file."""
85
+ missing_file = str(temp_site / 'missing_file.txt')
86
+ rc, stdout, stderr = main(['hs', 'cluster', missing_file])
87
+ assert rc == exit_status.runtime_error
88
+ assert stdout == ''
89
+ assert_output(r'CRITICAL .* FileNotFoundError: .* No such file .*', stderr, 1)
90
+
91
+
92
+ @mark.integration
93
+ def test_cluster_multiple_arguments() -> None:
94
+ """Error on multiple positional arguments."""
95
+ assert main_lines(['hs', 'cluster', 'file_a', 'file_b']) == (
96
+ exit_status.bad_argument, NO_OUTPUT, [
97
+ f'CRITICAL [hypershell] unrecognized arguments: file_b',
98
+ ]
99
+ )
100
+
101
+
102
+ OPTION_PAIR: Final[Dict[str, str]] = {
103
+ '-N': '-N/--num-tasks', '--num-tasks': '-N/--num-tasks',
104
+ '-t': '-t/--template', '--template': '-t/--template',
105
+ '-p': '-p/--port', '--port': '-p/--port',
106
+ '-b': '-b/--bundlesize', '--bundlesize': '-b/--bundlesize',
107
+ '-w': '-w/--bundlewait', '--bundlewait': '-w/--bundlewait',
108
+ '-r': '-r/--max-retries', '--max-retries': '-r/--max-retries',
109
+ '-o': '-o/--output', '--output': '-o/--output',
110
+ '-e': '-e/--errors', '--errors': '-e/--errors',
111
+ '-f': '-f/--failures', '--failures': '-f/--failures',
112
+ '--ssh-args': '--ssh-args', '--ssh-group': '--ssh-group',
113
+ '--remote-exe': '--remote-exe',
114
+ '-d': '-d/--delay-start', '--delay-start': '-d/--delay-start',
115
+ '-T': '-T/--timeout', '--timeout': '-T/--timeout',
116
+ '-W': '-W/--task-timeout', '--task-timeout': '-W/--task-timeout',
117
+ '-S': '-S/--signalwait', '--signalwait': '-S/--signalwait',
118
+ '-F': '-F/--factor', '--factor': '-F/--factor',
119
+ '-P': '-P/--period', '--period': '-P/--period',
120
+ '-I': '-I/--init-size', '--init-size': '-I/--init-size',
121
+ '-X': '-X/--min-size', '--min-size': '-X/--min-size',
122
+ '-Y': '-Y/--max-size', '--max-size': '-Y/--max-size',
123
+ }
124
+
125
+
126
+ @mark.integration
127
+ @mark.parametrize('opt', list(OPTION_PAIR))
128
+ def test_cluster_missing_option_value(opt: str) -> None:
129
+ """Error on missing option value."""
130
+ assert main_lines(['hs', 'cluster', '-', opt]) == (
131
+ exit_status.bad_argument, NO_OUTPUT, [
132
+ f'CRITICAL [hypershell] argument {OPTION_PAIR[opt]}: expected one argument',
133
+ ]
134
+ )
135
+
136
+
137
+ @mark.integration
138
+ def test_cluster_basic(temp_site: Path) -> None:
139
+ """Run small collection of tasks without database."""
140
+ taskfile = create_taskfile_echo(temp_site, count=4)
141
+ rc, stdout, stderr = main(['hs', 'cluster', taskfile, '--no-db', '--no-confirm'])
142
+ assert rc == exit_status.success
143
+ assert stdout == '\n'.join(map(str, range(4)))
144
+ assert_output(r'Registered client', stderr, 1)
145
+ assert_output(r'INFO \[hypershell.client\] Running task', stderr, 4)
146
+ assert_output(r'DEBUG \[hypershell.client\] Running task', stderr, 4)
147
+ assert_output(r'\[hypershell.server\] Completed task', stderr, 4)
148
+ assert_output(r'\[hypershell.client\] Completed task', stderr, 4)
149
+
150
+
151
+ @mark.integration
152
+ def test_cluster_database(temp_site: Path) -> None:
153
+ """Run small collection of tasks from file to database."""
154
+
155
+ # Start out with an empty database
156
+ assert main_lines(['hs', 'list', '--count']) == (
157
+ exit_status.success, ['0', ], NO_OUTPUT
158
+ )
159
+
160
+ # Run with `seq 4` inputs
161
+ taskfile = create_taskfile_echo(temp_site, count=4)
162
+ rc, stdout, stderr = main(['hs', 'cluster', taskfile])
163
+ assert rc == exit_status.success
164
+ assert stdout == '\n'.join(map(str, range(4)))
165
+ assert_output(r'Registered client', stderr, 1)
166
+ assert_output(r'INFO \[hypershell.client\] Running task', stderr, 4)
167
+ assert_output(r'DEBUG \[hypershell.client\] Running task', stderr, 4)
168
+ assert_output(r'\[hypershell.server\] Completed task', stderr, 4)
169
+ assert_output(r'\[hypershell.client\] Completed task', stderr, 4)
170
+
171
+ # Now there are 4 tasks in the database
172
+ assert main_lines(['hs', 'list', '--count']) == (
173
+ exit_status.success, ['4', ], NO_OUTPUT
174
+ )
175
+
176
+ # Confirm arguments
177
+ assert main_lines(['hs', 'list', 'args', '-f', 'plain', '-s', 'submit_time']) == (
178
+ exit_status.success, ['echo 0', 'echo 1', 'echo 2', 'echo 3'], NO_OUTPUT
179
+ )
180
+
181
+ # Confirm status
182
+ assert main_lines(['hs', 'list', 'exit_status', '-f', 'plain', '-s', 'submit_time']) == (
183
+ exit_status.success, ['0'] * 4, NO_OUTPUT
184
+ )