executorlib 1.9.3__tar.gz → 1.9.4__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 (54) hide show
  1. {executorlib-1.9.3 → executorlib-1.9.4}/PKG-INFO +33 -6
  2. {executorlib-1.9.3 → executorlib-1.9.4}/README.md +27 -0
  3. {executorlib-1.9.3 → executorlib-1.9.4}/pyproject.toml +6 -6
  4. executorlib-1.9.4/src/executorlib/_version.py +24 -0
  5. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/executor/base.py +12 -0
  6. executorlib-1.9.4/src/executorlib/standalone/batched.py +39 -0
  7. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/command.py +1 -1
  8. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/hdf.py +11 -5
  9. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/inputcheck.py +54 -1
  10. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/interactive/arguments.py +15 -4
  11. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/interactive/communication.py +6 -0
  12. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/select.py +30 -0
  13. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/serialize.py +12 -0
  14. executorlib-1.9.4/src/executorlib/standalone/validate.py +117 -0
  15. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/base.py +25 -1
  16. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/file/shared.py +97 -3
  17. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/dependency.py +44 -22
  18. executorlib-1.9.3/src/executorlib/_version.py +0 -34
  19. executorlib-1.9.3/src/executorlib/standalone/batched.py +0 -27
  20. executorlib-1.9.3/src/executorlib/standalone/validate.py +0 -64
  21. {executorlib-1.9.3 → executorlib-1.9.4}/.gitignore +0 -0
  22. {executorlib-1.9.3 → executorlib-1.9.4}/LICENSE +0 -0
  23. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/__init__.py +0 -0
  24. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/api.py +0 -0
  25. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/backend/__init__.py +0 -0
  26. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/backend/cache_parallel.py +0 -0
  27. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/backend/cache_serial.py +0 -0
  28. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/backend/interactive_parallel.py +0 -0
  29. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/backend/interactive_serial.py +0 -0
  30. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/executor/__init__.py +0 -0
  31. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/executor/flux.py +0 -0
  32. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/executor/single.py +0 -0
  33. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/executor/slurm.py +0 -0
  34. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/__init__.py +0 -0
  35. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/error.py +0 -0
  36. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/interactive/__init__.py +0 -0
  37. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/interactive/backend.py +0 -0
  38. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/interactive/spawner.py +0 -0
  39. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/queue.py +0 -0
  40. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/standalone/scheduler.py +0 -0
  41. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/__init__.py +0 -0
  42. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/file/__init__.py +0 -0
  43. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/file/backend.py +0 -0
  44. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/file/spawner_pysqa.py +0 -0
  45. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/file/spawner_subprocess.py +0 -0
  46. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/file/task_scheduler.py +0 -0
  47. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/__init__.py +0 -0
  48. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/blockallocation.py +0 -0
  49. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/dependency_plot.py +0 -0
  50. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/onetoone.py +0 -0
  51. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/shared.py +0 -0
  52. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/spawner_flux.py +0 -0
  53. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/spawner_pysqa.py +0 -0
  54. {executorlib-1.9.3 → executorlib-1.9.4}/src/executorlib/task_scheduler/interactive/spawner_slurm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: executorlib
3
- Version: 1.9.3
3
+ Version: 1.9.4
4
4
  Summary: Up-scale python functions for high performance computing (HPC) with executorlib.
5
5
  Project-URL: Homepage, https://github.com/pyiron/executorlib
6
6
  Project-URL: Documentation, https://executorlib.readthedocs.io
@@ -36,7 +36,7 @@ License: BSD 3-Clause License
36
36
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
37
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
38
  License-File: LICENSE
39
- Keywords: executor,flux-framework,high performance computing,hpc,slurm,task scheduler
39
+ Keywords: concurrent.futures,distributed computing,executor,flux-framework,gpu,high performance computing,hpc,mpi4py,parallel computing,python,scientific computing,slurm,task scheduler
40
40
  Classifier: Development Status :: 5 - Production/Stable
41
41
  Classifier: Intended Audience :: Science/Research
42
42
  Classifier: License :: OSI Approved :: BSD License
@@ -52,16 +52,16 @@ Requires-Dist: pyzmq<=27.1.0,>=25.0.0
52
52
  Provides-Extra: all
53
53
  Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'all'
54
54
  Requires-Dist: ipython<=9.9.0,>=7.33.0; extra == 'all'
