hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a200__py3-none-any.whl

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 (132) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
  2. hpcflow/_version.py +1 -1
  3. hpcflow/data/scripts/bad_script.py +2 -0
  4. hpcflow/data/scripts/do_nothing.py +2 -0
  5. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  6. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  7. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  8. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  11. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  12. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  13. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  15. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  16. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  23. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  24. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  25. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  26. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  27. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  28. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  29. hpcflow/data/scripts/script_exit_test.py +5 -0
  30. hpcflow/data/template_components/environments.yaml +1 -1
  31. hpcflow/sdk/__init__.py +5 -0
  32. hpcflow/sdk/app.py +166 -92
  33. hpcflow/sdk/cli.py +263 -84
  34. hpcflow/sdk/cli_common.py +99 -5
  35. hpcflow/sdk/config/callbacks.py +38 -1
  36. hpcflow/sdk/config/config.py +102 -13
  37. hpcflow/sdk/config/errors.py +19 -5
  38. hpcflow/sdk/config/types.py +3 -0
  39. hpcflow/sdk/core/__init__.py +25 -1
  40. hpcflow/sdk/core/actions.py +914 -262
  41. hpcflow/sdk/core/cache.py +76 -34
  42. hpcflow/sdk/core/command_files.py +14 -128
  43. hpcflow/sdk/core/commands.py +35 -6
  44. hpcflow/sdk/core/element.py +122 -50
  45. hpcflow/sdk/core/errors.py +58 -2
  46. hpcflow/sdk/core/execute.py +207 -0
  47. hpcflow/sdk/core/loop.py +408 -50
  48. hpcflow/sdk/core/loop_cache.py +4 -4
  49. hpcflow/sdk/core/parameters.py +382 -37
  50. hpcflow/sdk/core/run_dir_files.py +13 -40
  51. hpcflow/sdk/core/skip_reason.py +7 -0
  52. hpcflow/sdk/core/task.py +119 -30
  53. hpcflow/sdk/core/task_schema.py +68 -0
  54. hpcflow/sdk/core/test_utils.py +66 -27
  55. hpcflow/sdk/core/types.py +54 -1
  56. hpcflow/sdk/core/utils.py +136 -19
  57. hpcflow/sdk/core/workflow.py +1587 -356
  58. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  59. hpcflow/sdk/demo/cli.py +7 -0
  60. hpcflow/sdk/helper/cli.py +1 -0
  61. hpcflow/sdk/log.py +42 -15
  62. hpcflow/sdk/persistence/base.py +405 -53
  63. hpcflow/sdk/persistence/json.py +177 -52
  64. hpcflow/sdk/persistence/pending.py +237 -69
  65. hpcflow/sdk/persistence/store_resource.py +3 -2
  66. hpcflow/sdk/persistence/types.py +15 -4
  67. hpcflow/sdk/persistence/zarr.py +928 -81
  68. hpcflow/sdk/submission/jobscript.py +1408 -489
  69. hpcflow/sdk/submission/schedulers/__init__.py +40 -5
  70. hpcflow/sdk/submission/schedulers/direct.py +33 -19
  71. hpcflow/sdk/submission/schedulers/sge.py +51 -16
  72. hpcflow/sdk/submission/schedulers/slurm.py +44 -16
  73. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  74. hpcflow/sdk/submission/shells/base.py +68 -20
  75. hpcflow/sdk/submission/shells/bash.py +222 -129
  76. hpcflow/sdk/submission/shells/powershell.py +200 -150
  77. hpcflow/sdk/submission/submission.py +852 -119
  78. hpcflow/sdk/submission/types.py +18 -21
  79. hpcflow/sdk/typing.py +24 -5
  80. hpcflow/sdk/utils/arrays.py +71 -0
  81. hpcflow/sdk/utils/deferred_file.py +55 -0
  82. hpcflow/sdk/utils/hashing.py +16 -0
  83. hpcflow/sdk/utils/patches.py +12 -0
  84. hpcflow/sdk/utils/strings.py +33 -0
  85. hpcflow/tests/api/test_api.py +32 -0
  86. hpcflow/tests/conftest.py +19 -0
  87. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  88. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  89. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  90. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  91. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  92. hpcflow/tests/scripts/test_main_scripts.py +821 -70
  93. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  94. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  95. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
  96. hpcflow/tests/unit/test_action.py +176 -0
  97. hpcflow/tests/unit/test_app.py +20 -0
  98. hpcflow/tests/unit/test_cache.py +46 -0
  99. hpcflow/tests/unit/test_cli.py +133 -0
  100. hpcflow/tests/unit/test_config.py +122 -1
  101. hpcflow/tests/unit/test_element_iteration.py +47 -0
  102. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  103. hpcflow/tests/unit/test_loop.py +1332 -27
  104. hpcflow/tests/unit/test_meta_task.py +325 -0
  105. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  106. hpcflow/tests/unit/test_parameter.py +13 -0
  107. hpcflow/tests/unit/test_persistence.py +190 -8
  108. hpcflow/tests/unit/test_run.py +109 -3
  109. hpcflow/tests/unit/test_run_directories.py +29 -0
  110. hpcflow/tests/unit/test_shell.py +20 -0
  111. hpcflow/tests/unit/test_submission.py +5 -76
  112. hpcflow/tests/unit/test_workflow_template.py +31 -0
  113. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  114. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  115. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  116. hpcflow/tests/unit/utils/test_patches.py +5 -0
  117. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  118. hpcflow/tests/workflows/__init__.py +0 -0
  119. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  120. hpcflow/tests/workflows/test_jobscript.py +332 -0
  121. hpcflow/tests/workflows/test_run_status.py +198 -0
  122. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  123. hpcflow/tests/workflows/test_submission.py +140 -0
  124. hpcflow/tests/workflows/test_workflows.py +142 -2
  125. hpcflow/tests/workflows/test_zip.py +18 -0
  126. hpcflow/viz_demo.ipynb +6587 -3
  127. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/METADATA +7 -4
  128. hpcflow_new2-0.2.0a200.dist-info/RECORD +222 -0
  129. hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
  130. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/LICENSE +0 -0
  131. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/WHEEL +0 -0
  132. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,145 @@
1
1
  import pytest
2
2
 
3
3
  from click.testing import CliRunner
4
+ import click.exceptions
4
5
 
5
6
  from hpcflow import __version__
6
7
  from hpcflow.app import app as hf
8
+ from hpcflow.sdk.cli import ErrorPropagatingClickContext
9
+ from hpcflow.sdk.cli_common import BoolOrString
7
10
 
8
11
 
9
12
  def test_version() -> None:
10
13
  runner = CliRunner()
11
14
  result = runner.invoke(hf.cli, args="--version")
12
15
  assert result.output.strip() == f"hpcFlow, version {__version__}"
16
+
17
+
18
+ def test_BoolOrString_convert():
19
+ param_type = BoolOrString(["a"])
20
+ assert param_type.convert(True, None, None) == True
21
+ assert param_type.convert(False, None, None) == False
22
+ assert param_type.convert("yes", None, None) == True
23
+ assert param_type.convert("no", None, None) == False
24
+ assert param_type.convert("on", None, None) == True
25
+ assert param_type.convert("off", None, None) == False
26
+ assert param_type.convert("a", None, None) == "a"
27
+ with pytest.raises(click.exceptions.BadParameter):
28
+ param_type.convert("b", None, None)
29
+
30
+
31
+ def test_error_propagated_with_custom_context_class():
32
+ class MyException(ValueError):
33
+ pass
34
+
35
+ class MyContextManager:
36
+
37
+ # set to True when MyException is raised within this context manager
38
+ raised = False
39
+
40
+ def __enter__(self):
41
+ return self
42
+
43
+ def __exit__(self, exc_type, exc_val, exc_tb):
44
+ if exc_type == MyException:
45
+ self.__class__.raised = True
46
+
47
+ @click.group()
48
+ @click.pass_context
49
+ def cli(ctx):
50
+ ctx.with_resource(MyContextManager())
51
+
52
+ cli.context_class = ErrorPropagatingClickContext # use custom click Context
53
+
54
+ @cli.command()
55
+ def my_command():
56
+ raise MyException()
57
+
58
+ runner = CliRunner()
59
+ runner.invoke(cli, args="my-command")
60
+
61
+ assert MyContextManager.raised
62
+
63
+
64
+ def test_error_not_propagated_without_custom_context_class():
65
+ class MyException(ValueError):
66
+ pass
67
+
68
+ class MyContextManager:
69
+
70
+ # set to True when MyException is raised within this context manager
71
+ raised = False
72
+
73
+ def __enter__(self):
74
+ return self
75
+
76
+ def __exit__(self, exc_type, exc_val, exc_tb):
77
+ if exc_type == MyException:
78
+ self.__class__.raised = True
79
+
80
+ @click.group()
81
+ @click.pass_context
82
+ def cli(ctx):
83
+ ctx.with_resource(MyContextManager())
84
+
85
+ @cli.command()
86
+ def my_command():
87
+ raise MyException()
88
+
89
+ runner = CliRunner()
90
+ runner.invoke(cli, args="my-command")
91
+
92
+ assert not MyContextManager.raised
93
+
94
+
95
+ def test_std_stream_file_created(tmp_path):
96
+ """Test exception is intercepted and printed to the specified --std-stream file."""
97
+ error_file = tmp_path / "std_stream.txt"
98
+ runner = CliRunner()
99
+ result = runner.invoke(
100
+ hf.cli, args=f'--std-stream "{str(error_file)}" internal noop --raise'
101
+ )
102
+ assert error_file.is_file()
103
+ std_stream_contents = error_file.read_text()
104
+ assert "ValueError: internal noop raised!" in std_stream_contents
105
+ assert result.exit_code == 1
106
+ assert result.exc_info[0] == SystemExit
107
+
108
+
109
+ def test_std_stream_file_not_created(null_config, tmp_path):
110
+ """Test std stream file is not created when no ouput/errors/exceptions"""
111
+ error_file = tmp_path / "std_stream.txt"
112
+ runner = CliRunner()
113
+ result = runner.invoke(hf.cli, args=f'--std-stream "{str(error_file)}" internal noop')
114
+ assert not error_file.is_file()
115
+ assert result.exit_code == 0
116
+
117
+
118
+ def test_cli_exception():
119
+ """Test exception is passed to click"""
120
+ runner = CliRunner()
121
+ result = runner.invoke(hf.cli, args="internal noop --raise")
122
+ assert result.exit_code == 1
123
+ assert result.exc_info[0] == ValueError
124
+
125
+
126
+ def test_cli_click_exit_code_zero(tmp_path):
127
+ """Test Click's `Exit` exception is ignored by the `redirect_std_to_file` context manager when the exit code is zero."""
128
+ error_file = tmp_path / "std_stream.txt"
129
+ runner = CliRunner()
130
+ result = runner.invoke(
131
+ hf.cli, args=f'--std-stream "{str(error_file)}" internal noop --click-exit-code 0'
132
+ )
133
+ assert result.exit_code == 0
134
+ assert not error_file.is_file()
135
+
136
+
137
+ def test_cli_click_exit_code_non_zero(tmp_path):
138
+ """Test Click's `Exit` exception is not ignored by the `redirect_std_to_file` context manager when the exit code is non-zero."""
139
+ error_file = tmp_path / "std_stream.txt"
140
+ runner = CliRunner()
141
+ result = runner.invoke(
142
+ hf.cli, args=f'--std-stream "{str(error_file)}" internal noop --click-exit-code 2'
143
+ )
144
+ assert result.exit_code == 2
145
+ assert error_file.is_file()
@@ -1,9 +1,15 @@
1
1
  from __future__ import annotations
2
2
  import os
3
+ import time
3
4
  import pytest
4
5
 
5
6
  from hpcflow.app import app as hf
6
- from hpcflow.sdk.config.errors import ConfigFileValidationError, ConfigItemCallbackError
7
+ from hpcflow.sdk.config.errors import (
8
+ ConfigFileValidationError,
9
+ ConfigItemCallbackError,
10
+ ConfigNonConfigurableError,
11
+ ConfigReadOnlyError,
12
+ )
7
13
 
8
14
 
9
15
  def test_reset_config(new_null_config) -> None:
@@ -72,3 +78,118 @@ def test_without_callbacks_ctx_manager(null_config) -> None:
72
78
 
73
79
  # unload the modified config so it's not reused by other tests
74
80
  hf.unload_config()
81
+
82
+
83
+ @pytest.mark.xfail(reason="Might occasionally fail.")
84
+ def test_cache_faster_than_no_cache(null_config):
85
+ n = 10_000
86
+ tic = time.perf_counter()
87
+ for _ in range(n):
88
+ _ = hf.config.machine
89
+ toc = time.perf_counter()
90
+ elapsed_no_cache = toc - tic
91
+
92
+ with hf.config.cached_config():
93
+ tic = time.perf_counter()
94
+ for _ in range(n):
95
+ _ = hf.config.machine
96
+ toc = time.perf_counter()
97
+ elapsed_cache = toc - tic
98
+
99
+ assert elapsed_cache < elapsed_no_cache
100
+
101
+
102
+ def test_cache_read_only(new_null_config):
103
+ """Check we cannot modify the config when using the cache"""
104
+
105
+ # check we can set an item first:
106
+ hf.machine = "abc"
107
+ assert hf.machine == "abc"
108
+
109
+ with pytest.raises(ConfigReadOnlyError):
110
+ with hf.config.cached_config():
111
+ hf.config.set("machine", "123")
112
+
113
+ with pytest.raises(ConfigReadOnlyError):
114
+ with hf.config.cached_config():
115
+ hf.config.machine = "456"
116
+
117
+
118
+ def test_workflow_template_config_validation(new_null_config, tmp_path):
119
+ wkt = hf.WorkflowTemplate(
120
+ tasks=[],
121
+ config={"log_file_level": "debug"},
122
+ name="test_workflow_config_validation",
123
+ )
124
+ assert wkt.config == {"log_file_level": "debug"}
125
+
126
+
127
+ def test_workflow_template_config_validation_raises(unload_config, tmp_path):
128
+ with pytest.raises(ConfigNonConfigurableError):
129
+ hf.WorkflowTemplate(
130
+ tasks=[],
131
+ config={"bad_key": "debug"},
132
+ name="test_workflow_config_validation_raises",
133
+ )
134
+
135
+ # workflow template config validation should not need to load the whole config:
136
+ assert not hf.is_config_loaded
137
+
138
+
139
+ def test_config_with_updates(new_null_config):
140
+ level_1 = hf.config.get("log_console_level")
141
+ with hf.config._with_updates({"log_console_level": "debug"}):
142
+ level_2 = hf.config.get("log_console_level")
143
+ level_3 = hf.config.get("log_console_level")
144
+ assert level_1 == level_3 != level_2
145
+ hf.reload_config()
146
+
147
+
148
+ @pytest.mark.integration
149
+ def test_workflow_template_config_set(new_null_config, tmp_path):
150
+ """Test we can set a workflow-level config item and that it is correctly applied
151
+ during execution."""
152
+
153
+ t1 = hf.Task(
154
+ schema=hf.task_schemas.test_t1_conditional_OS,
155
+ inputs={"p1": 101},
156
+ )
157
+ log_path = tmp_path / "log.log"
158
+ hf.config.set("log_file_level", "warning")
159
+ hf.config.set("log_file_path", log_path)
160
+
161
+ log_str_1 = "this should not appear in the log file"
162
+ hf.submission_logger.debug(log_str_1)
163
+
164
+ log_str_2 = "this should appear in the log file"
165
+ hf.submission_logger.warning(log_str_2)
166
+
167
+ assert log_path.is_file()
168
+ log_file_contents = log_path.read_text()
169
+ assert log_str_1 not in log_file_contents
170
+ assert log_str_2 in log_file_contents
171
+
172
+ wk = hf.Workflow.from_template_data(
173
+ tasks=[t1],
174
+ config={"log_file_level": "debug"},
175
+ resources={"any": {"write_app_logs": True}},
176
+ workflow_name="test_workflow_config",
177
+ template_name="test_workflow_config",
178
+ path=tmp_path,
179
+ )
180
+ wk.submit(wait=True, status=False, add_to_known=False)
181
+
182
+ # check some DEBUG messages present in the run logs
183
+ debug_str = " DEBUG hpcflow.persistence:"
184
+
185
+ run = wk.get_EARs_from_IDs([0])[0]
186
+ run_log_path = run.get_app_log_path()
187
+ assert run_log_path.is_file()
188
+
189
+ run_log_contents = run_log_path.read_text()
190
+ assert debug_str in run_log_contents
191
+
192
+ # log file level should not have changed:
193
+ assert hf.config.get("log_file_level") == "warning"
194
+
195
+ hf.reload_config()
@@ -39,3 +39,50 @@ def test_decode(null_config, tmp_path: Path, store: str):
39
39
  assert sorted(iter_i.schema_parameters) == sorted(
40
40
  ["resources.any", "inputs.p1", "outputs.p2"]
41
41
  )
