hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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 (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -12,13 +12,13 @@ import os
12
12
  from pathlib import Path
13
13
  import random
14
14
  import string
15
- from typing import Dict, Optional, Union
15
+ from typing import cast, TYPE_CHECKING
16
16
 
17
17
  from ruamel.yaml import YAML
18
18
 
19
- from hpcflow.sdk.core.validation import get_schema
19
+ from hpcflow.sdk.core.validation import Schema, get_schema
20
20
 
21
- from .errors import (
21
+ from hpcflow.sdk.config.errors import (
22
22
  ConfigChangeFileUpdateError,
23
23
  ConfigDefaultValidationError,
24
24
  ConfigFileInvocationIncompatibleError,
@@ -28,6 +28,12 @@ from .errors import (
28
28
  ConfigValidationError,
29
29
  )
30
30
 
31
+ if TYPE_CHECKING:
32
+ from typing import Any
33
+ from ..typing import PathLike
34
+ from .config import Config, ConfigOptions
35
+ from .types import ConfigDict, DefaultConfiguration, InvocationDescriptor
36
+
31
37
 
32
38
  class ConfigFile:
33
39
  """
@@ -43,7 +49,7 @@ class ConfigFile:
43
49
  Configuration options.
44
50
  """
45
51
 
46
- def __init__(self, directory, logger, config_options):
52
+ def __init__(self, directory, logger: logging.Logger, config_options: ConfigOptions):
47
53
  #: Where to log messages.
48
54
  self.logger = logger
49
55
  #: The directory containing the configuration file.
@@ -53,27 +59,59 @@ class ConfigFile:
53
59
  directory=directory,
54
60
  )
55
61
 
56
- self._configs = []
62
+ self._configs: list[Config] = []
57
63
 
58
64
  # set by _load_file_data:
59
- #: The path to the config file.
60
- self.path = None
61
- #: The cached contents of the config file.
62
- self.contents = None
63
- #: The parsed contents of the config file.
64
- self.data = None
65
- #: The parsed contents of the config file where the alternate parser was used.
66
- self.data_rt = None
65
+ self.__path: Path | None = None
66
+ self.__contents: str | None = None
67
+ self.__data: ConfigDict | None = None
68
+ self.__data_rt: ConfigDict | None = None
67
69
 
68
70
  self._load_file_data(config_options)
69
- self.file_schema = self._validate(self.data)
71
+ self.file_schema = self._validate(self.__data)
72
+
73
+ @property
74
+ def data(self) -> ConfigDict:
75
+ """
76
+ The parsed contents of the config file.
77
+ """
78
+ d = self.__data
79
+ assert d is not None
80
+ return d
81
+
82
+ @property
83
+ def data_rt(self) -> ConfigDict:
84
+ """
85
+ The parsed contents of the config file where the alternate parser was used.
86
+ """
87
+ drt = self.__data_rt
88
+ assert drt is not None
89
+ return drt
90
+
91
+ @property
92
+ def path(self) -> Path:
93
+ """
94
+ The path to the config file.
95
+ """
96
+ p = self.__path
97
+ assert p is not None
98
+ return p
99
+
100
+ @property
101
+ def contents(self) -> str:
102
+ """
103
+ The cached contents of the config file.
104
+ """
105
+ c = self.__contents
106
+ assert c is not None
107
+ return c
70
108
 
71
109
  @staticmethod
72
110
  def select_invocation(
73
- configs: Dict,
74
- run_time_info: Dict,
75
- path: Path,
76
- config_key: Union[str, None] = None,
111
+ configs: dict[str, Any],
112
+ run_time_info: dict[str, Any],
113
+ path: PathLike,
114
+ config_key: str | None = None,
77
115
  ) -> str:
78
116
  """Select a matching configuration for this invocation using run-time info."""
79
117
  if not config_key:
@@ -82,7 +120,6 @@ class ConfigFile:
82
120
  # for a config to "match", each "match key" must match the relevant run
83
121
  # time info attribute. If a "match key" has multiple values, at least
84
122
  # one value must match the run time info attribute:
85
- is_match = True
86
123
  for match_k, match_v in c_dat_i["invocation"]["match"].items():
87
124
  # test for a matching glob pattern (where multiple may be specified):
88
125
  if not isinstance(match_v, list):
@@ -93,39 +130,31 @@ class ConfigFile:
93
130
  except KeyError:
94
131
  raise ConfigFileInvocationUnknownMatchKey(match_k)
95
132
 
96
- is_match_i = False
97
- for match_i in match_v:
98
- if fnmatch.filter(names=[k_value], pat=match_i):
99
- is_match_i = True
100
- break
101
-
102
- if not is_match_i:
103
- is_match = False
133
+ if not any(
134
+ fnmatch.filter(names=[k_value], pat=match_i)
135
+ for match_i in match_v
136
+ ):
104
137
  break
105
-
106
- if is_match:
138
+ else:
107
139
  all_matches[c_name_i] = len(c_dat_i["invocation"]["match"])
108
140
 
109
- if all_matches:
110
- # for multiple matches select the more specific one:
111
- all_sorted = sorted(all_matches.items(), key=lambda x: x[1], reverse=True)
112
- config_key = all_sorted[0][0]
113
- else:
141
+ if not all_matches:
114
142
  raise ConfigFileInvocationIncompatibleError(config_key)
143
+ # for multiple matches select the more specific one:
144
+ config_key = max(all_matches.items(), key=lambda x: x[1])[0]
115
145
 
116
146
  elif config_key not in configs:
117
- raise ConfigInvocationKeyNotFoundError(config_key, path, list(configs.keys()))
147
+ raise ConfigInvocationKeyNotFoundError(config_key, path, list(configs))
118
148
 
119
149
  return config_key
120
150
 
121
- def _validate(self, data):
151
+ def _validate(self, data: dict[str, Any] | None) -> Schema:
122
152
  file_schema = get_schema("config_file_schema.yaml")
123
- file_validated = file_schema.validate(data)
124
- if not file_validated.is_valid:
153
+ if not (file_validated := file_schema.validate(data)).is_valid:
125
154
  raise ConfigFileValidationError(file_validated.get_failures_string())
126
155
  return file_schema
127
156
 
128
- def get_invoc_data(self, config_key):
157
+ def get_invoc_data(self, config_key: str) -> DefaultConfiguration:
129
158
  """
130
159
  Get the invocation data for the given configuration.
131
160
 
@@ -136,7 +165,7 @@ class ConfigFile:
136
165
  """
137
166
  return self.data["configs"][config_key]
138
167
 
139
- def get_invocation(self, config_key):
168
+ def get_invocation(self, config_key: str) -> InvocationDescriptor:
140
169
  """
141
170
  Get the invocation for the given configuration.
142
171
 
@@ -147,7 +176,7 @@ class ConfigFile:
147
176
  """
148
177
  return self.get_invoc_data(config_key)["invocation"]
149
178
 
150
- def save(self):
179
+ def save(self) -> None:
151
180
  """
152
181
  Write the (modified) configuration to the configuration file.
153
182
  """
@@ -155,33 +184,38 @@ class ConfigFile:
155
184
  new_data_rt = copy.deepcopy(self.data_rt)
156
185
  new_contents = ""
157
186
 
158
- modified_names = []
187
+ modified_names: list[str] = []
159
188
  for config in self._configs:
160
- modified_names += list(config._modified_keys.keys()) + config._unset_keys
161
- for k, v in config._modified_keys.items():
162
- new_data["configs"][config._config_key]["config"][k] = v
163
- new_data_rt["configs"][config._config_key]["config"][k] = v
189
+ modified_names.extend(config._modified_keys)
190
+ modified_names.extend(config._unset_keys)
191
+
192
+ new_data_config = new_data["configs"][config._config_key]["config"]
193
+ new_data_rt_config = new_data_rt["configs"][config._config_key]["config"]
194
+ new_data_config.update(config._modified_keys)
195
+ new_data_rt_config.update(config._modified_keys)
164
196
 
165
197
  for k in config._unset_keys:
166
- del new_data["configs"][config._config_key]["config"][k]
167
- del new_data_rt["configs"][config._config_key]["config"][k]
198
+ del cast("dict", new_data_config)[k]
199
+ del cast("dict", new_data_rt_config)[k]
168
200
 
169
201
  try:
170
202
  new_contents = self._dump(new_data_rt)
171
203
  except Exception as err:
172
204
  raise ConfigChangeFileUpdateError(names=modified_names, err=err) from None
173
205
 
174
- self.data = new_data
175
- self.data_rt = new_data_rt
176
- self.contents = new_contents
206
+ self.__data = new_data
207
+ self.__data_rt = new_data_rt
208
+ self.__contents = new_contents
177
209
 
178
210
  for config in self._configs:
179
- config._unset_keys = []
211
+ config._unset_keys = set()
180
212
  config._modified_keys = {}
181
213
 
182
214
  @staticmethod
183
215
  def _resolve_config_dir(
184
- config_opt, logger, directory: Optional[Union[str, Path]] = None
216
+ config_opt: ConfigOptions,
217
+ logger: logging.Logger,
218
+ directory: str | Path | None = None,
185
219
  ) -> Path:
186
220
  """Find the directory in which to locate the configuration file.
187
221
 
@@ -204,23 +238,23 @@ class ConfigFile:
204
238
  """
205
239
 
206
240
  if not directory:
207
- directory = Path(
241
+ path = Path(
208
242
  os.getenv(config_opt.directory_env_var, config_opt.default_directory)
209
243
  ).expanduser()
210
244
  else:
211
- directory = Path(directory)
245
+ path = Path(directory)
212
246
 
213
- if not directory.is_dir():
247
+ if not path.is_dir():
214
248
  logger.debug(
215
- f"Configuration directory does not exist. Generating here: {str(directory)!r}."
249
+ f"Configuration directory does not exist. Generating here: {str(path)!r}."
216
250
  )
217
- directory.mkdir()
251
+ path.mkdir()
218
252
  else:
219
- logger.debug(f"Using configuration directory: {str(directory)!r}.")
253
+ logger.debug(f"Using configuration directory: {str(path)!r}.")
220
254
 
221
- return directory.resolve()
255
+ return path.resolve()
222
256
 
223
- def _dump(self, config_data: Dict, path: Optional[Path] = None) -> str:
257
+ def _dump(self, config_data: ConfigDict, path: Path | None = None) -> str:
224
258
  """Dump the specified config data to the specified config file path.
225
259
 
226
260
  Parameters
@@ -265,22 +299,23 @@ class ConfigFile:
265
299
 
266
300
  return new_contents
267
301
 
268
- def add_default_config(self, config_options, name=None) -> str:
302
+ def add_default_config(
303
+ self, config_options: ConfigOptions, name: str | None = None
304
+ ) -> str:
269
305
  """Add a new default config to the config file, and create the file if it doesn't
270
306
  exist."""
271
307
 
272
- is_new_file = False
273
- if not self.path.exists():
308
+ if self.path.exists():
309
+ is_new_file = False
310
+ new_data: ConfigDict = copy.deepcopy(self.data)
311
+ new_data_rt: ConfigDict = copy.deepcopy(self.data_rt)
312
+ else:
274
313
  is_new_file = True
275
314
  new_data = {"configs": {}}
276
315
  new_data_rt = {"configs": {}}
277
- else:
278
- new_data = copy.deepcopy(self.data)
279
- new_data_rt = copy.deepcopy(self.data_rt)
280
316
 
281
317
  if not name:
282
- chars = string.ascii_letters
283
- name = "".join(random.choices(chars, k=6))
318
+ name = "".join(random.choices(string.ascii_letters, k=6))
284
319
 
285
320
  def_config = copy.deepcopy(config_options.default_config)
286
321
  new_config = {name: def_config}
@@ -303,14 +338,14 @@ class ConfigFile:
303
338
  except (ConfigFileValidationError, ConfigValidationError) as err:
304
339
  raise ConfigDefaultValidationError(err) from None
305
340
 
306
- self.data_rt = new_data_rt
307
- self.data = new_data
308
- self.contents = self._dump(new_data_rt)
341
+ self.__data_rt = new_data_rt
342
+ self.__data = new_data
343
+ self.__contents = self._dump(new_data_rt)
309
344
 
310
345
  return name
311
346
 
312
347
  @staticmethod
313
- def get_config_file_path(directory):
348
+ def get_config_file_path(directory: Path) -> Path:
314
349
  """
315
350
  Get the path to the configuration file.
316
351
  """
@@ -323,10 +358,10 @@ class ConfigFile:
323
358
  return path_yml
324
359
  return path_yaml
325
360
 
326
- def _load_file_data(self, config_options):
361
+ def _load_file_data(self, config_options: ConfigOptions):
327
362
  """Load data from the configuration file (config.yaml or config.yml)."""
328
363
 
329
- self.path = self.get_config_file_path(self.directory)
364
+ self.__path = self.get_config_file_path(self.directory)
330
365
  if not self.path.is_file():
331
366
  self.logger.info(
332
367
  "No config.yaml found in the configuration directory. Generating "
@@ -343,13 +378,13 @@ class ConfigFile:
343
378
  handle.seek(0)
344
379
  data_rt = yaml_rt.load(handle)
345
380
 
346
- self.contents = contents
347
- self.data = data
348
- self.data_rt = data_rt
381
+ self.__contents = contents
382
+ self.__data = data
383
+ self.__data_rt = data_rt
349
384
 
350
385
  def get_config_item(
351
- self, config_key, name, raise_on_missing=False, default_value=None
352
- ):
386
+ self, config_key: str, name: str, *, raise_on_missing=False, default_value=None
387
+ ) -> Any | None:
353
388
  """
354
389
  Get a configuration item.
355
390
 
@@ -365,11 +400,12 @@ class ConfigFile:
365
400
  The default value to use when the config item is absent
366
401
  (and ``raise_on_missing`` is not specified).
367
402
  """
368
- if raise_on_missing and name not in self.get_invoc_data(config_key)["config"]:
403
+ cfg = self.get_invoc_data(config_key)["config"]
404
+ if raise_on_missing and name not in cfg:
369
405
  raise ValueError(f"missing from file: {name!r}")
370
- return self.get_invoc_data(config_key)["config"].get(name, default_value)
406
+ return cfg.get(name, default_value)
371
407
 
372
- def is_item_set(self, config_key, name):
408
+ def is_item_set(self, config_key: str, name: str) -> bool:
373
409
  """
374
410
  Determine if a configuration item is set.
375
411
 
@@ -382,11 +418,11 @@ class ConfigFile:
382
418
  """
383
419
  try:
384
420
  self.get_config_item(config_key, name, raise_on_missing=True)
421
+ return True
385
422
  except ValueError:
386
423
  return False
387
- return True
388
424
 
389
- def rename_config_key(self, config_key: str, new_config_key: str):
425
+ def rename_config_key(self, config_key: str, new_config_key: str) -> None:
390
426
  """
391
427
  Change the config key of the loaded config.
392
428
 
@@ -409,22 +445,22 @@ class ConfigFile:
409
445
  config._meta_data["config_key"] = new_config_key
410
446
  config._config_key = new_config_key
411
447
 
412
- self.data_rt = new_data_rt
413
- self.data = new_data
414
- self.contents = self._dump(new_data_rt)
448
+ self.__data_rt = new_data_rt
449
+ self.__data = new_data
450
+ self.__contents = self._dump(new_data_rt)
415
451
 
416
452
  def update_invocation(
417
453
  self,
418
454
  config_key: str,
419
- environment_setup: Optional[str] = None,
420
- match: Optional[Dict] = None,
421
- ):
455
+ environment_setup: str | None = None,
456
+ match: dict[str, str | list[str]] | None = None,
457
+ ) -> None:
422
458
  """
423
459
  Modify the invocation parameters of the loaded config.
424
460
 
425
461
  Parameters
426
462
  ----------
427
- config_key: str
463
+ config_key:
428
464
  The name of the configuration within the configuration file.
429
465
  environment_setup:
430
466
  The new value of the ``environment_setup`` key.
@@ -442,6 +478,6 @@ class ConfigFile:
442
478
  if match:
443
479
  invoc["match"].update(match)
444
480
 
445
- self.data_rt = new_data_rt
446
- self.data = new_data
447
- self.contents = self._dump(new_data_rt)
481
+ self.__data_rt = new_data_rt
482
+ self.__data = new_data
483
+ self.__contents = self._dump(new_data_rt)