libpipe 0.6__tar.gz → 0.8__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.8/PKG-INFO ADDED
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.3
2
+ Name: libpipe
3
+ Version: 0.8
4
+ Summary: Pipeline orchestration and node management utilities
5
+ License: GPL-3.0-or-later
6
+ Author: Florent Mertens
7
+ Author-email: florent.mertens@gmail.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Provides-Extra: dev
16
+ Requires-Dist: asyncssh (>=2.0)
17
+ Requires-Dist: click (>=8.0)
18
+ Requires-Dist: progressbar2 (>=3.40)
19
+ Requires-Dist: pytest (>=8.0) ; extra == "dev"
20
+ Requires-Dist: pytest-cov (>=5.0) ; extra == "dev"
21
+ Requires-Dist: ruff (==0.16.1) ; extra == "dev"
22
+ Requires-Dist: toml (>=0.10)
23
+ Project-URL: Homepage, https://gitlab.com/flomertens/libpipe
24
+ Project-URL: Issues, https://gitlab.com/flomertens/libpipe/-/issues
25
+ Project-URL: Repository, https://gitlab.com/flomertens/libpipe
26
+ Description-Content-Type: text/markdown
27
+
28
+ Libpipe
29
+ =======
30
+
31
+ Common pipeline framework and library
32
+
33
+ Installation
34
+ ------------
35
+
36
+ libpipe can be installed via pip:
37
+
38
+ $ pip install libpipe
39
+
40
+ and requires Python 3.10 or higher.
41
+
42
+ Worker pools on a Slurm cluster
43
+ -------------------------------
44
+
45
+ A `WorkerPool` runs its tasks locally, on other hosts over SSH, or through Slurm with the
46
+ host `slurm`: each task is then started with `srun`, as a job of its own when the pool runs
47
+ on a login node, as a job step inside an allocation (`salloc`, `sbatch`).
48
+
49
+ pool = worker.WorkerPool(['slurm'], name='Imaging', max_tasks_per_worker=8,
50
+ slurm_options='-p regular --mem=200G --time=24:00:00')
51
+
52
+ `max_tasks_per_worker` is the number of jobs in flight. The srun options (partition,
53
+ account, resources) come from `slurm_options`, or from the `LIBPIPE_SLURM_OPTIONS`
54
+ environment variable; `max_time` becomes the job time limit when the options set none.
55
+ Host constraints (`run_on_host`) do not apply: Slurm places the tasks, so the data must be
56
+ on a file system shared with the compute nodes. Give the memory (`--mem`) in the options:
57
+ inside an allocation, a step without it can claim all the memory of its node.
58
+
libpipe-0.8/README.md ADDED
@@ -0,0 +1,30 @@
1
+ Libpipe
2
+ =======
3
+
4
+ Common pipeline framework and library
5
+
6
+ Installation
7
+ ------------
8
+
9
+ libpipe can be installed via pip:
10
+
11
+ $ pip install libpipe
12
+
13
+ and requires Python 3.10 or higher.
14
+
15
+ Worker pools on a Slurm cluster
16
+ -------------------------------
17
+
18
+ A `WorkerPool` runs its tasks locally, on other hosts over SSH, or through Slurm with the
19
+ host `slurm`: each task is then started with `srun`, as a job of its own when the pool runs
20
+ on a login node, as a job step inside an allocation (`salloc`, `sbatch`).
21
+
22
+ pool = worker.WorkerPool(['slurm'], name='Imaging', max_tasks_per_worker=8,
23
+ slurm_options='-p regular --mem=200G --time=24:00:00')
24
+
25
+ `max_tasks_per_worker` is the number of jobs in flight. The srun options (partition,
26
+ account, resources) come from `slurm_options`, or from the `LIBPIPE_SLURM_OPTIONS`
27
+ environment variable; `max_time` becomes the job time limit when the options set none.
28
+ Host constraints (`run_on_host`) do not apply: Slurm places the tasks, so the data must be
29
+ on a file system shared with the compute nodes. Give the memory (`--mem`) in the options:
30
+ inside an allocation, a step without it can claim all the memory of its node.
@@ -1,11 +1,10 @@
1
1
  # Some parts inspired by the AttrDict package written by Brendan Curran-Johnson (MIT Licence)
2
2
 
3
- import re
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({mapping})".format(mapping=repr(self._mapping))
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
- "'{cls}' instance has no attribute '{name}'".format(
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
- "'{cls}' does not allow attribute creation.".format(
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
- "'{cls}' does not allow attribute deletion.".format(
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] + key.split('.'))
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 '%s()' % (self.__class__.__name__,)
360
- return '%s(%r)' % (self.__class__.__name__, list(self))
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 time
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 map(lambda x: "%s/%s" % (path, x, ), res)
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}', f'{dir_out}/{filename_out}')
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}', f'{dir_out}/{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
- if 'type' in v.keys():
75
- t = v['type']
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, **config_modifiers):
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', help='Maximum concurrent tasks on a node', type=int, default=5)
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
- for i, cmd in enumerate(open(cmd_list).readlines()):
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', help='Range of nodes to use. By default use the target nodes in the joblist',
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', help='Maximum concurrent tasks on a node', type=int, default=5)
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 = set([job['target_node'] for job in jobs['jobs'] if job['target_node'] is not None])
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: