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.
@@ -1,21 +1,29 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: libpipe
3
- Version: 0.5
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 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: