oh-my-batch 0.1.1__tar.gz → 0.2.1__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: oh-my-batch
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary:
5
5
  License: GPL
6
6
  Author: weihong.xu
@@ -22,19 +22,33 @@ Description-Content-Type: text/markdown
22
22
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/oh-my-batch)](https://pypi.org/project/oh-my-batch/)
23
23
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/oh-my-batch)](https://pypi.org/project/oh-my-batch/)
24
24
 
25
- A simple tool to manipulate batch tasks designed for scientific computing community.
25
+ A toolkit to manipulate batch tasks with command line. Designed for scientific computing community.
26
26
 
27
27
  ## Features
28
28
  * `omb combo`: generate folders/files from different combinations of parameters
29
29
  * `omb batch`: generate batch scripts from multiple working directories
30
30
  * `omb job`: track the state of job in job schedular
31
31
 
32
+ ## Shell Tips
33
+ `oh-my-batch` is intended to help you implement computational workflows with shell scripts.
34
+ To make the best use of `oh-my-batch`, you need to know some shell tips.
35
+
36
+ * [Retry commands until success in shell script](https://stackoverflow.com/a/79191004/3099733)
37
+
32
38
  ## Install
33
39
  ```bash
34
40
  pip install oh-my-batch
35
41
  ```
36
42
 
37
43
  ## Use cases
44
+ ### Load useful functions to your shell script
45
+ You can load useful functions from `oh-my-batch` this way:
46
+
47
+ ```bash
48
+ omb misc export-shell-func > omb-func.sh && source omb-func.sh
49
+ ```
50
+
51
+ This will load some useful functions to your shell script, for example, `checkpoint` function.
38
52
 
39
53
  ### Generate files from different combinations of parameters
40
54
 
@@ -99,9 +113,9 @@ cat > tmp/lammps_header.sh <<EOF
99
113
  EOF
100
114
 
101
115
  omb batch \
102
- add_work_dir tmp/tasks/* - \
103
- add_header_file tmp/lammps_header.sh - \
104
- add_command "checkpoint lmp.done ./run.sh" - \
116
+ add_work_dirs tmp/tasks/* - \
117
+ add_header_files tmp/lammps_header.sh - \
118
+ add_cmds "checkpoint lmp.done ./run.sh" - \
105
119
  make tmp/lmp-{i}.slurm --concurrency 2
106
120
  ```
107
121
 
@@ -4,19 +4,33 @@
4
4
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/oh-my-batch)](https://pypi.org/project/oh-my-batch/)
5
5
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/oh-my-batch)](https://pypi.org/project/oh-my-batch/)
6
6
 
7
- A simple tool to manipulate batch tasks designed for scientific computing community.
7
+ A toolkit to manipulate batch tasks with command line. Designed for scientific computing community.
8
8
 
9
9
  ## Features
10
10
  * `omb combo`: generate folders/files from different combinations of parameters
11
11
  * `omb batch`: generate batch scripts from multiple working directories
12
12
  * `omb job`: track the state of job in job schedular
13
13
 
14
+ ## Shell Tips
15
+ `oh-my-batch` is intended to help you implement computational workflows with shell scripts.
16
+ To make the best use of `oh-my-batch`, you need to know some shell tips.
17
+
18
+ * [Retry commands until success in shell script](https://stackoverflow.com/a/79191004/3099733)
19
+
14
20
  ## Install
15
21
  ```bash
16
22
  pip install oh-my-batch
17
23
  ```
18
24
 
19
25
  ## Use cases
26
+ ### Load useful functions to your shell script
27
+ You can load useful functions from `oh-my-batch` this way:
28
+
29
+ ```bash
30
+ omb misc export-shell-func > omb-func.sh && source omb-func.sh
31
+ ```
32
+
33
+ This will load some useful functions to your shell script, for example, `checkpoint` function.
20
34
 
21
35
  ### Generate files from different combinations of parameters
22
36
 
@@ -81,9 +95,9 @@ cat > tmp/lammps_header.sh <<EOF
81
95
  EOF
82
96
 
83
97
  omb batch \
84
- add_work_dir tmp/tasks/* - \
85
- add_header_file tmp/lammps_header.sh - \
86
- add_command "checkpoint lmp.done ./run.sh" - \
98
+ add_work_dirs tmp/tasks/* - \
99
+ add_header_files tmp/lammps_header.sh - \
100
+ add_cmds "checkpoint lmp.done ./run.sh" - \
87
101
  make tmp/lmp-{i}.slurm --concurrency 2
88
102
  ```
89
103
 
@@ -1,6 +1,7 @@
1
+ GREEN='\033[0;32m'
2
+ NC='\033[0m'
1
3
 
2
4
  checkpoint() {
3
- # Usage: checkpoint <flag_file> <command> [arg1] [arg2] ...
4
5
  local flag_file="$1"
5
6
  shift # Remove the first argument so $@ contains only the command and its arguments
6
7
  if [ -f "$flag_file" ]; then
@@ -10,6 +11,7 @@ checkpoint() {
10
11
  local exit_code=$?
11
12
  if [ $exit_code -eq 0 ]; then
12
13
  local current_time=$(date '+%Y-%m-%d %H:%M:%S')
14
+ mkdir -p "$(dirname "$flag_file")"
13
15
  printf 'Command succeeded at %s\n' "$current_time" > "$flag_file"
14
16
  echo "Created flag file '$flag_file' with timestamp: $current_time"
15
17
  else
@@ -18,3 +20,16 @@ checkpoint() {
18
20
  fi
19
21
  fi
20
22
  }
23
+
24
+ echo -e "${GREEN}Functiion: checkpoint${NC}"
25
+ cat <<EOF
26
+ Usage:
27
+ checkpoint <flag_file> <command> [arg1] [arg2] ...
28
+
29
+ Set a flag file to indicate the completion of a command,
30
+ so that it will not be executed again next time.
31
+
32
+ Example:
33
+ checkpoint lmp.done lmp -in in.lmp
34
+
35
+ EOF
@@ -13,7 +13,7 @@ class BatchMaker:
13
13
  self._script_bottom = []
14
14
  self._command = []
15
15
 
16
- def add_work_dir(self, *dir: str):
16
+ def add_work_dirs(self, *dir: str):
17
17
  """
18
18
  Add working directories
19
19
 
@@ -22,39 +22,55 @@ class BatchMaker:
22
22
  self._work_dirs.extend(expand_globs(dir))
23
23
  return self
24
24
 
25
- def add_header_file(self, file: str, encoding='utf-8'):
25
+ def add_header_files(self, *file: str, encoding='utf-8'):
26
26
  """
27
27
  Add script header from files
28
28
 
29
29
  :param file: File path
30
30
  :param encoding: File encoding
31
31
  """
32
- with open(file, 'r', encoding=encoding) as f:
33
- self._script_header.append(f.read())
32
+ self._script_header.extend(load_files(*file, encoding=encoding))
34
33
  return self
35
34
 
36
- def add_bottom_file(self, file: str, encoding='utf-8'):
35
+ def add_headers(self, *header: str):
36
+ """
37
+ Add script header
38
+
39
+ :param header: Header lines
40
+ """
41
+ self._script_header.extend(header)
42
+ return self
43
+
44
+ def add_bottom_files(self, *file: str, encoding='utf-8'):
37
45
  """
38
46
  Add script bottom from files
39
47
 
40
48
  :param file: File path
41
49
  :param encoding: File encoding
42
50
  """
43
- with open(file, 'r', encoding=encoding) as f:
44
- self._script_bottom.append(f.read())
51
+ self._script_bottom.extend(load_files(*file, encoding=encoding))
52
+ return self
53
+
54
+ def add_bottoms(self, *bottom: str):
55
+ """
56
+ Add script bottom
45
57
 
46
- def add_command_file(self, file: str, encoding='utf-8'):
58
+ :param bottom: Bottom lines
59
+ """
60
+ self._script_bottom.extend(bottom)
61
+ return self
62
+
63
+ def add_cmd_files(self, *file: str, encoding='utf-8'):
47
64
  """
48
65
  Add commands from files to run under every working directory
49
66
 
50
67
  :param file: File path
51
68
  :param encoding: File encoding
52
69
  """
53
- with open(file, 'r', encoding=encoding) as f:
54
- self._command.append(f.read())
70
+ self._command.extend(load_files(*file, encoding=encoding))
55
71
  return self
56
72
 
57
- def add_command(self, *cmd: str):
73
+ def add_cmds(self, *cmd: str):
58
74
  """
59
75
  add commands to run under every working directory
60
76
 
@@ -68,10 +84,10 @@ class BatchMaker:
68
84
  Make batch script files from the previous setup
69
85
 
70
86
  :param path: Path to save batch script files, use {i} to represent index
71
- :param concurrency: Number of concurrent commands to run
87
+ :param concurrency: Number of scripts to to make
72
88
  """
73
89
  # inject pre-defined functions
74
- self.add_header_file(get_asset('functions.sh'))
90
+ self.add_header_files(get_asset('functions.sh'))
75
91
 
76
92
  header = '\n'.join(self._script_header)
77
93
  bottom = '\n'.join(self._script_bottom)
@@ -80,10 +96,10 @@ class BatchMaker:
80
96
  work_dirs_arr = "\n".join(shlex.quote(w) for w in work_dirs)
81
97
  body.extend([
82
98
  '[ -n "$PBS_O_WORKDIR" ] && cd $PBS_O_WORKDIR # fix PBS',
83
- f'work_dirs=({work_dirs_arr})',
99
+ f'WORK_DIRS=({work_dirs_arr})',
84
100
  '',
85
- 'for work_dir in "${work_dirs[@]}"; do',
86
- 'pushd $work_dir',
101
+ 'for WORK_DIR in "${WORK_DIRS[@]}"; do',
102
+ 'pushd $WORK_DIR',
87
103
  *self._command,
88
104
  'popd',
89
105
  'done'
@@ -94,3 +110,17 @@ class BatchMaker:
94
110
  with open(out_path, 'w', encoding=encoding) as f:
95
111
  f.write(script)
96
112
  os.chmod(out_path, mode_translate(str(mode)))
113
+
114
+
115
+ def load_files(*file, encoding='utf-8', raise_invalid=False):
116
+ """
117
+ Load files from paths
118
+
119
+ :param files: List of file paths
120
+ :return: List of file contents
121
+ """
122
+ result = []
123
+ for file in expand_globs(file, raise_invalid=raise_invalid):
124
+ with open(file, 'r', encoding=encoding) as f:
125
+ result.append(f.read())
126
+ return result
@@ -23,6 +23,9 @@ class OhMyBatch:
23
23
  def job(self):
24
24
  return JobCli()
25
25
 
26
+ def misc(self):
27
+ from .misc import Misc
28
+ return Misc()
26
29
 
27
30
  def main():
28
31
  fire.Fire(OhMyBatch)
@@ -6,7 +6,7 @@ import time
6
6
  import os
7
7
  import re
8
8
 
9
- from .util import expand_globs, shell_run, parse_csv
9
+ from .util import expand_globs, shell_run, parse_csv, ensure_dir, log_cp
10
10
 
11
11
 
12
12
  logger = logging.getLogger(__name__)
@@ -70,6 +70,7 @@ class BaseJobManager:
70
70
  while True:
71
71
  self._update_jobs(jobs, max_tries, opts)
72
72
  if recovery:
73
+ ensure_dir(recovery)
73
74
  with open(recovery, 'w', encoding='utf-8') as f:
74
75
  json.dump(jobs, f, indent=2)
75
76
 
@@ -103,7 +104,7 @@ class Slurm(BaseJobManager):
103
104
  query_cmd = f'{self._sacct_bin} -X -P --format=JobID,JobName,State -j {",".join(job_ids)}'
104
105
  cp = shell_run(query_cmd)
105
106
  if cp.returncode != 0:
106
- logger.error('Failed to query job status: %s', cp.stderr.decode('utf-8'))
107
+ logger.error('Failed to query job status: %s', log_cp(cp))
107
108
  return jobs
108
109
  logger.info('Job status:\n%s', cp.stdout.decode('utf-8'))
109
110
  new_state = parse_csv(cp.stdout.decode('utf-8'))
@@ -132,7 +133,7 @@ class Slurm(BaseJobManager):
132
133
  cp = shell_run(submit_cmd)
133
134
  if cp.returncode != 0:
134
135
  job['state'] = JobState.FAILED
135
- logger.error('Failed to submit job: %s', cp.stderr.decode('utf-8'))
136
+ logger.error('Failed to submit job: %s', log_cp(cp))
136
137
  else:
137
138
  job['id'] = self._parse_job_id(cp.stdout.decode('utf-8'))
138
139
  assert job['id'], 'Failed to parse job id'
@@ -0,0 +1,14 @@
1
+ from .assets import get_asset
2
+
3
+ class Misc:
4
+
5
+ def export_shell_func(self):
6
+ """
7
+ Export shell functions for batch scripts
8
+
9
+ For example, you can load them to you shell environment with the following command:
10
+ omb misc export-shell-func > omb-func.sh && source omb-func.sh
11
+ """
12
+ shell_func = get_asset('functions.sh')
13
+ with open(shell_func, 'r', encoding='utf-8') as f:
14
+ print(f.read())
@@ -83,4 +83,18 @@ def parse_csv(text: str, delimiter="|"):
83
83
  Parse CSV text to list of dictionaries
84
84
  """
85
85
  reader = csv.DictReader(text.splitlines(), delimiter=delimiter)
86
- return list(reader)
86
+ return list(reader)
87
+
88
+
89
+ def log_cp(cp):
90
+ """
91
+ Log child process
92
+ """
93
+ log = f'Command: {cp.args}\nReturn code: {cp.returncode}'
94
+
95
+ out = cp.stdout.decode('utf-8').strip()
96
+ if out:
97
+ log += f'\nSTDOUT:\n{out}'
98
+ err = cp.stderr.decode('utf-8').strip()
99
+ if err:
100
+ log += f'\nSTDERR:\n{err}'
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "oh-my-batch"
3
- version = "0.1.1"
3
+ version = "0.2.1"
4
4
  description = ""
5
5
  authors = ["weihong.xu <xuweihong.cn@gmail.com>"]
6
6
  license = "GPL"
@@ -11,7 +11,6 @@ packages = [{include = "oh_my_batch"}]
11
11
  python = "^3.8"
12
12
  fire = "^0.7.0"
13
13
 
14
-
15
14
  [build-system]
16
15
  requires = ["poetry-core"]
17
16
  build-backend = "poetry.core.masonry.api"
File without changes