42
+
43
+
44
+ @pytest.mark.integration
45
+ def test_loop_skipped_true_single_action_elements(null_config, tmp_path):
46
+ ts = hf.TaskSchema(
47
+ objective="t1",
48
+ inputs=[hf.SchemaInput("p1")],
49
+ outputs=[hf.SchemaInput("p1")],
50
+ actions=[
51
+ hf.Action(
52
+ commands=[
53
+ hf.Command(
54
+ command="echo $(( <<parameter:p1>> + 100 ))",
55
+ stdout="<<int(parameter:p1)>>",
56
+ ),
57
+ ]
58
+ ),
59
+ ],
60
+ )
61
+ loop_term = hf.Rule(path="outputs.p1", condition={"value.equal_to": 300})
62
+ wk = hf.Workflow.from_template_data(
63
+ template_name="test_loop_skipped",
64
+ path=tmp_path,
65
+ tasks=[hf.Task(schema=ts, inputs={"p1": 100})],
66
+ loops=[
67
+ hf.Loop(name="my_loop", tasks=[0], termination=loop_term, num_iterations=3)
68
+ ],
69
+ )
70
+ # loop should terminate after the second iteration; third iteration should
71
+ # be loop-skipped
72
+ wk.submit(wait=True, add_to_known=False, status=False)
73
+ iters = wk.get_all_element_iterations()
74
+
75
+ assert not iters[0].loop_skipped
76
+ assert not iters[1].loop_skipped
77
+ assert iters[2].loop_skipped
78
+
79
+ # check latest iteration is the latest non-loop-skipped iteration:
80
+ assert wk.tasks[0].elements[0].latest_iteration_non_skipped.id_ == iters[1].id_
81
+
82
+ # check element inputs are from latest non-loop-skipped iteration:
83
+ assert wk.tasks[0].elements[0].inputs.p1.value == 200
84
+ assert wk.tasks[0].elements[0].get("inputs.p1") == 200
85
+
86
+ # check element outputs are from latest non-loop-skipped iteration:
87
+ assert wk.tasks[0].elements[0].outputs.p1.value == 300
88
+ assert wk.tasks[0].elements[0].get("outputs.p1") == 300