cluster_tools 3.2.4__tar.gz → 3.4.0__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 (41) hide show
  1. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/PKG-INFO +1 -1
  2. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/__init__.py +31 -0
  3. cluster_tools-3.4.0/cluster_tools/executors/batching.py +177 -0
  4. cluster_tools-3.4.0/cluster_tools/version.py +1 -0
  5. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools.egg-info/PKG-INFO +1 -1
  6. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools.egg-info/SOURCES.txt +2 -0
  7. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/pyproject.toml +1 -1
  8. cluster_tools-3.4.0/tests/test_batching.py +200 -0
  9. cluster_tools-3.2.4/cluster_tools/version.py +0 -1
  10. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/LICENSE +0 -0
  11. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/README.md +0 -0
  12. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/call.py +0 -0
  13. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/file_wait_thread.py +0 -0
  14. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/multiprocessing_logging_handler.py +0 -0
  15. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/pickling.py +0 -0
  16. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/reflection.py +0 -0
  17. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/string_.py +0 -0
  18. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/tailf.py +0 -0
  19. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/_utils/warning.py +0 -0
  20. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/executor_protocol.py +0 -0
  21. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/executors/dask.py +0 -0
  22. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/executors/multiprocessing_.py +0 -0
  23. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/executors/multiprocessing_pickle.py +0 -0
  24. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/executors/sequential.py +0 -0
  25. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/executors/sequential_pickle.py +0 -0
  26. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/remote.py +0 -0
  27. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/schedulers/__init__.py +0 -0
  28. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/schedulers/cluster_executor.py +0 -0
  29. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/schedulers/kube.py +0 -0
  30. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/schedulers/pbs.py +0 -0
  31. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools/schedulers/slurm.py +0 -0
  32. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools.egg-info/dependency_links.txt +0 -0
  33. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools.egg-info/requires.txt +0 -0
  34. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/cluster_tools.egg-info/top_level.txt +0 -0
  35. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/setup.cfg +0 -0
  36. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/tests/test_all.py +0 -0
  37. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/tests/test_dask.py +0 -0
  38. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/tests/test_deref_main.py +0 -0
  39. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/tests/test_kubernetes.py +0 -0
  40. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/tests/test_multiprocessing.py +0 -0
  41. {cluster_tools-3.2.4 → cluster_tools-3.4.0}/tests/test_slurm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluster_tools
3
- Version: 3.2.4
3
+ Version: 3.4.0
4
4
  Summary: Utility library for easily distributing code execution on clusters
5
5
  Author-email: scalable minds <hello@scalableminds.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Literal, overload
2
2
 
3
3
  from cluster_tools.executor_protocol import Executor
4
+ from cluster_tools.executors.batching import BatchingExecutor
4
5
  from cluster_tools.executors.dask import DaskExecutor
5
6
  from cluster_tools.executors.multiprocessing_ import MultiprocessingExecutor
6
7
  from cluster_tools.executors.multiprocessing_pickle import MultiprocessingPickleExecutor
@@ -86,6 +87,12 @@ def get_executor(
86
87
  ) -> SequentialExecutor: ...
87
88
 
88
89
 
90
+ @overload
91
+ def get_executor(
92
+ environment: Literal["batching"], **kwargs: Any
93
+ ) -> BatchingExecutor: ...
94
+
95
+
89
96
  @overload
