libpipe 0.5__tar.gz → 0.7__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.
- {libpipe-0.5 → libpipe-0.7}/PKG-INFO +13 -5
- {libpipe-0.5 → libpipe-0.7}/libpipe/attrmap.py +9 -16
- {libpipe-0.5 → libpipe-0.7}/libpipe/futils.py +11 -9
- {libpipe-0.5 → libpipe-0.7}/libpipe/settings.py +4 -6
- {libpipe-0.5 → libpipe-0.7}/libpipe/tools/nodetool.py +14 -10
- {libpipe-0.5 → libpipe-0.7}/libpipe/worker.py +220 -96
- libpipe-0.7/pyproject.toml +61 -0
- libpipe-0.5/libpipe/tests/settings_test.toml +0 -73
- libpipe-0.5/libpipe/tests/test_settings.py +0 -305
- libpipe-0.5/libpipe/tests/test_worker.py +0 -274
- libpipe-0.5/libpipe/tools/__init__.py +0 -0
- libpipe-0.5/pyproject.toml +0 -29
- {libpipe-0.5 → libpipe-0.7}/LICENSE +0 -0
- {libpipe-0.5 → libpipe-0.7}/README.md +0 -0
- {libpipe-0.5 → libpipe-0.7}/libpipe/__init__.py +0 -0
- {libpipe-0.5 → libpipe-0.7}/libpipe/msutils.py +0 -0
- {libpipe-0.5/libpipe/tests → libpipe-0.7/libpipe/tools}/__init__.py +0 -0
- {libpipe-0.5 → libpipe-0.7}/libpipe/tools/libnodes_bash.sh +0 -0
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: libpipe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7
|
|
4
4
|
Summary: Pipeline orchestration and node management utilities
|
|
5
|
-
License: GPL-3.0-or-later
|
|
5
|
+
License-Expression: GPL-3.0-or-later
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Florent Mertens
|
|
7
8
|
Author-email: florent.mertens@gmail.com
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Provides-Extra: dev
|
|
15
17
|
Requires-Dist: asyncssh (>=2.0)
|
|
16
18
|
Requires-Dist: click (>=8.0)
|
|
17
19
|
Requires-Dist: progressbar2 (>=3.40)
|
|
20
|
+
Requires-Dist: pytest (>=8.0) ; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov (>=5.0) ; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff (==0.16.1) ; extra == "dev"
|
|
18
23
|
Requires-Dist: toml (>=0.10)
|
|
24
|
+
Project-URL: Homepage, https://gitlab.com/flomertens/libpipe
|
|
25
|
+
Project-URL: Issues, https://gitlab.com/flomertens/libpipe/-/issues
|
|
26
|
+
Project-URL: Repository, https://gitlab.com/flomertens/libpipe
|
|
19
27
|
Description-Content-Type: text/markdown
|
|
20
28
|
|
|
21
29
|
Libpipe
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# Some parts inspired by the AttrDict package written by Brendan Curran-Johnson (MIT Licence)
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import collections
|
|
4
4
|
import copy
|
|
5
5
|
import functools
|
|
6
|
-
|
|
6
|
+
import re
|
|
7
7
|
from collections.abc import Mapping
|
|
8
|
-
import collections
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
def merge(left, right):
|
|
@@ -130,7 +129,7 @@ class AttrMap(Mapping):
|
|
|
130
129
|
# sequence type seems like more trouble than it is worth.
|
|
131
130
|
# If people want full serialization, they can pickle, and in
|
|
132
131
|
# 99% of cases, sequence_type won't change anyway
|
|
133
|
-
return "AttrMap({
|
|
132
|
+
return f"AttrMap({self._mapping!r})"
|
|
134
133
|
|
|
135
134
|
@classmethod
|
|
136
135
|
def _valid_name(cls, key):
|
|
@@ -158,9 +157,7 @@ class AttrMap(Mapping):
|
|
|
158
157
|
|
|
159
158
|
if key not in self or not self._valid_name(key):
|
|
160
159
|
raise AttributeError(
|
|
161
|
-
"'{
|
|
162
|
-
cls=self.__class__.__name__, name=key
|
|
163
|
-
)
|
|
160
|
+
f"'{self.__class__.__name__}' instance has no attribute '{key}'"
|
|
164
161
|
)
|
|
165
162
|
|
|
166
163
|
return self._build(self[key])
|
|
@@ -251,9 +248,7 @@ class AttrMap(Mapping):
|
|
|
251
248
|
Mapping.__setattr__(self, key, value)
|
|
252
249
|
else:
|
|
253
250
|
raise TypeError(
|
|
254
|
-
"'{
|
|
255
|
-
cls=self.__class__.__name__
|
|
256
|
-
)
|
|
251
|
+
f"'{self.__class__.__name__}' does not allow attribute creation."
|
|
257
252
|
)
|
|
258
253
|
|
|
259
254
|
def __delattr__(self, key, force=False): # pragma: no cover
|
|
@@ -267,9 +262,7 @@ class AttrMap(Mapping):
|
|
|
267
262
|
Mapping.__delattr__(self, key, force=force)
|
|
268
263
|
else:
|
|
269
264
|
raise TypeError(
|
|
270
|
-
"'{
|
|
271
|
-
cls=self.__class__.__name__
|
|
272
|
-
)
|
|
265
|
+
f"'{self.__class__.__name__}' does not allow attribute deletion."
|
|
273
266
|
)
|
|
274
267
|
|
|
275
268
|
def set_data(self, mapping):
|
|
@@ -301,7 +294,7 @@ class AttrMap(Mapping):
|
|
|
301
294
|
|
|
302
295
|
def get(self, key, default=None):
|
|
303
296
|
try:
|
|
304
|
-
return functools.reduce(lambda x, y: x[y], [self
|
|
297
|
+
return functools.reduce(lambda x, y: x[y], [self, *key.split('.')])
|
|
305
298
|
except KeyError:
|
|
306
299
|
return None
|
|
307
300
|
|
|
@@ -356,8 +349,8 @@ class OrderedSet(collections.abc.MutableSet):
|
|
|
356
349
|
|
|
357
350
|
def __repr__(self):
|
|
358
351
|
if not self:
|
|
359
|
-
return '
|
|
360
|
-
return '
|
|
352
|
+
return f'{self.__class__.__name__}()'
|
|
353
|
+
return f'{self.__class__.__name__}({list(self)!r})'
|
|
361
354
|
|
|
362
355
|
def __eq__(self, other):
|
|
363
356
|
if isinstance(other, OrderedSet):
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Author: F. Mertens
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
5
|
import os
|
|
7
|
-
import
|
|
6
|
+
import re
|
|
8
7
|
import shutil
|
|
8
|
+
import time
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def file_not_found_retry(fct, n_try, *args, **kargs):
|
|
@@ -28,7 +28,7 @@ def reglob(path, exp, invert=False):
|
|
|
28
28
|
else:
|
|
29
29
|
res = [f for f in os.listdir(path) if not m.search(f)]
|
|
30
30
|
|
|
31
|
-
return
|
|
31
|
+
return (f"{path}/{x}" for x in res)
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def rm_if_exist(directory, verbose=False):
|
|
@@ -54,21 +54,23 @@ def is_file(f):
|
|
|
54
54
|
def zip_copy(dir_ins, dir_outs, filename, filename_out=None, verbose=True, n_retry=1):
|
|
55
55
|
if not filename_out:
|
|
56
56
|
filename_out = filename
|
|
57
|
-
for dir_in, dir_out in zip(dir_ins, dir_outs):
|
|
57
|
+
for dir_in, dir_out in zip(dir_ins, dir_outs, strict=False):
|
|
58
58
|
if verbose:
|
|
59
59
|
print(f'Copy {dir_in}/{filename} -> {dir_out}/{filename_out}')
|
|
60
|
-
file_not_found_retry(shutil.copyfile, n_retry, f'{dir_in}/{filename}',
|
|
60
|
+
file_not_found_retry(shutil.copyfile, n_retry, f'{dir_in}/{filename}',
|
|
61
|
+
f'{dir_out}/{filename_out}')
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
def zip_copytree(dir_ins, dir_outs, dirname, verbose=True, n_retry=1):
|
|
64
|
-
for dir_in, dir_out in zip(dir_ins, dir_outs):
|
|
65
|
+
for dir_in, dir_out in zip(dir_ins, dir_outs, strict=False):
|
|
65
66
|
if verbose:
|
|
66
67
|
print(f'Copy {dir_in}/{dirname} -> {dir_out}/{dirname}')
|
|
67
|
-
file_not_found_retry(shutil.copytree, n_retry, f'{dir_in}/{dirname}',
|
|
68
|
+
file_not_found_retry(shutil.copytree, n_retry, f'{dir_in}/{dirname}',
|
|
69
|
+
f'{dir_out}/{dirname}')
|
|
68
70
|
|
|
69
71
|
|
|
70
72
|
def zip_rename(dir_ins, dir_outs, filename_or_dirname=None, verbose=True, n_retry=1):
|
|
71
|
-
for f_in, f_out in zip(dir_ins, dir_outs):
|
|
73
|
+
for f_in, f_out in zip(dir_ins, dir_outs, strict=False):
|
|
72
74
|
if filename_or_dirname is not None:
|
|
73
75
|
f_in = f'{f_in}/{filename_or_dirname}'
|
|
74
76
|
f_out = f'{f_out}/{filename_or_dirname}'
|
|
@@ -78,7 +80,7 @@ def zip_rename(dir_ins, dir_outs, filename_or_dirname=None, verbose=True, n_retr
|
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
def zip_rename_reg(dir_ins, dir_outs, regex, verbose=True, invert=False, n_retry=1):
|
|
81
|
-
for f_in, f_out in zip(dir_ins, dir_outs):
|
|
83
|
+
for f_in, f_out in zip(dir_ins, dir_outs, strict=False):
|
|
82
84
|
for f_in2 in reglob(f_in, regex, invert=invert):
|
|
83
85
|
f_out2 = f'{f_out}/{os.path.basename(f_in2)}'
|
|
84
86
|
if verbose:
|
|
@@ -71,11 +71,8 @@ class BaseSettings(attrmap.AttrMap):
|
|
|
71
71
|
else:
|
|
72
72
|
for k, v in c_file.items():
|
|
73
73
|
if isinstance(v, dict):
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
else:
|
|
77
|
-
t = k
|
|
78
|
-
if t not in c_default.keys():
|
|
74
|
+
t = v.get('type', k)
|
|
75
|
+
if t not in c_default:
|
|
79
76
|
print(f'Warning: no default for section {k} of type {t}')
|
|
80
77
|
continue
|
|
81
78
|
t_default = c_default[t]
|
|
@@ -120,7 +117,8 @@ class BaseSettings(attrmap.AttrMap):
|
|
|
120
117
|
with open(file, mode='w') as f:
|
|
121
118
|
toml.dump(dict(m), f)
|
|
122
119
|
|
|
123
|
-
def duplicate(self, out_file, copy_parset_files=False, strip_parents_keys=False,
|
|
120
|
+
def duplicate(self, out_file, copy_parset_files=False, strip_parents_keys=False,
|
|
121
|
+
**config_modifiers):
|
|
124
122
|
s_out = self.copy()
|
|
125
123
|
s_out.set_file(out_file)
|
|
126
124
|
s_out['default_settings'] = ''
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import json
|
|
4
|
+
import os
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
|
-
from libpipe import worker
|
|
9
|
-
from libpipe import __version__
|
|
10
|
-
|
|
8
|
+
from libpipe import __version__, worker
|
|
11
9
|
|
|
12
10
|
t_file = click.Path(exists=True, dir_okay=False)
|
|
13
11
|
|
|
@@ -21,7 +19,8 @@ def main():
|
|
|
21
19
|
@main.command('run_all')
|
|
22
20
|
@click.argument('cmd_list', type=t_file)
|
|
23
21
|
@click.option('--nodes_string', '-n', help='Range of nodes to use', type=str, default='localhost')
|
|
24
|
-
@click.option('--max_concurrent', '-m',
|
|
22
|
+
@click.option('--max_concurrent', '-m', type=int, default=5,
|
|
23
|
+
help='Maximum concurrent tasks on a node')
|
|
25
24
|
@click.option('--env_file', '-e', help='Environment source file', type=t_file)
|
|
26
25
|
@click.option('--dry_run', '-d', help='Dry run, only print commands', is_flag=True)
|
|
27
26
|
@click.option('--log_file', help='Log output to log_file', type=str, default=None)
|
|
@@ -31,7 +30,10 @@ def run_all(cmd_list, nodes_string, max_concurrent, env_file, dry_run, log_file)
|
|
|
31
30
|
env_file=env_file, dry_run=dry_run, debug=dry_run)
|
|
32
31
|
|
|
33
32
|
i_log_file = log_file
|
|
34
|
-
|
|
33
|
+
with open(cmd_list) as f:
|
|
34
|
+
cmds = f.readlines()
|
|
35
|
+
|
|
36
|
+
for i, cmd in enumerate(cmds):
|
|
35
37
|
if log_file is not None:
|
|
36
38
|
i_log_file = log_file + f'.{i + 1}'
|
|
37
39
|
pool.add(cmd.strip(), output_file=i_log_file)
|
|
@@ -64,7 +66,7 @@ def run_on_all_nodes(cmd, nodes_string, env_file, dry_run):
|
|
|
64
66
|
@click.option('--log_file', '-l', help='Log output to log_file', type=str, default=None)
|
|
65
67
|
def init_job(name, cmd, target_node, log_file):
|
|
66
68
|
''' Add a job to a job list file NAME. If job file does not exist, create one. '''
|
|
67
|
-
if os.path.exists(name):
|
|
69
|
+
if os.path.exists(name):
|
|
68
70
|
with open(name) as json_file:
|
|
69
71
|
jobs = json.load(json_file)
|
|
70
72
|
else:
|
|
@@ -78,9 +80,11 @@ def init_job(name, cmd, target_node, log_file):
|
|
|
78
80
|
|
|
79
81
|
@main.command('run_joblist')
|
|
80
82
|
@click.argument('name', type=t_file)
|
|
81
|
-
@click.option('--nodes_string', '-n',
|
|
83
|
+
@click.option('--nodes_string', '-n',
|
|
84
|
+
help='Range of nodes to use. By default use the target nodes in the joblist',
|
|
82
85
|
type=str, default='joblist')
|
|
83
|
-
@click.option('--max_concurrent', '-m',
|
|
86
|
+
@click.option('--max_concurrent', '-m', type=int, default=5,
|
|
87
|
+
help='Maximum concurrent tasks on a node')
|
|
84
88
|
@click.option('--env_file', '-e', help='Environment source file', type=t_file)
|
|
85
89
|
@click.option('--dry_run', '-d', help='Dry run, only print commands', is_flag=True)
|
|
86
90
|
@click.option('--worker_name', '-w', help='Name of the worker', type=str, default='Run joblist')
|
|
@@ -90,7 +94,7 @@ def run_joblist(name, nodes_string, max_concurrent, env_file, dry_run, worker_na
|
|
|
90
94
|
jobs = json.load(json_file)
|
|
91
95
|
|
|
92
96
|
if nodes_string == 'joblist':
|
|
93
|
-
all_nodes =
|
|
97
|
+
all_nodes = {job['target_node'] for job in jobs['jobs'] if job['target_node'] is not None}
|
|
94
98
|
if len(all_nodes) == 0:
|
|
95
99
|
all_nodes = ['localhost']
|
|
96
100
|
else:
|