55
- Requires-Dist: mpi4py<=4.1.1,>=3.1.4; extra == 'all'
55
+ Requires-Dist: mpi4py<=4.1.2,>=3.1.4; extra == 'all'
56
56
  Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'all'
57
57
  Requires-Dist: pydantic<=2.13.4,>=2.5.3; extra == 'all'
58
58
  Requires-Dist: pygraphviz<=1.14,>=1.10; extra == 'all'
59
- Requires-Dist: pysqa==0.4.1; extra == 'all'
59
+ Requires-Dist: pysqa==0.4.2; extra == 'all'
60
60
  Provides-Extra: cache
61
61
  Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'cache'
62
62
  Provides-Extra: cluster
63
63
  Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'cluster'
64
- Requires-Dist: pysqa==0.4.1; extra == 'cluster'
64
+ Requires-Dist: pysqa==0.4.2; extra == 'cluster'
65
65
  Provides-Extra: graph
66
66
  Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'graph'
67
67
  Requires-Dist: pygraphviz<=1.14,>=1.10; extra == 'graph'
@@ -70,7 +70,7 @@ Requires-Dist: ipython<=9.9.0,>=7.33.0; extra == 'graphnotebook'
70
70
  Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'graphnotebook'
71
71
  Requires-Dist: pygraphviz<=1.14,>=1.10; extra == 'graphnotebook'
72
72
  Provides-Extra: mpi
73
- Requires-Dist: mpi4py<=4.1.1,>=3.1.4; extra == 'mpi'
73
+ Requires-Dist: mpi4py<=4.1.2,>=3.1.4; extra == 'mpi'
74
74
  Provides-Extra: validation
75
75
  Requires-Dist: pydantic<=2.13.4,>=2.5.3; extra == 'validation'
76
76
  Description-Content-Type: text/markdown
@@ -96,6 +96,26 @@ Up-scale python functions for high performance computing (HPC) with executorlib.
96
96
  machine learning pipelines and simulation workflows executorlib provides optional caching of intermediate results for
97
97
  iterative development in interactive environments like jupyter notebooks.
98
98
 