90
97
  def get_executor(
91
98
  environment: Literal["sequential_with_pickling"], **kwargs: Any
@@ -113,6 +120,30 @@ def get_executor(environment: str, **kwargs: Any) -> "Executor":
113
120
  return MultiprocessingExecutor(**kwargs)
114
121
  elif environment == "sequential":
115
122
  return SequentialExecutor(**kwargs)
123
+ elif environment == "batching":
124
+ executor_config = kwargs.get("executor")
125
+ if not isinstance(executor_config, dict):
126
+ raise ValueError(
127
+ "The 'batching' executor requires a nested 'executor' config."
128
+ )
129
+
130
+ inner_executor_config = executor_config.copy()
131
+ name = inner_executor_config.pop("name", None)
132
+ if name is None:
133
+ raise ValueError(
134
+ "The 'batching' executor's nested 'executor' config requires a 'name' key."
135
+ )
136
+
137
+ inner_executor = get_executor(name, **inner_executor_config)
138
+ batch_size = kwargs.get("batch_size")
139
+ target_job_count = kwargs.get("target_job_count")
140
+ if batch_size is None and target_job_count is None:
141
+ raise ValueError(
142
+ "The 'batching' executor requires either a 'batch_size' or 'target_job_count' key."
143
+ )
144
+ return BatchingExecutor(
145
+ inner_executor, batch_size=batch_size, target_job_count=target_job_count
146
+ )
116
147
  elif environment == "sequential_with_pickling":
117
148
  return SequentialPickleExecutor(**kwargs)
118
149
  elif environment == "multiprocessing_with_pickling":
@@ -0,0 +1,177 @@
1
+ import math
2
+ import warnings
3
+ from collections.abc import Callable, Iterable, Iterator
4
+ from concurrent.futures import Future, as_completed
5
+ from functools import partial
6
+ from itertools import islice
7
+ from os import PathLike
8
+ from types import TracebackType
9
+ from typing import TypeVar
10
+
11
+ from typing_extensions import ParamSpec
12
+
13
+ from cluster_tools.executor_protocol import Executor
14
+
15
+ _T = TypeVar("_T")
16
+ _S = TypeVar("_S")
17
+ _P = ParamSpec("_P")
18
+
19
+
20
+ def _apply_fn_to_batch(fn: Callable, batch: list) -> list:
21
+ return [fn(item) for item in batch]
22
+
23
+
24
+ def _iter_batches(iterable: Iterable[_S], batch_size: int) -> Iterator[list[_S]]:
25
+ it = iter(iterable)
26
+ while batch := list(islice(it, batch_size)):
27
+ yield batch
28
+
29
+
30
+ class BatchingExecutor:
31
+ """
32
+ Wraps another executor and groups items into batches before submission.
33
+
34
+ Specify exactly one of:
35
+ - batch_size: fixed number of items per job (must be > 0)
36
+ - target_job_count: desired number of jobs; batch size is computed as
37
+ ceil(n_items / target_job_count) for each map call (must be > 0)
38
+ """
39
+
40
+ _executor: Executor
41
+ _batch_size: int | None
42
+ _target_job_count: int | None
43
+
44
+ def __init__(
45
+ self,
46
+ executor: Executor,
47
+ *,
48
+ batch_size: int | None = None,
49
+ target_job_count: int | None = None,
50
+ ) -> None:
51
+ if batch_size is not None and target_job_count is not None:
52
+ raise ValueError("Specify either batch_size or target_job_count, not both")
53
+ if batch_size is None and target_job_count is None:
54
+ raise ValueError("Either batch_size or target_job_count must be specified")
55
+ if batch_size is not None and batch_size <= 0:
56
+ raise ValueError("batch_size must be greater than 0")
57
+ if target_job_count is not None and target_job_count <= 0:
58
+ raise ValueError("target_job_count must be greater than 0")
59
+ self._executor = executor
60
+ self._batch_size = batch_size
61
+ self._target_job_count = target_job_count
62
+
63
+ @property
64
+ def batch_size(self) -> int | None:
65
+ return self._batch_size
66
+
67
+ @property
68
+ def target_job_count(self) -> int | None:
69
+ return self._target_job_count
70
+
71
+ def _resolve_batch_size(self, n_items: int) -> int:
72
+ if self._batch_size is not None:
73
+ return self._batch_size
74
+ assert self._target_job_count is not None
75
+ if n_items == 0:
76
+ return 1
77
+ return math.ceil(n_items / self._target_job_count)
78
+
79
+ def __enter__(self) -> "BatchingExecutor":
80
+ self._executor.__enter__()
81
+ return self
82
+
83
+ def __exit__(
84
+ self,
85
+ exc_type: type[BaseException] | None,
86
+ exc_val: BaseException | None,
87
+ exc_tb: TracebackType | None,
88
+ ) -> None:
89
+ self._executor.__exit__(exc_type, exc_val, exc_tb)
90
+
91
+ @classmethod
92
+ def as_completed(cls, futures: list[Future[_T]]) -> Iterator[Future[_T]]:
93
+ return as_completed(futures)
94
+
95
+ def submit(
96
+ self,
97
+ __fn: Callable[_P, _T],
98
+ /,
99
+ *args: _P.args,
100
+ **kwargs: _P.kwargs,
101
+ ) -> Future[_T]:
102
+ warnings.warn(
103
+ "BatchingExecutor.submit() bypasses batching and submits a single job directly.",
104
+ stacklevel=2,
105
+ )
106
+ return self._executor.submit(__fn, *args, **kwargs)
107
+
108
+ def map_to_futures(
109
+ self,
110
+ fn: Callable[[_S], _T],
111
+ args: Iterable[_S],
112
+ output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
113
+ ) -> list[Future[_T]]:
114
+ if output_pickle_path_getter is not None:
115
+ raise NotImplementedError(
116
+ "BatchingExecutor does not support output_pickle_path_getter"
117
+ )
118
+
119
+ items = list(args)
120
+ batch_size = self._resolve_batch_size(len(items))
121
+ all_item_futures: list[Future[_T]] = []
122
+
123
+ for batch in _iter_batches(items, batch_size):
124
+ batch_fn = partial(_apply_fn_to_batch, fn)
125
+ batch_fn.__name__ = f"batch_{fn.__name__}" # type: ignore[attr-defined]
126
+ (batch_future,) = self._executor.map_to_futures(batch_fn, [batch])
127
+ item_futures: list[Future[_T]] = [Future() for _ in batch]
128
+ all_item_futures.extend(item_futures)
129
+
130
+ def on_batch_done(
131
+ batch_future: Future[list[_T]],
132
+ _item_futures: list[Future[_T]] = item_futures,
133
+ ) -> None:
134
+ try:
135
+ results = batch_future.result()
136
+ for future, result in zip(_item_futures, results):
137
+ future.set_result(result)
138
+ except Exception as e:
139
+ for future in _item_futures:
140
+ future.set_exception(e)
141
+
142
+ batch_future.add_done_callback(on_batch_done)
143
+
144
+ return all_item_futures
145
+
146
+ def map(
147
+ self,
148
+ fn: Callable[[_S], _T],
149
+ iterables: Iterable[_S],
150
+ timeout: float | None = None,
151
+ chunksize: int | None = None,
152
+ ) -> Iterator[_T]:
153
+ if chunksize is not None:
154
+ raise ValueError(
155
+ "BatchingExecutor does not support chunksize. Use batch_size instead."
156
+ )
157
+
158
+ items = list(iterables)
159
+ batch_size = self._resolve_batch_size(len(items))
160
+
161
+ def result_generator() -> Iterator[_T]:
162
+ batch_fn = partial(_apply_fn_to_batch, fn)
163
+ batch_fn.__name__ = f"batch_{fn.__name__}" # type: ignore[attr-defined]
164
+ for batch_results in self._executor.map(
165
+ batch_fn,
166
+ _iter_batches(items, batch_size),
167
+ timeout=timeout,
168
+ ):
169
+ yield from batch_results
170
+
171
+ return result_generator()
172
+
173
+ def forward_log(self, fut: Future[_T]) -> _T:
174
+ return self._executor.forward_log(fut)
175
+
176
+ def shutdown(self, wait: bool = True, *, cancel_futures: bool = False) -> None:
177
+ self._executor.shutdown(wait=wait, cancel_futures=cancel_futures)
@@ -0,0 +1 @@
1
+ __version__ = '3.4.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluster_tools
3
- Version: 3.2.4
3
+ Version: 3.4.0
4
4
  Summary: Utility library for easily distributing code execution on clusters
5
5
  Author-email: scalable minds <hello@scalableminds.com>
6
6
  License-Expression: MIT
@@ -18,6 +18,7 @@ cluster_tools/_utils/reflection.py
18
18
  cluster_tools/_utils/string_.py
19
19
  cluster_tools/_utils/tailf.py
20
20
  cluster_tools/_utils/warning.py
21
+ cluster_tools/executors/batching.py
21
22
  cluster_tools/executors/dask.py
22
23
  cluster_tools/executors/multiprocessing_.py
23
24
  cluster_tools/executors/multiprocessing_pickle.py
@@ -29,6 +30,7 @@ cluster_tools/schedulers/kube.py
29
30
  cluster_tools/schedulers/pbs.py
30
31
  cluster_tools/schedulers/slurm.py
31
32
  tests/test_all.py
33
+ tests/test_batching.py
32
34
  tests/test_dask.py
33
35
  tests/test_deref_main.py
34
36
  tests/test_kubernetes.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cluster_tools"
3
- version = "3.2.4" # filled by dunamai
3
+ version = "3.4.0" # filled by dunamai
4
4
  description = "Utility library for easily distributing code execution on clusters"
5
5
  authors = [{name= "scalable minds", email="hello@scalableminds.com"}]
6
6
  readme = "README.md"
@@ -0,0 +1,200 @@
1
+ from concurrent.futures import CancelledError, Future
2
+ from pathlib import Path
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ import cluster_tools
8
+ from cluster_tools.executors.batching import BatchingExecutor
9
+ from cluster_tools.executors.multiprocessing_ import MultiprocessingExecutor
10
+ from cluster_tools.executors.sequential import SequentialExecutor
11
+
12
+
13
+ def double(x: int) -> int:
14
+ return x * 2
15
+
16
+
17
+ def raise_runtime_error(_: int) -> int:
18
+ raise RuntimeError("failure")
19
+
20
+
21
+ @pytest.fixture(params=["sequential", "multiprocessing"])
22
+ def inner_executor(request: pytest.FixtureRequest) -> cluster_tools.Executor:
23
+ if request.param == "sequential":
24
+ return SequentialExecutor()
25
+ return MultiprocessingExecutor(max_workers=2)
26
+
27
+
28
+ def test_map_basic(inner_executor: cluster_tools.Executor) -> None:
29
+ with BatchingExecutor(inner_executor, batch_size=3) as executor:
30
+ results = list(executor.map(double, [1, 2, 3, 4, 5]))
31
+ assert results == [2, 4, 6, 8, 10]
32
+
33
+
34
+ def test_map_exact_batch_boundary() -> None:
35
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
36
+ results = list(executor.map(double, [1, 2, 3]))
37
+ assert results == [2, 4, 6]
38
+
39
+
40
+ def test_map_single_item() -> None:
41
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
42
+ results = list(executor.map(double, [5]))
43
+ assert results == [10]
44
+
45
+
46
+ def test_map_empty() -> None:
47
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
48
+ results = list(executor.map(double, []))
49
+ assert results == []
50
+
51
+
52
+ def test_map_calls_inner_with_batches() -> None:
53
+ """Verify the inner executor receives batches, not individual items."""
54
+ inner = SequentialExecutor()
55
+ received_batches: list[list[int]] = []
56
+ original_map = inner.map
57
+
58
+ def spy(fn, iterables, **kwargs): # type: ignore[no-untyped-def]
59
+ batches = list(iterables)
60
+ received_batches.extend(batches)
61
+ return original_map(fn, iter(batches), **kwargs)
62
+
63
+ with patch.object(inner, "map", side_effect=spy):
64
+ with BatchingExecutor(inner, batch_size=3) as executor:
65
+ results = list(executor.map(double, range(7)))
66
+
67
+ assert results == [0, 2, 4, 6, 8, 10, 12]
68
+ assert received_batches == [[0, 1, 2], [3, 4, 5], [6]]
69
+
70
+
71
+ def test_map_to_futures(inner_executor: cluster_tools.Executor) -> None:
72
+ with BatchingExecutor(inner_executor, batch_size=3) as executor:
73
+ futures = executor.map_to_futures(double, [1, 2, 3, 4, 5])
74
+ assert len(futures) == 5
75
+ assert [f.result() for f in futures] == [2, 4, 6, 8, 10]
76
+
77
+
78
+ def test_map_to_futures_empty() -> None:
79
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
80
+ futures = executor.map_to_futures(double, [])
81
+ assert futures == []
82
+
83
+
84
+ def test_map_chunksize_raises() -> None:
85
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
86
+ with pytest.raises(ValueError):
87
+ list(executor.map(double, [1, 2, 3], chunksize=2))
88
+
89
+
90
+ def test_map_to_futures_output_pickle_path_getter_raises() -> None:
91
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
92
+ with pytest.raises(NotImplementedError):
93
+ executor.map_to_futures(
94
+ double, [1], output_pickle_path_getter=lambda _: Path("/tmp/x")
95
+ )
96
+
97
+
98
+ def test_map_to_futures_exception_propagates_to_all_item_futures() -> None:
99
+ # SequentialExecutor raises synchronously, so use MultiprocessingExecutor
100
+ # to exercise the on_batch_done exception path (failure arrives via future).
101
+ with BatchingExecutor(
102
+ MultiprocessingExecutor(max_workers=1), batch_size=3
103
+ ) as executor:
104
+ futures = executor.map_to_futures(raise_runtime_error, [1, 2, 3])
105
+
106
+ assert len(futures) == 3
107
+ for fut in futures:
108
+ with pytest.raises(RuntimeError, match="failure"):
109
+ fut.result()
110
+
111
+
112
+ def test_submit_warns() -> None:
113
+ with BatchingExecutor(SequentialExecutor(), batch_size=3) as executor:
114
+ with pytest.warns(UserWarning, match="bypasses batching"):
115
+ fut = executor.submit(double, 5)
116
+ assert fut.result() == 10
117
+
118
+
119
+ def test_init_requires_one_of_batch_size_or_target_job_count() -> None:
120
+ with pytest.raises(ValueError, match="Either batch_size or target_job_count"):
121
+ BatchingExecutor(SequentialExecutor())
122
+
123
+
124
+ def test_init_rejects_both() -> None:
125
+ with pytest.raises(ValueError, match="not both"):
126
+ BatchingExecutor(SequentialExecutor(), batch_size=3, target_job_count=2)
127
+
128
+
129
+ def test_init_rejects_zero_batch_size() -> None:
130
+ with pytest.raises(ValueError, match="batch_size must be greater than 0"):
131
+ BatchingExecutor(SequentialExecutor(), batch_size=0)
132
+
133
+
134
+ def test_init_rejects_zero_target_job_count() -> None:
135
+ with pytest.raises(ValueError, match="target_job_count must be greater than 0"):
136
+ BatchingExecutor(SequentialExecutor(), target_job_count=0)
137
+
138
+
139
+ def test_target_job_count_map() -> None:
140
+ """With target_job_count=2 and 7 items, batch_size=ceil(7/2)=4 → 2 jobs."""
141
+ inner = SequentialExecutor()
142
+ received_batches: list[list[int]] = []
143
+ original_map = inner.map
144
+
145
+ def spy(fn, iterables, **kwargs): # type: ignore[no-untyped-def]
146
+ batches = list(iterables)
147
+ received_batches.extend(batches)
148
+ return original_map(fn, iter(batches), **kwargs)
149
+
150
+ with patch.object(inner, "map", side_effect=spy):
151
+ with BatchingExecutor(inner, target_job_count=2) as executor:
152
+ results = list(executor.map(double, range(7)))
153
+
154
+ assert results == [0, 2, 4, 6, 8, 10, 12]
155
+ assert received_batches == [[0, 1, 2, 3], [4, 5, 6]]
156
+
157
+
158
+ def test_target_job_count_map_to_futures() -> None:
159
+ with BatchingExecutor(SequentialExecutor(), target_job_count=2) as executor:
160
+ futures = executor.map_to_futures(double, [1, 2, 3, 4, 5])
161
+ assert [f.result() for f in futures] == [2, 4, 6, 8, 10]
162
+
163
+
164
+ def test_target_job_count_empty() -> None:
165
+ with BatchingExecutor(SequentialExecutor(), target_job_count=3) as executor:
166
+ results = list(executor.map(double, []))
167
+ assert results == []
168
+
169
+
170
+ def test_get_executor() -> None:
171
+ executor = cluster_tools.get_executor(
172
+ "batching",
173
+ executor={
174
+ "name": "multiprocessing",
175
+ "max_workers": 3,
176
+ },
177
+ batch_size=10,
178
+ )
179
+ assert isinstance(executor, BatchingExecutor)
180
+ assert isinstance(executor._executor, MultiprocessingExecutor)
181
+ assert executor._executor._max_workers == 3 # type: ignore[attr-defined]
182
+ assert executor.batch_size == 10
183
+
184
+
185
+ def test_get_executor_target_job_count() -> None:
186
+ executor = cluster_tools.get_executor(
187
+ "batching",
188
+ executor={"name": "sequential"},
189
+ target_job_count=4,
190
+ )
191
+ assert isinstance(executor, BatchingExecutor)
192
+ assert executor.target_job_count == 4
193
+
194
+
195
+ def test_get_executor_missing_sizing_raises() -> None:
196
+ with pytest.raises(ValueError, match="batch_size.*target_job_count"):
197
+ cluster_tools.get_executor(
198
+ "batching",
199
+ executor={"name": "sequential"},
200
+ )
@@ -1 +0,0 @@
1
- __version__ = '3.2.4'
File without changes
File without changes
File without changes