hpcflow 0.1.9__py3-none-any.whl → 0.2.0a271__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 (275) hide show
  1. hpcflow/__init__.py +2 -11
  2. hpcflow/__pyinstaller/__init__.py +5 -0
  3. hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
  4. hpcflow/_version.py +1 -1
  5. hpcflow/app.py +43 -0
  6. hpcflow/cli.py +2 -462
  7. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  8. hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
  9. hpcflow/data/jinja_templates/test/test_template.txt +8 -0
  10. hpcflow/data/programs/hello_world/README.md +1 -0
  11. hpcflow/data/programs/hello_world/hello_world.c +87 -0
  12. hpcflow/data/programs/hello_world/linux/hello_world +0 -0
  13. hpcflow/data/programs/hello_world/macos/hello_world +0 -0
  14. hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
  15. hpcflow/data/scripts/__init__.py +1 -0
  16. hpcflow/data/scripts/bad_script.py +2 -0
  17. hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
  18. hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
  19. hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
  20. hpcflow/data/scripts/do_nothing.py +2 -0
  21. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  22. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  23. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  24. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  25. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  26. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  27. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  28. hpcflow/data/scripts/generate_t1_file_01.py +7 -0
  29. hpcflow/data/scripts/import_future_script.py +7 -0
  30. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  31. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  32. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  33. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  34. hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
  35. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  36. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  37. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  38. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  39. hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
  40. hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
  41. hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
  42. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  43. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  44. hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
  45. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
  46. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  47. hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
  48. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
  49. hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
  50. hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
  51. hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
  52. hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
  53. hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
  54. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  55. hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
  56. hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
  57. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  58. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  59. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  60. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  61. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  62. hpcflow/data/scripts/parse_t1_file_01.py +4 -0
  63. hpcflow/data/scripts/script_exit_test.py +5 -0
  64. hpcflow/data/template_components/__init__.py +1 -0
  65. hpcflow/data/template_components/command_files.yaml +26 -0
  66. hpcflow/data/template_components/environments.yaml +13 -0
  67. hpcflow/data/template_components/parameters.yaml +14 -0
  68. hpcflow/data/template_components/task_schemas.yaml +139 -0
  69. hpcflow/data/workflows/workflow_1.yaml +5 -0
  70. hpcflow/examples.ipynb +1037 -0
  71. hpcflow/sdk/__init__.py +149 -0
  72. hpcflow/sdk/app.py +4266 -0
  73. hpcflow/sdk/cli.py +1479 -0
  74. hpcflow/sdk/cli_common.py +385 -0
  75. hpcflow/sdk/config/__init__.py +5 -0
  76. hpcflow/sdk/config/callbacks.py +246 -0
  77. hpcflow/sdk/config/cli.py +388 -0
  78. hpcflow/sdk/config/config.py +1410 -0
  79. hpcflow/sdk/config/config_file.py +501 -0
  80. hpcflow/sdk/config/errors.py +272 -0
  81. hpcflow/sdk/config/types.py +150 -0
  82. hpcflow/sdk/core/__init__.py +38 -0
  83. hpcflow/sdk/core/actions.py +3857 -0
  84. hpcflow/sdk/core/app_aware.py +25 -0
  85. hpcflow/sdk/core/cache.py +224 -0
  86. hpcflow/sdk/core/command_files.py +814 -0
  87. hpcflow/sdk/core/commands.py +424 -0
  88. hpcflow/sdk/core/element.py +2071 -0
  89. hpcflow/sdk/core/enums.py +221 -0
  90. hpcflow/sdk/core/environment.py +256 -0
  91. hpcflow/sdk/core/errors.py +1043 -0
  92. hpcflow/sdk/core/execute.py +207 -0
  93. hpcflow/sdk/core/json_like.py +809 -0
  94. hpcflow/sdk/core/loop.py +1320 -0
  95. hpcflow/sdk/core/loop_cache.py +282 -0
  96. hpcflow/sdk/core/object_list.py +933 -0
  97. hpcflow/sdk/core/parameters.py +3371 -0
  98. hpcflow/sdk/core/rule.py +196 -0
  99. hpcflow/sdk/core/run_dir_files.py +57 -0
  100. hpcflow/sdk/core/skip_reason.py +7 -0
  101. hpcflow/sdk/core/task.py +3792 -0
  102. hpcflow/sdk/core/task_schema.py +993 -0
  103. hpcflow/sdk/core/test_utils.py +538 -0
  104. hpcflow/sdk/core/types.py +447 -0
  105. hpcflow/sdk/core/utils.py +1207 -0
  106. hpcflow/sdk/core/validation.py +87 -0
  107. hpcflow/sdk/core/values.py +477 -0
  108. hpcflow/sdk/core/workflow.py +4820 -0
  109. hpcflow/sdk/core/zarr_io.py +206 -0
  110. hpcflow/sdk/data/__init__.py +13 -0
  111. hpcflow/sdk/data/config_file_schema.yaml +34 -0
  112. hpcflow/sdk/data/config_schema.yaml +260 -0
  113. hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
  114. hpcflow/sdk/data/files_spec_schema.yaml +5 -0
  115. hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
  116. hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
  117. hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
  118. hpcflow/sdk/demo/__init__.py +3 -0
  119. hpcflow/sdk/demo/cli.py +242 -0
  120. hpcflow/sdk/helper/__init__.py +3 -0
  121. hpcflow/sdk/helper/cli.py +137 -0
  122. hpcflow/sdk/helper/helper.py +300 -0
  123. hpcflow/sdk/helper/watcher.py +192 -0
  124. hpcflow/sdk/log.py +288 -0
  125. hpcflow/sdk/persistence/__init__.py +18 -0
  126. hpcflow/sdk/persistence/base.py +2817 -0
  127. hpcflow/sdk/persistence/defaults.py +6 -0
  128. hpcflow/sdk/persistence/discovery.py +39 -0
  129. hpcflow/sdk/persistence/json.py +954 -0
  130. hpcflow/sdk/persistence/pending.py +948 -0
  131. hpcflow/sdk/persistence/store_resource.py +203 -0
  132. hpcflow/sdk/persistence/types.py +309 -0
  133. hpcflow/sdk/persistence/utils.py +73 -0
  134. hpcflow/sdk/persistence/zarr.py +2388 -0
  135. hpcflow/sdk/runtime.py +320 -0
  136. hpcflow/sdk/submission/__init__.py +3 -0
  137. hpcflow/sdk/submission/enums.py +70 -0
  138. hpcflow/sdk/submission/jobscript.py +2379 -0
  139. hpcflow/sdk/submission/schedulers/__init__.py +281 -0
  140. hpcflow/sdk/submission/schedulers/direct.py +233 -0
  141. hpcflow/sdk/submission/schedulers/sge.py +376 -0
  142. hpcflow/sdk/submission/schedulers/slurm.py +598 -0
  143. hpcflow/sdk/submission/schedulers/utils.py +25 -0
  144. hpcflow/sdk/submission/shells/__init__.py +52 -0
  145. hpcflow/sdk/submission/shells/base.py +229 -0
  146. hpcflow/sdk/submission/shells/bash.py +504 -0
  147. hpcflow/sdk/submission/shells/os_version.py +115 -0
  148. hpcflow/sdk/submission/shells/powershell.py +352 -0
  149. hpcflow/sdk/submission/submission.py +1402 -0
  150. hpcflow/sdk/submission/types.py +140 -0
  151. hpcflow/sdk/typing.py +194 -0
  152. hpcflow/sdk/utils/arrays.py +69 -0
  153. hpcflow/sdk/utils/deferred_file.py +55 -0
  154. hpcflow/sdk/utils/hashing.py +16 -0
  155. hpcflow/sdk/utils/patches.py +31 -0
  156. hpcflow/sdk/utils/strings.py +69 -0
  157. hpcflow/tests/api/test_api.py +32 -0
  158. hpcflow/tests/conftest.py +123 -0
  159. hpcflow/tests/data/__init__.py +0 -0
  160. hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
  161. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  162. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  163. hpcflow/tests/data/workflow_1.json +10 -0
  164. hpcflow/tests/data/workflow_1.yaml +5 -0
  165. hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
  166. hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
  167. hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
  168. hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
  169. hpcflow/tests/programs/test_programs.py +180 -0
  170. hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
  171. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  172. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
  173. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  174. hpcflow/tests/scripts/test_main_scripts.py +1361 -0
  175. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  176. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  177. hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
  178. hpcflow/tests/unit/test_action.py +1066 -0
  179. hpcflow/tests/unit/test_action_rule.py +24 -0
  180. hpcflow/tests/unit/test_app.py +132 -0
  181. hpcflow/tests/unit/test_cache.py +46 -0
  182. hpcflow/tests/unit/test_cli.py +172 -0
  183. hpcflow/tests/unit/test_command.py +377 -0
  184. hpcflow/tests/unit/test_config.py +195 -0
  185. hpcflow/tests/unit/test_config_file.py +162 -0
  186. hpcflow/tests/unit/test_element.py +666 -0
  187. hpcflow/tests/unit/test_element_iteration.py +88 -0
  188. hpcflow/tests/unit/test_element_set.py +158 -0
  189. hpcflow/tests/unit/test_group.py +115 -0
  190. hpcflow/tests/unit/test_input_source.py +1479 -0
  191. hpcflow/tests/unit/test_input_value.py +398 -0
  192. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  193. hpcflow/tests/unit/test_json_like.py +1247 -0
  194. hpcflow/tests/unit/test_loop.py +2674 -0
  195. hpcflow/tests/unit/test_meta_task.py +325 -0
  196. hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
  197. hpcflow/tests/unit/test_object_list.py +116 -0
  198. hpcflow/tests/unit/test_parameter.py +243 -0
  199. hpcflow/tests/unit/test_persistence.py +664 -0
  200. hpcflow/tests/unit/test_resources.py +243 -0
  201. hpcflow/tests/unit/test_run.py +286 -0
  202. hpcflow/tests/unit/test_run_directories.py +29 -0
  203. hpcflow/tests/unit/test_runtime.py +9 -0
  204. hpcflow/tests/unit/test_schema_input.py +372 -0
  205. hpcflow/tests/unit/test_shell.py +129 -0
  206. hpcflow/tests/unit/test_slurm.py +39 -0
  207. hpcflow/tests/unit/test_submission.py +502 -0
  208. hpcflow/tests/unit/test_task.py +2560 -0
  209. hpcflow/tests/unit/test_task_schema.py +182 -0
  210. hpcflow/tests/unit/test_utils.py +616 -0
  211. hpcflow/tests/unit/test_value_sequence.py +549 -0
  212. hpcflow/tests/unit/test_values.py +91 -0
  213. hpcflow/tests/unit/test_workflow.py +827 -0
  214. hpcflow/tests/unit/test_workflow_template.py +186 -0
  215. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  216. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  217. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  218. hpcflow/tests/unit/utils/test_patches.py +5 -0
  219. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  220. hpcflow/tests/unit/utils/test_strings.py +97 -0
  221. hpcflow/tests/workflows/__init__.py +0 -0
  222. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  223. hpcflow/tests/workflows/test_jobscript.py +355 -0
  224. hpcflow/tests/workflows/test_run_status.py +198 -0
  225. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  226. hpcflow/tests/workflows/test_submission.py +140 -0
  227. hpcflow/tests/workflows/test_workflows.py +564 -0
  228. hpcflow/tests/workflows/test_zip.py +18 -0
  229. hpcflow/viz_demo.ipynb +6794 -0
  230. hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
  231. hpcflow-0.2.0a271.dist-info/METADATA +65 -0
  232. hpcflow-0.2.0a271.dist-info/RECORD +237 -0
  233. {hpcflow-0.1.9.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
  234. hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
  235. hpcflow/api.py +0 -458
  236. hpcflow/archive/archive.py +0 -308
  237. hpcflow/archive/cloud/cloud.py +0 -47
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -432
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -232
  243. hpcflow/copytree.py +0 -66
  244. hpcflow/data/examples/_config.yml +0 -14
  245. hpcflow/data/examples/damask/demo/1.run.yml +0 -4
  246. hpcflow/data/examples/damask/demo/2.process.yml +0 -29
  247. hpcflow/data/examples/damask/demo/geom.geom +0 -2052
  248. hpcflow/data/examples/damask/demo/load.load +0 -1
  249. hpcflow/data/examples/damask/demo/material.config +0 -185
  250. hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
  251. hpcflow/data/examples/damask/inputs/load.load +0 -1
  252. hpcflow/data/examples/damask/inputs/material.config +0 -185
  253. hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
  254. hpcflow/data/examples/damask/profiles/damask.yml +0 -4
  255. hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
  256. hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
  257. hpcflow/data/examples/damask/profiles/default.yml +0 -6
  258. hpcflow/data/examples/thinking.yml +0 -177
  259. hpcflow/errors.py +0 -2
  260. hpcflow/init_db.py +0 -37
  261. hpcflow/models.py +0 -2549
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -323
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -167
  268. hpcflow/variables.py +0 -544
  269. hpcflow-0.1.9.dist-info/METADATA +0 -168
  270. hpcflow-0.1.9.dist-info/RECORD +0 -45
  271. hpcflow-0.1.9.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.9.dist-info/top_level.txt +0 -1
  273. /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
  274. /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
  275. /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
@@ -0,0 +1,814 @@
1
+ """
2
+ Model of files that hold commands.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import copy
7
+ from dataclasses import dataclass, field, InitVar
8
+ from pathlib import Path
9
+ from textwrap import dedent
10
+ from typing import Protocol, cast, overload, TYPE_CHECKING
11
+ from typing_extensions import Final, override
12
+
13
+ from hpcflow.sdk.core.types import ActionData
14
+ from hpcflow.sdk.typing import PathLike, hydrate, ParamSource
15
+ from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
16
+ from hpcflow.sdk.core.utils import search_dir_files_by_regex
17
+ from hpcflow.sdk.core.zarr_io import zarr_decode
18
+ from hpcflow.sdk.core.values import process_demo_data_strings
19
+
20
+ if TYPE_CHECKING:
21
+ import os
22
+ from collections.abc import Mapping
23
+ from typing import Any, ClassVar
24
+ from typing_extensions import Self
25
+ from .actions import Action, ActionRule
26
+ from .environment import Environment
27
+ from .object_list import CommandFilesList
28
+ from .parameters import Parameter
29
+ from .task import ElementSet
30
+ from .workflow import Workflow
31
+
32
+
33
+ class FileNamePart(Protocol):
34
+ """
35
+ A filename or piece of filename that can be expanded.
36
+ """
37
+
38
+ def value(self, directory: str | os.PathLike = ".") -> str | list[str]:
39
+ """
40
+ Get the part of the file, possibly with directory specified.
41
+ Implementations of this may ignore the directory.
42
+ If a pattern, the expanded value may be a list of strings.
43
+ """
44
+
45
+
46
+ @dataclass(init=False)
47
+ @hydrate
48
+ class FileSpec(JSONLike):
49
+ """
50
+ A specification of a file handled by a workflow.
51
+ """
52
+
53
+ _validation_schema: ClassVar[str] = "files_spec_schema.yaml"
54
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
55
+ ChildObjectSpec(name="name", class_name="FileNameSpec"),
56
+ )
57
+
58
+ #: Label for this file specification.
59
+ label: Final[str]
60
+ #: The name of the file.
61
+ name: Final[FileNameSpec]
62
+ #: Documentation for the file specification.
63
+ doc: Final[str]
64
+ _hash_value: str | None = field(default=None, repr=False)
65
+
66
+ def __init__(
67
+ self,
68
+ label: str,
69
+ name: str | FileNameSpec,
70
+ doc: str = "",
71
+ _hash_value: str | None = None,
72
+ ) -> None:
73
+ self.label = label
74
+ self.name = self._app.FileNameSpec(name) if isinstance(name, str) else name
75
+ self.doc = doc
76
+ self._hash_value = _hash_value
77
+ self.__hash = hash((label, self.name))
78
+
79
+ def value(self, directory: str | os.PathLike = ".") -> str:
80
+ """
81
+ The path to a file, optionally resolved with respect to a particular directory.
82
+ """
83
+ return cast("str", self.name.value(directory))
84
+
85
+ def __eq__(self, other: object) -> bool:
86
+ if not isinstance(other, self.__class__):
87
+ return False
88
+ return self.label == other.label and self.name == other.name
89
+
90
+ def __hash__(self) -> int:
91
+ return self.__hash
92
+
93
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
94
+ d.pop("_FileSpec__hash")
95
+ return d
96
+
97
+ @property
98
+ def stem(self) -> FileNameStem:
99
+ """
100
+ The stem of the file name.
101
+ """
102
+ return self.name.stem
103
+
104
+ @property
105
+ def ext(self) -> FileNameExt:
106
+ """
107
+ The extension of the file name.
108
+ """
109
+ return self.name.ext
110
+
111
+ @property
112
+ def documentation(self) -> str:
113
+ """
114
+ Documentation for rendering via Jinja.
115
+ """
116
+ if self.doc:
117
+ import markupsafe
118
+
119
+ return markupsafe.Markup(self.doc)
120
+ return repr(self)
121
+
122
+
123
+ @hydrate
124
+ class FileNameSpec(JSONLike):
125
+ """
126
+ The name of a file handled by a workflow, or a pattern that matches multiple files.
127
+
128
+ Parameters
129
+ ----------
130
+ name: str
131
+ The name or pattern.
132
+ args: list
133
+ Positional arguments to use when formatting the name.
134
+ Can be omitted if the name does not contain a Python formatting pattern.
135
+ is_regex: bool
136
+ If true, the name is used as a regex to search for actual files.
137
+ """
138
+
139
+ def __init__(
140
+ self,
141
+ name: str,
142
+ args: list[FileNamePart] | None = None,
143
+ is_regex: bool = False,
144
+ ) -> None:
145
+ #: The name or pattern.
146
+ self.name: Final[str] = name
147
+ #: Positional arguments to use when formatting the name.
148
+ self.args: Final[tuple[FileNamePart, ...]] = tuple(args or [])
149
+ #: Whether the name is used as a regex to search for actual files.
150
+ self.is_regex: Final[bool] = is_regex
151
+ self.__hash = hash((name, self.args, is_regex))
152
+
153
+ def __eq__(self, other: object) -> bool:
154
+ if not isinstance(other, self.__class__):
155
+ return False
156
+ return (
157
+ self.name == other.name
158
+ and self.args == other.args
159
+ and self.is_regex == other.is_regex
160
+ )
161
+
162
+ def __hash__(self) -> int:
163
+ return self.__hash
164
+
165
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
166
+ d.pop("_FileNameSpec__hash")
167
+ return d
168
+
169
+ @property
170
+ def stem(self) -> FileNameStem:
171
+ """
172
+ The stem of the name or pattern.
173
+ """
174
+ return self._app.FileNameStem(self)
175
+
176
+ @property
177
+ def ext(self) -> FileNameExt:
178
+ """
179
+ The extension of the name or pattern.
180
+ """
181
+ return self._app.FileNameExt(self)
182
+
183
+ def value(self, directory: str | os.PathLike = ".") -> list[str] | str:
184
+ """
185
+ Get the template-resolved name of the file
186
+ (or files matched if the name is a regex pattern).
187
+
188
+ Parameters
189
+ ----------
190
+ directory: PathLike
191
+ Where to resolve values with respect to.
192
+ """
193
+ format_args = [arg.value(Path(directory)) for arg in self.args]
194
+ value = self.name.format(*format_args)
195
+ if self.is_regex:
196
+ return search_dir_files_by_regex(value, directory=directory)
197
+ return value
198
+
199
+ def __repr__(self) -> str:
200
+ return f"{self.__class__.__name__}({self.name})"
201
+
202
+
203
+ @dataclass
204
+ class FileNameStem(JSONLike):
205
+ """
206
+ The stem of a file name.
207
+ """
208
+
209
+ #: The file specification this is derived from.
210
+ file_name: FileNameSpec
211
+
212
+ def value(self, directory: str | os.PathLike = ".") -> str:
213
+ """
214
+ Get the stem, possibly with directory specified.
215
+ """
216
+ d = self.file_name.value(directory)
217
+ if self.file_name.is_regex:
218
+ raise ValueError("cannot get the stem of a regex match")
219
+ assert not isinstance(d, list)
220
+ return Path(d).stem
221
+
222
+
223
+ @dataclass
224
+ class FileNameExt(JSONLike):
225
+ """
226
+ The extension of a file name.
227
+ """
228
+
229
+ #: The file specification this is derived from.
230
+ file_name: FileNameSpec
231
+
232
+ def value(self, directory: str | os.PathLike = ".") -> str:
233
+ """
234
+ Get the extension.
235
+ """
236
+ d = self.file_name.value(directory)
237
+ if self.file_name.is_regex:
238
+ raise ValueError("cannot get the extension of a regex match")
239
+ assert not isinstance(d, list)
240
+ return Path(d).suffix
241
+
242
+
243
+ @dataclass
244
+ @hydrate
245
+ class InputFileGenerator(JSONLike):
246
+ """
247
+ Represents a script that is run to generate input files for an action.
248
+
249
+ Parameters
250
+ ----------
251
+ input_file:
252
+ The file to generate.
253
+ inputs: list[~hpcflow.app.Parameter]
254
+ The input parameters to the generator.
255
+ script:
256
+ The script that generates the input.
257
+ environment:
258
+ The environment in which to run the generator.
259
+ script_pass_env_spec:
260
+ Whether to pass in the environment.
261
+ abortable:
262
+ Whether the generator can be stopped early.
263
+ Quick-running scripts tend to not need this.
264
+ rules: list[~hpcflow.app.ActionRule]
265
+ User-specified rules for whether to run the generator.
266
+ """
267
+
268
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
269
+ ChildObjectSpec(
270
+ name="input_file",
271
+ class_name="FileSpec",
272
+ shared_data_primary_key="label",
273
+ shared_data_name="command_files",
274
+ ),
275
+ ChildObjectSpec(
276
+ name="inputs",
277
+ class_name="Parameter",
278
+ is_multiple=True,
279
+ json_like_name="from_inputs",
280
+ shared_data_primary_key="typ",
281
+ shared_data_name="parameters",
282
+ ),
283
+ ChildObjectSpec(
284
+ name="rules",
285
+ class_name="ActionRule",
286
+ is_multiple=True,
287
+ parent_ref="input_file_generator",
288
+ ),
289
+ )
290
+
291
+ #: The file to generate.
292
+ input_file: FileSpec
293
+ #: The input parameters to the generator.
294
+ inputs: list[Parameter]
295
+ #: The script that generates the inputs.
296
+ script: str | None = None
297
+ #: Information about data input to the script.
298
+ script_data_in: str | Mapping[str, str | ActionData] | None = None
299
+ #: Information about data output from the script.
300
+ script_data_out: str | Mapping[str, str | ActionData] | None = None
301
+ #: The environment in which to run the generator.
302
+ environment: Environment | None = None
303
+ #: Whether to pass in the environment.
304
+ script_pass_env_spec: bool = False
305
+ #: The builtin path to a template that generates the input file.
306
+ jinja_template: str | None = None
307
+ #: The external path to a template that generates the input file.
308
+ jinja_template_path: str | None = None
309
+ #: Whether the generator can be stopped early.
310
+ #: Quick-running scripts tend to not need this.
311
+ abortable: bool = False
312
+ #: User-specified rules for whether to run the generator.
313
+ rules: list[ActionRule] = field(default_factory=list)
314
+ #: Whether the generator requires a working directory.
315
+ requires_dir: bool = True
316
+
317
+ def get_action_rules(self) -> list[ActionRule]:
318
+ """
319
+ Get the rules that allow testing if this input file generator must be run or
320
+ not for a given element.
321
+ """
322
+ return [
323
+ self._app.ActionRule.check_missing(f"input_files.{self.input_file.label}")
324
+ ] + self.rules
325
+
326
+
327
+ @dataclass
328
+ @hydrate
329
+ class OutputFileParser(JSONLike):
330
+ """
331
+ Represents a script that is run to parse output files from an action and create outputs.
332
+
333
+ Parameters
334
+ ----------
335
+ output_files: list[FileSpec]
336
+ The output files that this parser will parse.
337
+ output: ~hpcflow.app.Parameter
338
+ The singular output parsed by this parser. Not to be confused with `outputs` (plural).
339
+ script: str
340
+ The name of the file containing the output file parser source.
341
+ environment: ~hpcflow.app.Environment
342
+ The environment to use to run the parser.
343
+ inputs: list[str]
344
+ The other inputs to the parser.
345
+ outputs: list[str]
346
+ Optional multiple outputs from the upstream actions of the schema that are
347
+ required to parametrise this parser.
348
+ options: dict
349
+ Miscellaneous options.
350
+ script_pass_env_spec: bool
351
+ Whether to pass the environment specifier to the script.
352
+ abortable: bool
353
+ Whether this script can be aborted.
354
+ save_files: list[str]
355
+ The files that should be saved to the persistent store for the workflow.
356
+ clean_up: list[str]
357
+ The files that should be immediately removed.
358
+ rules: list[~hpcflow.app.ActionRule]
359
+ Rules for whether to enable this parser.
360
+ """
361
+
362
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
363
+ ChildObjectSpec(
364
+ name="output",
365
+ class_name="Parameter",
366
+ shared_data_name="parameters",
367
+ shared_data_primary_key="typ",
368
+ ),
369
+ ChildObjectSpec(
370
+ name="output_files",
371
+ json_like_name="from_files",
372
+ class_name="FileSpec",
373
+ is_multiple=True,
374
+ shared_data_primary_key="label",
375
+ shared_data_name="command_files",
376
+ ),
377
+ ChildObjectSpec(
378
+ name="save_files",
379
+ class_name="FileSpec",
380
+ is_multiple=True,
381
+ shared_data_primary_key="label",
382
+ shared_data_name="command_files",
383
+ ),
384
+ ChildObjectSpec(
385
+ name="clean_up",
386
+ class_name="FileSpec",
387
+ is_multiple=True,
388
+ shared_data_primary_key="label",
389
+ shared_data_name="command_files",
390
+ ),
391
+ ChildObjectSpec(
392
+ name="rules",
393
+ class_name="ActionRule",
394
+ is_multiple=True,
395
+ parent_ref="output_file_parser",
396
+ ),
397
+ )
398
+
399
+ #: The output files that this parser will parse.
400
+ output_files: list[FileSpec]
401
+ #: The singular output parsed by this parser.
402
+ #: Not to be confused with :py:attr:`outputs` (plural).
403
+ output: Parameter | None = None
404
+ #: The name of the file containing the output file parser source.
405
+ script: str | None = None
406
+ #: The environment to use to run the parser.
407
+ environment: Environment | None = None
408
+ #: The other inputs to the parser.
409
+ inputs: list[str] | None = None
410
+ #: Optional multiple outputs from the upstream actions of the schema that are
411
+ #: required to parametrise this parser.
412
+ #: Not to be confused with :py:attr:`output` (singular).
413
+ outputs: list[str] | None = None
414
+ #: Miscellaneous options.
415
+ options: dict[str, Any] | None = None
416
+ #: Whether to pass the environment specifier to the script.
417
+ script_pass_env_spec: bool = False
418
+ #: Whether this script can be aborted.
419
+ abortable: bool = False
420
+ #: The files that should be saved to the persistent store for the workflow.
421
+ save_files: InitVar[list[FileSpec] | bool] = True
422
+ _save_files: list[FileSpec] = field(init=False)
423
+ #: The files that should be immediately removed.
424
+ clean_up: list[str] = field(default_factory=list)
425
+ #: Rules for whether to enable this parser.
426
+ rules: list[ActionRule] = field(default_factory=list)
427
+ #: Whether the parser requires a working directory.
428
+ requires_dir: bool = True
429
+
430
+ def __post_init__(self, save_files: list[FileSpec] | bool) -> None:
431
+ if not save_files:
432
+ # save no files
433
+ self._save_files = []
434
+ elif save_files is True:
435
+ # save all output files
436
+ self._save_files = [out_f for out_f in self.output_files]
437
+ else:
438
+ self._save_files = save_files
439
+
440
+ @override
441
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
442
+ d = super()._postprocess_to_dict(d)
443
+ if "_save_files" in d:
444
+ d["save_files"] = d.pop("_save_files")
445
+ return d
446
+
447
+ @classmethod
448
+ def from_json_like( # type: ignore[override]
449
+ cls, json_like: dict[str, Any], shared_data: Mapping | None = None
450
+ ) -> Self:
451
+ if "save_files" in json_like:
452
+ if not json_like["save_files"]:
453
+ json_like["save_files"] = []
454
+ elif json_like["save_files"] is True:
455
+ json_like["save_files"] = [i for i in json_like["output_files"]]
456
+ return super().from_json_like(json_like, shared_data)
457
+
458
+ def get_action_rules(self) -> list[ActionRule]:
459
+ """Get the rules that allow testing if this output file parser must be run or not
460
+ for a given element."""
461
+ return [
462
+ self._app.ActionRule.check_missing(f"output_files.{out_f.label}")
463
+ for out_f in self.output_files
464
+ ] + self.rules
465
+
466
+
467
+ @hydrate
468
+ class _FileContentsSpecifier(JSONLike):
469
+ """Class to represent the contents of a file, either via a file-system path or
470
+ directly."""
471
+
472
+ #: What file is this? Only if known.
473
+ file: FileSpec
474
+
475
+ def __init__(
476
+ self,
477
+ path: Path | str | None = None,
478
+ contents: str | None = None,
479
+ extension: str = "",
480
+ store_contents: bool = True,
481
+ ) -> None:
482
+ if path is not None and contents is not None:
483
+ raise ValueError("Specify exactly one of `path` and `contents`.")
484
+
485
+ if contents is not None and not store_contents:
486
+ raise ValueError(
487
+ "`store_contents` cannot be set to False if `contents` was specified."
488
+ )
489
+
490
+ self._path = process_demo_data_strings(self._app, path)
491
+ self._contents = contents
492
+ self._extension = extension
493
+ self._store_contents = store_contents
494
+
495
+ # assigned by `make_persistent`
496
+ self._workflow: Workflow | None = None
497
+ self._value_group_idx: int | None = None
498
+
499
+ # assigned by parent `ElementSet`
500
+ self._element_set: ElementSet | None = None
501
+
502
+ def __deepcopy__(self, memo: dict | None) -> Self:
503
+ kwargs = self.to_dict()
504
+ value_group_idx = kwargs.pop("value_group_idx")
505
+ obj = self.__class__(**copy.deepcopy(kwargs, memo))
506
+ obj._value_group_idx = value_group_idx
507
+ obj._workflow = self._workflow
508
+ obj._element_set = self._element_set
509
+ return obj
510
+
511
+ @property
512
+ def normalised_path(self) -> str:
513
+ """
514
+ Full workflow value path to the file.
515
+
516
+ Note
517
+ ----
518
+ This is not the same as the path in the filesystem, but is closely
519
+ related.
520
+ """
521
+ return str(self._path) if self._path else "."
522
+
523
+ @override
524
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
525
+ out = super()._postprocess_to_dict(d)
526
+ if "_workflow" in out:
527
+ del out["_workflow"]
528
+ return {k.lstrip("_"): v for k, v in out.items()}
529
+
530
+ @classmethod
531
+ def _json_like_constructor(cls, json_like: dict[str, Any]) -> Self:
532
+ """Invoked by `JSONLike.from_json_like` instead of `__init__`."""
533
+
534
+ _value_group_idx = json_like.pop("value_group_idx", None)
535
+ obj = cls(**json_like)
536
+ obj._value_group_idx = _value_group_idx
537
+
538
+ return obj
539
+
540
+ def _get_members(self, ensure_contents: bool = False) -> dict[str, Any]:
541
+ out = self.to_dict()
542
+ del out["value_group_idx"]
543
+
544
+ if ensure_contents and self._store_contents and self._contents is None:
545
+ out["contents"] = self.read_contents()
546
+
547
+ return out
548
+
549
+ def make_persistent(
550
+ self,
551
+ workflow: Workflow,
552
+ source: ParamSource,
553
+ ) -> tuple[str, list[int], bool]:
554
+ """Save to a persistent workflow.
555
+
556
+ Returns
557
+ -------
558
+ String is the data path for this task input and integer list
559
+ contains the indices of the parameter data Zarr groups where the data is
560
+ stored.
561
+
562
+ """
563
+
564
+ if self._value_group_idx is not None:
565
+ data_ref = self._value_group_idx
566
+ is_new = False
567
+ if not workflow.check_parameters_exist(data_ref):
568
+ raise RuntimeError(
569
+ f"{self.__class__.__name__} has a data reference "
570
+ f"({data_ref}), but does not exist in the workflow."
571
+ )
572
+ # TODO: log if already persistent.
573
+ else:
574
+ data_ref = workflow._add_file(
575
+ store_contents=self.store_contents,
576
+ is_input=True,
577
+ source=source,
578
+ path=self.path,
579
+ contents=self.contents,
580
+ filename=self.file.name.name,
581
+ )
582
+ # data_ref = workflow._add_parameter_data(
583
+ # data=self._get_members(ensure_contents=True, use_file_label=True),
584
+ # source=source,
585
+ # )
586
+ is_new = True
587
+ self._value_group_idx = data_ref
588
+ self._workflow = workflow
589
+ self._path = None
590
+ self._contents = None
591
+ self._extension = ""
592
+ self._store_contents = True
593
+
594
+ return (self.normalised_path, [data_ref], is_new)
595
+
596
+ @overload
597
+ def _get_value(self, value_name: None = None) -> dict[str, Any]: ...
598
+
599
+ @overload
600
+ def _get_value(self, value_name: str) -> Any: ...
601
+
602
+ def _get_value(self, value_name: str | None = None) -> Any:
603
+ # TODO: fix
604
+ assert self._value_group_idx is None
605
+ if self._value_group_idx is not None:
606
+ from ..persistence.zarr import ZarrPersistentStore
607
+
608
+ assert isinstance(self.workflow._store, ZarrPersistentStore)
609
+ # FIXME: Next two lines are both thoroughly broken, but at least resolve to something
610
+ grp = self.workflow._store._get_parameter_group(self._value_group_idx)
611
+ val = zarr_decode(grp)
612
+ else:
613
+ val = self._get_members(ensure_contents=(value_name == "contents"))
614
+ if value_name:
615
+ return val.get(value_name)
616
+
617
+ return val
618
+
619
+ def read_contents(self) -> str:
620
+ """
621
+ Get the actual contents of the file.
622
+ """
623
+ with self.__path.open("r") as fh:
624
+ return fh.read()
625
+
626
+ @property
627
+ def __path(self) -> Path:
628
+ path = self._get_value("path")
629
+ assert path is not None
630
+ return Path(path)
631
+
632
+ @property
633
+ def path(self) -> Path | None:
634
+ """
635
+ The path to the file.
636
+ """
637
+ path = self._get_value("path")
638
+ return Path(path) if path else None
639
+
640
+ @property
641
+ def store_contents(self) -> Any:
642
+ """
643
+ Whether the file's contents are stored in the workflow's persistent store.
644
+ """
645
+ return self._get_value("store_contents")
646
+
647
+ @property
648
+ def contents(self) -> str:
649
+ """
650
+ The contents of the file.
651
+ """
652
+ if self.store_contents:
653
+ return self._get_value("contents")
654
+ else:
655
+ return self.read_contents()
656
+
657
+ @property
658
+ def extension(self) -> str:
659
+ """
660
+ The extension of the file.
661
+ """
662
+ return self._get_value("extension")
663
+
664
+ @property
665
+ def workflow(self) -> Workflow:
666
+ """
667
+ The owning workflow.
668
+ """
669
+ if self._workflow:
670
+ return self._workflow
671
+ elif self._element_set:
672
+ w_tmpl = self._element_set.task_template.workflow_template
673
+ if w_tmpl and w_tmpl.workflow:
674
+ return w_tmpl.workflow
675
+ raise NotImplementedError
676
+
677
+
678
+ @hydrate
679
+ class InputFile(_FileContentsSpecifier):
680
+ """
681
+ An input file.
682
+
683
+ Parameters
684
+ ----------
685
+ file:
686
+ What file is this?
687
+ path: Path
688
+ Where is the (original) file?
689
+ contents: str
690
+ What is the contents of the file (if already known)?
691
+ extension: str
692
+ What is the extension of the file?
693
+ store_contents: bool
694
+ Are the file's contents to be cached in the workflow persistent store?
695
+ """
696
+
697
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
698
+ ChildObjectSpec(
699
+ name="file",
700
+ class_name="FileSpec",
701
+ shared_data_name="command_files",
702
+ shared_data_primary_key="label",
703
+ ),
704
+ )
705
+
706
+ def __init__(
707
+ self,
708
+ file: FileSpec | str,
709
+ path: Path | str | None = None,
710
+ contents: str | None = None,
711
+ extension: str = "",
712
+ store_contents: bool = True,
713
+ ) -> None:
714
+ if not isinstance(file, FileSpec):
715
+ files: CommandFilesList = self._app.command_files
716
+ self.file = files.get(file)
717
+ else:
718
+ self.file = file
719
+
720
+ super().__init__(path, contents, extension, store_contents)
721
+
722
+ def _get_members(
723
+ self, ensure_contents: bool = False, use_file_label: bool = False
724
+ ) -> dict[str, Any]:
725
+ out = super()._get_members(ensure_contents)
726
+ if use_file_label:
727
+ out["file"] = self.file.label
728
+ return out
729
+
730
+ def __repr__(self) -> str:
731
+ val_grp_idx = ""
732
+ if self._value_group_idx is not None:
733
+ val_grp_idx = f", value_group_idx={self._value_group_idx}"
734
+
735
+ path_str = ""
736
+ if self.path is not None:
737
+ path_str = f", path={self.path!r}"
738
+
739
+ return (
740
+ f"{self.__class__.__name__}("
741
+ f"file={self.file.label!r}"
742
+ f"{path_str}"
743
+ f"{val_grp_idx}"
744
+ f")"
745
+ )
746
+
747
+ @property
748
+ def normalised_files_path(self) -> str:
749
+ """
750
+ Standard name for the file within the workflow.
751
+ """
752
+ return self.file.label
753
+
754
+ @property
755
+ def normalised_path(self) -> str:
756
+ return f"input_files.{self.normalised_files_path}"
757
+
758
+
759
+ @hydrate
760
+ class InputFileGeneratorSource(_FileContentsSpecifier):
761
+ """
762
+ The source of code for use in an input file generator.
763
+
764
+ Parameters
765
+ ----------
766
+ generator:
767
+ How to generate the file.
768
+ path:
769
+ Path to the file to generate.
770
+ contents:
771
+ Contents of the file. Only used when recreating this object.
772
+ extension:
773
+ File name extension.
774
+ """
775
+
776
+ def __init__(
777
+ self,
778
+ generator: InputFileGenerator,
779
+ path: Path | str | None = None,
780
+ contents: str | None = None,
781
+ extension: str = "",
782
+ ):
783
+ #: How to generate the file.
784
+ self.generator = generator
785
+ super().__init__(path, contents, extension)
786
+
787
+
788
+ @hydrate
789
+ class OutputFileParserSource(_FileContentsSpecifier):
790
+ """
791
+ The source of code for use in an output file parser.
792
+
793
+ Parameters
794
+ ----------
795
+ parser:
796
+ How to parse the file.
797
+ path: Path
798
+ Path to the file to parse.
799
+ contents:
800
+ Contents of the file. Only used when recreating this object.
801
+ extension:
802
+ File name extension.
803
+ """
804
+
805
+ def __init__(
806
+ self,
807
+ parser: OutputFileParser,
808
+ path: Path | str | None = None,
809
+ contents: str | None = None,
810
+ extension: str = "",
811
+ ):
812
+ #: How to parse the file.
813
+ self.parser = parser
814
+ super().__init__(path, contents, extension)