99
+ ## Why executorlib?
100
+ executorlib is the lightest path to take *existing* Python functions and scale them across high performance computing
101
+ (HPC) nodes — with per-function-call resource control and native [SLURM](https://slurm.schedmd.com) and
102
+ [flux](http://flux-framework.org) integration — without rewriting your code into a new paradigm. It extends the standard
103
+ library [Executor interface](https://docs.python.org/3/library/concurrent.futures.html#executor-objects) you already
104
+ know, rather than asking you to adopt a new data, actor, or workflow model.
105
+
106
+ | | executorlib | [Futures](https://docs.python.org/3/library/concurrent.futures.html) | [Dask](https://www.dask.org) | [Parsl](https://parsl-project.org) | [Ray](https://www.ray.io) |
107
+ |---|---|---|---|---|---|
108
+ | Drop-in `Executor` API | ✅ | ✅ | ⚠️ | ⚠️ | ❌ |
109
+ | Per-call resource assignment | ✅ | ❌ | ⚠️ | ✅ | ✅ |
110
+ | Native HPC scheduler (SLURM/flux) | ✅ | ❌ | ⚠️ | ✅ | ⚠️ |
111
+ | MPI-parallel functions | ✅ | ❌ | ⚠️ | ⚠️ | ⚠️ |
112
+ | Caching of results | ✅ | ❌ | ⚠️ | ✅ | ❌ |
113
+ | Setup / learning overhead | Low | Very low | Medium | Medium | Medium |
114
+
115
+ ✅ first-class · ⚠️ possible via add-on/config · ❌ not supported. See the full
116
+ [comparison: when to use which](https://executorlib.readthedocs.io/en/latest/comparison.html) for honest guidance on
117
+ when another tool is the better fit.
118
+
99
119
  ## Examples
100
120
  The Python standard library provides the [Executor interface](https://docs.python.org/3/library/concurrent.futures.html#executor-objects)
101
121
  with the [ProcessPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor) and the
@@ -207,6 +227,13 @@ as hierarchical job scheduler within the allocations.
207
227
  * [HPC Job Executor](https://executorlib.readthedocs.io/en/latest/installation.html#hpc-job-executor)
208
228
  * [Visualisation](https://executorlib.readthedocs.io/en/latest/installation.html#visualisation)
209
229
  * [For Developers](https://executorlib.readthedocs.io/en/latest/installation.html#for-developers)
230
+ * [Comparison](https://executorlib.readthedocs.io/en/latest/comparison.html#comparison)
231
+ * [At a glace](https://executorlib.readthedocs.io/en/latest/comparison.html#at-a-glance)
232
+ * [Concurrent futures](https://executorlib.readthedocs.io/en/latest/comparison.html#concurrent-futures-the-python-standard-library)
233
+ * [Dask](https://executorlib.readthedocs.io/en/latest/comparison.html#dask)
234
+ * [Parsl](https://executorlib.readthedocs.io/en/latest/comparison.html#parsl)
235
+ * [Ray](https://executorlib.readthedocs.io/en/latest/comparison.html#ray)
236
+ * [Choose executorlib when](https://executorlib.readthedocs.io/en/latest/comparison.html#choose-executorlib-when)
210
237
  * [Single Node Executor](https://executorlib.readthedocs.io/en/latest/1-single-node.html)
211
238
  * [Basic Functionality](https://executorlib.readthedocs.io/en/latest/1-single-node.html#basic-functionality)
212
239
  * [Parallel Functions](https://executorlib.readthedocs.io/en/latest/1-single-node.html#parallel-functions)
@@ -19,6 +19,26 @@ Up-scale python functions for high performance computing (HPC) with executorlib.
19
19
  machine learning pipelines and simulation workflows executorlib provides optional caching of intermediate results for
20
20
  iterative development in interactive environments like jupyter notebooks.
21
21
 
22
+ ## Why executorlib?
23
+ executorlib is the lightest path to take *existing* Python functions and scale them across high performance computing
24
+ (HPC) nodes — with per-function-call resource control and native [SLURM](https://slurm.schedmd.com) and
25
+ [flux](http://flux-framework.org) integration — without rewriting your code into a new paradigm. It extends the standard
26
+ library [Executor interface](https://docs.python.org/3/library/concurrent.futures.html#executor-objects) you already
27
+ know, rather than asking you to adopt a new data, actor, or workflow model.
28
+
29
+ | | executorlib | [Futures](https://docs.python.org/3/library/concurrent.futures.html) | [Dask](https://www.dask.org) | [Parsl](https://parsl-project.org) | [Ray](https://www.ray.io) |
30
+ |---|---|---|---|---|---|
31
+ | Drop-in `Executor` API | ✅ | ✅ | ⚠️ | ⚠️ | ❌ |
32
+ | Per-call resource assignment | ✅ | ❌ | ⚠️ | ✅ | ✅ |
33
+ | Native HPC scheduler (SLURM/flux) | ✅ | ❌ | ⚠️ | ✅ | ⚠️ |
34
+ | MPI-parallel functions | ✅ | ❌ | ⚠️ | ⚠️ | ⚠️ |
35
+ | Caching of results | ✅ | ❌ | ⚠️ | ✅ | ❌ |
36
+ | Setup / learning overhead | Low | Very low | Medium | Medium | Medium |
37
+
38
+ ✅ first-class · ⚠️ possible via add-on/config · ❌ not supported. See the full
39
+ [comparison: when to use which](https://executorlib.readthedocs.io/en/latest/comparison.html) for honest guidance on
40
+ when another tool is the better fit.
41
+
22
42
  ## Examples
23
43
  The Python standard library provides the [Executor interface](https://docs.python.org/3/library/concurrent.futures.html#executor-objects)
24
44
  with the [ProcessPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor) and the
@@ -130,6 +150,13 @@ as hierarchical job scheduler within the allocations.
130
150
  * [HPC Job Executor](https://executorlib.readthedocs.io/en/latest/installation.html#hpc-job-executor)
131
151
  * [Visualisation](https://executorlib.readthedocs.io/en/latest/installation.html#visualisation)
132
152
  * [For Developers](https://executorlib.readthedocs.io/en/latest/installation.html#for-developers)
153
+ * [Comparison](https://executorlib.readthedocs.io/en/latest/comparison.html#comparison)
154
+ * [At a glace](https://executorlib.readthedocs.io/en/latest/comparison.html#at-a-glance)
155
+ * [Concurrent futures](https://executorlib.readthedocs.io/en/latest/comparison.html#concurrent-futures-the-python-standard-library)
156
+ * [Dask](https://executorlib.readthedocs.io/en/latest/comparison.html#dask)
157
+ * [Parsl](https://executorlib.readthedocs.io/en/latest/comparison.html#parsl)
158
+ * [Ray](https://executorlib.readthedocs.io/en/latest/comparison.html#ray)
159
+ * [Choose executorlib when](https://executorlib.readthedocs.io/en/latest/comparison.html#choose-executorlib-when)
133
160
  * [Single Node Executor](https://executorlib.readthedocs.io/en/latest/1-single-node.html)
134
161
  * [Basic Functionality](https://executorlib.readthedocs.io/en/latest/1-single-node.html#basic-functionality)
135
162
  * [Parallel Functions](https://executorlib.readthedocs.io/en/latest/1-single-node.html#parallel-functions)
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
2
  requires = [
3
- "hatchling>=1.27.0,<=1.29.0",
3
+ "hatchling>=1.27.0,<=1.30.1",
4
4
  "hatch-vcs==0.5.0",
5
5
  "cloudpickle>=2.0.0,<=3.1.2",
6
6
  "pyzmq>=25.0.0,<=27.1.0",
@@ -15,7 +15,7 @@ authors = [
15
15
  ]
16
16
  readme = "README.md"
17
17
  license = { file = "LICENSE" }
18
- keywords = ["high performance computing", "hpc", "task scheduler", "slurm", "flux-framework", "executor"]
18
+ keywords = ["high performance computing", "hpc", "task scheduler", "slurm", "flux-framework", "executor", "python", "parallel computing", "distributed computing", "scientific computing", "concurrent.futures", "mpi4py", "gpu"]
19
19
  requires-python = ">3.9, <3.14"
20
20
  classifiers = [
21
21
  "Development Status :: 5 - Production/Stable",
@@ -50,17 +50,17 @@ graphnotebook = [
50
50
  "networkx>=2.8.8,<=3.6.1",
51
51
  "ipython>=7.33.0,<=9.9.0",
52
52
  ]
53
- mpi = ["mpi4py>=3.1.4,<=4.1.1"]
53
+ mpi = ["mpi4py>=3.1.4,<=4.1.2"]
54
54
  cluster = [
55
- "pysqa==0.4.1",
55
+ "pysqa==0.4.2",
56
56
  "h5py>=3.6.0,<=3.16.0",
57
57
  ]
58
58
  validation = [
59
59
  "pydantic>=2.5.3,<=2.13.4",
60
60
  ]
61
61
  all = [
62
- "mpi4py>=3.1.4,<=4.1.1",
63
- "pysqa==0.4.1",
62
+ "mpi4py>=3.1.4,<=4.1.2",
63
+ "pysqa==0.4.2",
64
64
  "h5py>=3.6.0,<=3.16.0",
65
65
  "pydantic>=2.5.3,<=2.13.4",
66
66
  "pygraphviz>=1.10,<=1.14",
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.9.4'
22
+ __version_tuple__ = version_tuple = (1, 9, 4)
23
+
24
+ __commit_id__ = commit_id = None
@@ -25,10 +25,22 @@ class BaseExecutor(FutureExecutor, ABC):
25
25
 
26
26
  @property
27
27
  def max_workers(self) -> Optional[int]:
28
+ """
29
+ Return the number of parallel workers configured for this executor.
30
+
31
+ Returns:
32
+ Optional[int]: The maximum number of parallel workers, or None if unconstrained.
33
+ """
28
34
  return self._task_scheduler.max_workers
29
35
 
30
36
  @max_workers.setter
31
37
  def max_workers(self, max_workers: int):
38
+ """
39
+ Set the number of parallel workers on the underlying task scheduler.
40
+
41
+ Args:
42
+ max_workers (int): New maximum number of parallel workers.
43
+ """
32
44
  self._task_scheduler.max_workers = max_workers
33
45
 
34
46
  @property
@@ -0,0 +1,39 @@
1
+ from concurrent.futures import Future
2
+
3
+
4
+ def batched_futures(
5
+ lst: list[Future], nested_skip_lst: list[Future[list]], n: int
6
+ ) -> tuple[bool, list[Future]]:
7
+ """
8
+ Batch n completed future objects. If the number of completed futures is smaller than n and the end of the batch is
9
+ not reached yet, then an empty list is returned. If n future objects are done, which are not included in the skip_set
10
+ then they are returned as batch.
11
+
12
+ Args:
13
+ lst (list): list of all future objects
14
+ nested_skip_lst (list): list of future objects, which contain the list of future objects ids which should be skipped for the batch
15
+ n (int): batch size
16
+
17
+ Returns:
18
+ list: results of the batched futures
19
+ """
20
+ skip_set = {fid for f in nested_skip_lst for fid in f.result()}
21
+
22
+ done_lst = []
23
+ failed_lst = []
24
+ n_expected = min(n, len(lst) - len(skip_set))
25
+ for v in lst:
26
+ if id(v) not in skip_set and v.done():
27
+ if v.exception() is not None:
28
+ failed_lst.append(v)
29
+ elif id(v) not in skip_set and v.done():
30
+ done_lst.append(v)
31
+ if len(done_lst) == n_expected:
32
+ return True, done_lst
33
+ if (len(lst) - len(skip_set)) == len(failed_lst):
34
+ return (
35
+ False,
36
+ failed_lst[:n_expected],
37
+ ) # raise the exception only after all futures have failed
38
+ else:
39
+ return True, []
@@ -153,7 +153,7 @@ def generate_slurm_command(
153
153
  command_prepend_lst += ["--mpi=" + pmi_mode]
154
154
  if num_nodes is not None:
155
155
  command_prepend_lst += ["-N", str(num_nodes)]
156
- if threads_per_core > 1:
156
+ if threads_per_core >= 1:
157
157
  command_prepend_lst += ["--cpus-per-task=" + str(threads_per_core)]
158
158
  if gpus_per_core > 0:
159
159
  command_prepend_lst += ["--gpus-per-task=" + str(gpus_per_core)]
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import os
2
3
  from concurrent.futures import Future
3
4
  from time import sleep
@@ -34,10 +35,11 @@ def dump(file_name: Optional[str], data_dict: dict) -> None:
34
35
  with h5py.File(file_name_abs, "a") as fname:
35
36
  for data_key, data_value in data_dict.items():
36
37
  if data_key in group_dict:
37
- fname.create_dataset(
38
- name="/" + group_dict[data_key],
39
- data=np.void(cloudpickle.dumps(data_value)),
40
- )
38
+ with contextlib.suppress(ValueError):
39
+ fname.create_dataset(
40
+ name="/" + group_dict[data_key],
41
+ data=np.void(cloudpickle.dumps(data_value)),
42
+ )
41
43
 
42
44
 
43
45
  def load(file_name: str) -> dict:
@@ -171,7 +173,11 @@ def get_cache_files(cache_directory: str) -> list[str]:
171
173
  file_lst = []
172
174
  cache_directory_abs = os.path.abspath(cache_directory)
173
175
  for dirpath, _, filenames in os.walk(cache_directory_abs):
174
- file_lst += [os.path.join(dirpath, f) for f in filenames if f.endswith("_o.h5")]
176
+ file_lst += [
177
+ os.path.join(dirpath, f)
178
+ for f in filenames
179
+ if f.endswith("_o.h5") or f.endswith("_i.h5")
180
+ ]
175
181
  return file_lst
176
182
 
177
183
 
@@ -141,6 +141,16 @@ def check_init_function(
141
141
  def check_max_workers_and_cores(
142
142
  max_workers: Optional[int], max_cores: Optional[int]
143
143
  ) -> None:
144
+ """
145
+ Check that neither max_workers nor max_cores is set when using a pysqa-based backend.
146
+
147
+ Args:
148
+ max_workers (int, optional): Maximum number of workers.
149
+ max_cores (int, optional): Maximum number of cores.
150
+
151
+ Raises:
152
+ ValueError: If max_workers or max_cores is not None.
153
+ """
144
154
  if max_workers is not None:
145
155
  raise ValueError(
146
156
  "The number of workers cannot be controlled with the pysqa based backend."
@@ -152,6 +162,15 @@ def check_max_workers_and_cores(
152
162
 
153
163
 
154
164
  def check_hostname_localhost(hostname_localhost: Optional[bool]) -> None:
165
+ """
166
+ Check that hostname_localhost is not set when using a pysqa-based backend.
167
+
168
+ Args:
169
+ hostname_localhost (bool, optional): Flag to use localhost for ZMQ connections.
170
+
171
+ Raises:
172
+ ValueError: If hostname_localhost is not None.
173
+ """
155
174
  if hostname_localhost is not None:
156
175
  raise ValueError(
157
176
  "The option to connect to hosts based on their hostname is not available with the pysqa based backend."
@@ -159,6 +178,16 @@ def check_hostname_localhost(hostname_localhost: Optional[bool]) -> None:
159
178
 
160
179
 
161
180
  def check_restart_limit(restart_limit: int, block_allocation: bool = True) -> None:
181
+ """
182
+ Check that restart_limit is only used together with block_allocation.
183
+
184
+ Args:
185
+ restart_limit (int): Maximum number of times a worker process may be restarted.
186
+ block_allocation (bool): Whether block allocation is enabled. Defaults to True.
187
+
188
+ Raises:
189
+ ValueError: If restart_limit is non-zero and block_allocation is False.
190
+ """
162
191
  if not block_allocation and restart_limit != 0:
163
192
  raise ValueError(
164
193
  "The option to specify a restart limit for worker processes is only available with block_allocation=True."
@@ -166,6 +195,15 @@ def check_restart_limit(restart_limit: int, block_allocation: bool = True) -> No
166
195
 
167
196
 
168
197
  def check_pmi_mode(pmi_mode: Optional[str]) -> None:
198
+ """
199
+ Check that pmi_mode is not set on a local workstation without SLURM or flux.
200
+
201
+ Args:
202
+ pmi_mode (str, optional): PMI interface name (e.g. "pmix", "pmi1", "pmi2").
203
+
204
+ Raises:
205
+ ValueError: If pmi_mode is not None.
206
+ """
169
207
  if pmi_mode is not None:
170
208
  raise ValueError(
171
209
  "The option to specify the pmi mode is not available on a local workstation, it requires SLURM or flux."
@@ -199,7 +237,22 @@ def validate_number_of_cores(
199
237
  set_local_cores: bool = False,
200
238
  ) -> int:
201
239
  """
202
- Validate the number of cores and return the appropriate value.
240
+ Validate the number of cores and return the number of workers to use.
241
+
242
+ Precedence: max_cores / cores_per_worker > max_workers > CPU count (with warning).
243
+
244
+ Args:
245
+ max_cores (int, optional): Total number of cores available.
246
+ max_workers (int, optional): Explicit number of parallel workers.
247
+ cores_per_worker (int, optional): Number of cores allocated to each worker. Defaults to 1.
248
+ set_local_cores (bool): When True, fall back to the local CPU count if neither
249
+ max_cores nor max_workers is given instead of raising an error. Defaults to False.
250
+
251
+ Returns:
252
+ int: Number of parallel workers to use.
253
+
254
+ Raises:
255
+ ValueError: If neither max_cores nor max_workers is set and set_local_cores is False.
203
256
  """
204
257
  if max_cores is not None and max_workers is None and cores_per_worker is not None:
205
258
  return int(max_cores / cores_per_worker)
@@ -27,10 +27,21 @@ def get_future_objects_from_input(args: tuple, kwargs: dict):
27
27
 
28
28
  find_future_in_list(lst=args)
29
29
  find_future_in_list(lst=kwargs.values())
30
- boolean_flag = len([future for future in future_lst if future.done()]) == len(
31
- future_lst
32
- )
33
- return future_lst, boolean_flag
30
+
31
+ return future_lst
32
+
33
+
34
+ def check_list_of_futures_is_done(future_lst: list[Future]) -> bool:
35
+ """
36
+ Check if all future objects in the list of future objects are done
37
+
38
+ Args:
39
+ future_lst (list): list of future objects
40
+
41
+ Returns:
42
+ bool: True if all future objects in the list of future objects are done, False otherwise
43
+ """
44
+ return len([future for future in future_lst if future.done()]) == len(future_lst)
34
45
 
35
46
 
36
47
  def get_exception_lst(future_lst: list[Future]) -> list:
@@ -240,6 +240,9 @@ def interface_connect(host: str, port: str) -> tuple[zmq.Context, zmq.Socket]:
240
240
  Args:
241
241
  host (str): hostname of the host running the SocketInterface instance to connect to.
242
242
  port (str): port on the host the SocketInterface instance is running on.
243
+
244
+ Returns:
245
+ Tuple[zmq.Context, zmq.Socket]: The ZMQ context and the connected PAIR socket.
243
246
  """
244
247
  context = zmq.Context()
245
248
  socket = context.socket(zmq.PAIR)
@@ -265,6 +268,9 @@ def interface_receive(socket: Optional[zmq.Socket]) -> dict:
265
268
 
266
269
  Args:
267
270
  socket (zmq.Socket): socket for the connection
271
+
272
+ Returns:
273
+ dict: Deserialized dictionary received from the socket, or an empty dict if socket is None.
268
274
  """
269
275
  if socket is not None:
270
276
  return cloudpickle.loads(socket.recv())
@@ -3,21 +3,51 @@ from typing import Any, Optional
3
3
 
4
4
 
5
5
  class FutureSelector(Future):
6
+ """
7
+ A Future wrapper that returns a single element from a result that is a tuple, list, or dict.
8
+
9
+ This is used by split_future() and get_item_from_future() to give callers an individual
10
+ Future-like object for each output of a function that returns a collection.
11
+
12
+ Args:
13
+ future (Future): The underlying future whose result is a collection.
14
+ selector (int | str): Index (for sequences) or key (for mappings) used to extract
15
+ the desired element from the result.
16
+ """
17
+
6
18
  def __init__(self, future: Future, selector: int | str):
19
+ """
20
+ Args:
21
+ future (Future): The underlying future whose result is a collection.
22
+ selector (int | str): Index or key used to extract an element from the result.
23
+ """
7
24
  self._future = future
8
25
  self._selector = selector
9
26
  super().__init__()
10
27
 
11
28
  def __getattr__(self, attr: str) -> Any:
29
+ """Delegate attribute access to the wrapped future."""
12
30
  return getattr(self._future, attr)
13
31
 
14
32
  def __setattr__(self, name: str, value: Any):
33
+ """Set attributes on the wrapped future, except for the two private instance variables."""
15
34
  if name in ["_future", "_selector"]:
16
35
  super().__setattr__(name, value)
17
36
  else:
18
37
  setattr(self._future, name, value)
19
38
 
20
39
  def result(self, timeout: Optional[float] = None) -> Any:
40
+ """
41
+ Return the selected element from the underlying future's result.
42
+
43
+ Args:
44
+ timeout (float, optional): Maximum seconds to wait for the result. Defaults to None
45
+ (wait indefinitely).
46
+
47
+ Returns:
48
+ Any: The element at position/key ``selector`` in the underlying result, or None if
49
+ the underlying result is None.
50
+ """
21
51
  result = self._future.result(timeout=timeout)
22
52
  if result is not None:
23
53
  return result[self._selector]
@@ -102,6 +102,18 @@ def _get_hash(binary: bytes) -> str:
102
102
 
103
103
 
104
104
  def _get_function_name(fn: Callable) -> str:
105
+ """
106
+ Return a human-readable name for a callable.
107
+
108
+ For regular functions and methods the ``__name__`` attribute is used. For callable objects
109
+ (classes implementing ``__call__``) the class name is extracted from ``__class__``.
110
+
111
+ Args:
112
+ fn (Callable): The callable whose name should be determined.
113
+
114
+ Returns:
115
+ str: A short name string suitable for use as part of a cache key prefix.
116
+ """
105
117
  if hasattr(fn, "__name__"):
106
118
  return fn.__name__
107
119
  else:
@@ -0,0 +1,117 @@
1
+ import warnings
2
+ from typing import Optional
3
+
4
+ try:
5
+ from pydantic import BaseModel, Extra
6
+
7
+ HAS_PYDANTIC = True
8
+ except ImportError:
9
+ from dataclasses import dataclass
10
+
11
+ BaseModel = object
12
+ Extra = None
13
+ HAS_PYDANTIC = False
14
+
15
+
16
+ class ResourceDictValidation(BaseModel):
17
+ """
18
+ Pydantic (or dataclass fallback) model for validating resource dictionaries passed to task submissions.
19
+
20
+ Attributes:
21
+ cores (int, optional): Number of MPI cores to be used for each function call.
22
+ threads_per_core (int, optional): Number of OpenMP threads to be used for each function call.
23
+ gpus_per_core (int, optional): Number of GPUs per worker.
24
+ cwd (str, optional): Current working directory where the parallel python task is executed.
25
+ cache_key (str, optional): External cache key to identify tasks on the file system.
26
+ cache_directory (str, optional): The directory to store cache files.
27
+ num_nodes (int, optional): Number of compute nodes used for the evaluation.
28
+ exclusive (bool, optional): Reserve exclusive access to selected compute nodes.
29
+ error_log_file (str, optional): Path to the error log file.
30
+ run_time_max (int, optional): Maximum allowed execution time in seconds.
31
+ priority (int, optional): Queuing system priority for the task.
32
+ slurm_cmd_args (list[str], optional): Additional command line arguments for the srun call.
33
+ submission_template (str, optional): Template for queuing system job submission scripts.
34
+ """
35
+
36
+ cores: Optional[int] = None
37
+ threads_per_core: Optional[int] = None
38
+ gpus_per_core: Optional[int] = None
39
+ cwd: Optional[str] = None
40
+ cache_key: Optional[str] = None
41
+ cache_directory: Optional[str] = None
42
+ num_nodes: Optional[int] = None
43
+ exclusive: Optional[bool] = None
44
+ error_log_file: Optional[str] = None
45
+ run_time_max: Optional[int] = None
46
+ priority: Optional[int] = None
47
+ slurm_cmd_args: Optional[list[str]] = None
48
+ submission_template: Optional[str] = None
49
+
50
+ if HAS_PYDANTIC:
51
+
52
+ class Config:
53
+ extra = Extra.forbid
54
+
55
+
56
+ if not HAS_PYDANTIC:
57
+ ResourceDictValidation = dataclass(ResourceDictValidation) # type: ignore
58
+
59
+
60
+ def _get_accepted_keys(class_type) -> list[str]:
61
+ """
62
+ Return a list of accepted field names from a Pydantic model or dataclass.
63
+
64
+ Args:
65
+ class_type: A Pydantic BaseModel subclass or a dataclass.
66
+
67
+ Returns:
68
+ list[str]: Field names declared on the class.
69
+
70
+ Raises:
71
+ TypeError: If the class type is neither a Pydantic model nor a dataclass.
72
+ """
73
+ if hasattr(class_type, "model_fields"):
74
+ return list(class_type.model_fields.keys())
75
+ elif hasattr(class_type, "__dataclass_fields__"):
76
+ return list(class_type.__dataclass_fields__.keys())
77
+ raise TypeError("Unsupported class type for validation")
78
+
79
+
80
+ def validate_resource_dict(resource_dict: dict) -> None:
81
+ """
82
+ Validate a resource dictionary against the declared fields of ResourceDictValidation.
83
+
84
+ Args:
85
+ resource_dict (dict): Dictionary of resource requirements to validate. Unknown keys raise
86
+ a validation error when pydantic is available.
87
+
88
+ Raises:
89
+ ValidationError: If any key/value pair violates the ResourceDictValidation schema.
90
+ """
91
+ _ = ResourceDictValidation(**resource_dict)
92
+
93
+
94
+ def validate_resource_dict_with_optional_keys(resource_dict: dict) -> None:
95
+ """
96
+ Validate a resource dictionary, allowing unknown keys with a warning instead of an error.
97
+
98
+ Known keys are validated against ResourceDictValidation. Unknown keys are collected and
99
+ emitted as a UserWarning so callers can detect unsupported options without hard failure.
100
+
101
+ Args:
102
+ resource_dict (dict): Dictionary of resource requirements to validate.
103
+
104
+ Raises:
105
+ ValidationError: If any known key/value pair violates the ResourceDictValidation schema.
106
+ """
107
+ accepted_keys = _get_accepted_keys(class_type=ResourceDictValidation)
108
+ optional_lst = [key for key in resource_dict if key not in accepted_keys]
109
+ validate_dict = {
110
+ key: value for key, value in resource_dict.items() if key in accepted_keys
111
+ }
112
+ _ = ResourceDictValidation(**validate_dict)
113
+ if len(optional_lst) > 0:
114
+ warnings.warn(
115
+ f"The following keys are not recognized and cannot be validated: {optional_lst}",
116
+ stacklevel=2,
117
+ )