cmd-queue 0.1.20__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.

Potentially problematic release.


This version of cmd-queue might be problematic. Click here for more details.

cmd_queue/__init__.py CHANGED
@@ -306,7 +306,7 @@ Example:
306
306
  __mkinit__ = """
307
307
  mkinit -m cmd_queue
308
308
  """
309
- __version__ = '0.1.20'
309
+ __version__ = '0.2.1'
310
310
 
311
311
 
312
312
  __submodules__ = {
cmd_queue/base_queue.py CHANGED
@@ -135,6 +135,11 @@ class Queue(ub.NiceRepr):
135
135
  name = kwargs.get('name', None)
136
136
  if name is None:
137
137
  name = kwargs['name'] = self.name + '-job-{}'.format(self.num_real_jobs)
138
+
139
+ # TODO: make sure name is path safe.
140
+ if ':' in name:
141
+ raise ValueError('Name must be path-safe')
142
+
138
143
  if self.all_depends:
139
144
  depends = kwargs.get('depends', None)
140
145
  if depends is None:
cmd_queue/main.py CHANGED
@@ -94,12 +94,15 @@ class CommonShowRun(CommonConfig):
94
94
 
95
95
  backend = scfg.Value('tmux', help='the execution backend to use', choices=['tmux', 'slurm', 'serial', 'airflow'])
96
96
 
97
+ gpus = scfg.Value(None, help='a comma separated list of the gpu numbers to spread across. tmux backend only.')
98
+
97
99
  def _build_queue(config):
98
100
  import cmd_queue
99
101
  import json
100
102
  queue = cmd_queue.Queue.create(size=max(1, config['workers']),
101
103
  backend=config['backend'],
102
- name=config['qname'])
104
+ name=config['qname'],
105
+ gpus=config['gpus'])
103
106
  # Run a new CLI queue
104
107
  data = json.loads(config.cli_queue_fpath.read_text())
105
108
  print('data = {}'.format(ub.urepr(data, nl=1)))
cmd_queue/serial_queue.py CHANGED
@@ -9,30 +9,6 @@ from cmd_queue import base_queue
9
9
  from cmd_queue.util import util_tags
10
10
 
11
11
 
12
- def indent(text, prefix=' '):
13
- r"""
14
- Indents a block of text
15
-
16
- Args:
17
- text (str): text to indent
18
- prefix (str, default = ' '): prefix to add to each line
19
-
20
- Returns:
21
- str: indented text
22
-
23
- >>> from cmd_queue.serial_queue import * # NOQA
24
- >>> text = ['aaaa', 'bb', 'cc\n dddd\n ef\n']
25
- >>> text = indent(text)
26
- >>> print(text)
27
- >>> text = indent(text)
28
- >>> print(text)
29
- """
30
- if isinstance(text, (list, tuple)):
31
- return indent('\n'.join(text), prefix)
32
- else:
33
- return prefix + text.replace('\n', '\n' + prefix)
34
-
35
-
36
12
  class BashJob(base_queue.Job):
37
13
  r"""
38
14
  A job meant to run inside of a larger bash file. Analog of SlurmJob
@@ -115,6 +91,21 @@ class BashJob(base_queue.Job):
115
91
  self.tags = util_tags.Tags.coerce(tags)
116
92
  self.allow_indent = allow_indent
117
93
 
94
+ def _test_bash_syntax_errors(self):
95
+ """
96
+ Check for bash syntax errors
97
+
98
+ Example:
99
+ >>> from cmd_queue.serial_queue import * # NOQA
100
+ >>> # Demo full boilerplate for a job with dependencies
101
+ >>> self = BashJob('basd syhi(', name='job1')
102
+ >>> import pytest
103
+ >>> with pytest.raises(SyntaxError):
104
+ >>> self._test_bash_syntax_errors()
105
+ """
106
+ bash_text = self.finalize_text()
107
+ _check_bash_text_for_syntax_errors(bash_text)
108
+
118
109
  def finalize_text(self, with_status=True, with_gaurds=True,
119
110
  conditionals=None, **kwargs):
120
111
  script = []
@@ -575,6 +566,10 @@ class SerialQueue(base_queue.Queue):
575
566
  r"""
576
567
  Print info about the commands, optionally with rich
577
568
 
569
+ Args:
570
+ *args: see :func:`cmd_queue.base_queue.Queue.print_commands`.
571
+ **kwargs: see :func:`cmd_queue.base_queue.Queue.print_commands`.
572
+
578
573
  CommandLine:
579
574
  xdoctest -m cmd_queue.serial_queue SerialQueue.print_commands
580
575
 
@@ -713,3 +708,40 @@ def _bash_json_dump(json_fmt_parts, fpath):
713
708
  printf_part = 'printf ' + printf_body + ' \\\n ' + printf_args
714
709
  dump_code = printf_part + ' \\\n ' + redirect_part
715
710
  return dump_code
711
+
712
+
713
+ def indent(text, prefix=' '):
714
+ r"""
715
+ Indents a block of text
716
+
717
+ Args:
718
+ text (str): text to indent
719
+ prefix (str, default = ' '): prefix to add to each line
720
+
721
+ Returns:
722
+ str: indented text
723
+
724
+ >>> from cmd_queue.serial_queue import * # NOQA
725
+ >>> text = ['aaaa', 'bb', 'cc\n dddd\n ef\n']
726
+ >>> text = indent(text)
727
+ >>> print(text)
728
+ >>> text = indent(text)
729
+ >>> print(text)
730
+ """
731
+ if isinstance(text, (list, tuple)):
732
+ return indent('\n'.join(text), prefix)
733
+ else:
734
+ return prefix + text.replace('\n', '\n' + prefix)
735
+
736
+
737
+ def _check_bash_text_for_syntax_errors(bash_text):
738
+ import tempfile
739
+ tmpdir = tempfile.TemporaryDirectory()
740
+ with tmpdir:
741
+ dpath = ub.Path(tmpdir.name)
742
+ fpath = dpath / 'job_text.sh'
743
+ fpath.write_text(bash_text)
744
+ info = ub.cmd(['bash', '-nv', fpath])
745
+ if info.returncode != 0:
746
+ print(info.stderr)
747
+ raise SyntaxError('bash syntax error')
cmd_queue/slurm_queue.py CHANGED
@@ -41,23 +41,51 @@ from cmd_queue import base_queue # NOQA
41
41
  from cmd_queue.util import util_tags
42
42
 
43
43
 
44
- def _coerce_mem(mem):
44
+ try:
45
+ from functools import cache # Python 3.9+ only
46
+ except ImportError:
47
+ from ubelt import memoize as cache
48
+
49
+
50
+ @cache
51
+ def _unit_registery():
52
+ import sys
53
+ if sys.version_info[0:2] == (3, 9):
54
+ # backwards compatability support for numpy 2.0 and pint on cp39
55
+ try:
56
+ import numpy as np
57
+ except ImportError:
58
+ ...
59
+ else:
60
+ if not np.__version__.startswith('1.'):
61
+ np.cumproduct = np.cumprod
62
+ import pint
63
+ reg = pint.UnitRegistry()
64
+ return reg
65
+
66
+
67
+ def _coerce_mem_megabytes(mem):
45
68
  """
69
+ Transform input into an integer representing amount of megabytes.
70
+
46
71
  Args:
47
72
  mem (int | str): integer number of megabytes or a parseable string
48
73
 
74
+ Returns:
75
+ int: number of megabytes
76
+
49
77
  Example:
78
+ >>> # xdoctest: +REQUIRES(module:pint)
50
79
  >>> from cmd_queue.slurm_queue import * # NOQA
51
- >>> print(_coerce_mem(30602))
52
- >>> print(_coerce_mem('4GB'))
53
- >>> print(_coerce_mem('32GB'))
54
- >>> print(_coerce_mem('300000000 bytes'))
80
+ >>> print(_coerce_mem_megabytes(30602))
81
+ >>> print(_coerce_mem_megabytes('4GB'))
82
+ >>> print(_coerce_mem_megabytes('32GB'))
83
+ >>> print(_coerce_mem_megabytes('300000000 bytes'))
55
84
  """
56
85
  if isinstance(mem, int):
57
86
  assert mem > 0
58
87
  elif isinstance(mem, str):
59
- import pint
60
- reg = pint.UnitRegistry()
88
+ reg = _unit_registery()
61
89
  mem = reg.parse_expression(mem)
62
90
  mem = int(mem.to('megabytes').m)
63
91
  else:
@@ -190,6 +218,7 @@ class SlurmJob(base_queue.Job):
190
218
  Represents a slurm job that hasn't been submitted yet
191
219
 
192
220
  Example:
221
+ >>> # xdoctest: +REQUIRES(module:pint)
193
222
  >>> from cmd_queue.slurm_queue import * # NOQA
194
223
  >>> self = SlurmJob('python -c print("hello world")', 'hi', cpus=5, gpus=1, mem='10GB')
195
224
  >>> command = self._build_sbatch_args()
@@ -235,17 +264,13 @@ class SlurmJob(base_queue.Job):
235
264
  return ' \\\n '.join(args)
236
265
 
237
266
  def _build_sbatch_args(self, jobname_to_varname=None):
238
- # job_name = 'todo'
239
- # output_fpath = '$HOME/.cache/slurm/logs/job-%j-%x.out'
240
- # command = "python -c 'import sys; sys.exit(1)'"
241
- # -c 2 -p priority --gres=gpu:1
242
267
  sbatch_args = ['sbatch']
243
268
  if self.name:
244
269
  sbatch_args.append(f'--job-name="{self.name}"')
245
270
  if self.cpus:
246
271
  sbatch_args.append(f'--cpus-per-task={self.cpus}')
247
272
  if self.mem:
248
- mem = _coerce_mem(self.mem)
273
+ mem = _coerce_mem_megabytes(self.mem)
249
274
  sbatch_args.append(f'--mem={mem}')
250
275
  if self.gpus and 'gres' not in self._sbatch_kvargs:
251
276
  ub.schedule_deprecation(
@@ -277,7 +302,8 @@ class SlurmJob(base_queue.Job):
277
302
 
278
303
  for key, value in self._sbatch_kvargs.items():
279
304
  key = key.replace('_', '-')
280
- sbatch_args.append(f'--{key}="{value}"')
305
+ if value is not None:
306
+ sbatch_args.append(f'--{key}="{value}"')
281
307
 
282
308
  for key, flag in self._sbatch_flags.items():
283
309
  if flag:
@@ -342,6 +368,19 @@ class SlurmQueue(base_queue.Queue):
342
368
  CommandLine:
343
369
  xdoctest -m cmd_queue.slurm_queue SlurmQueue
344
370
 
371
+ Example:
372
+ >>> from cmd_queue.slurm_queue import * # NOQA
373
+ >>> self = SlurmQueue()
374
+ >>> job0 = self.submit('echo "hi from $SLURM_JOBID"')
375
+ >>> job1 = self.submit('echo "hi from $SLURM_JOBID"', depends=[job0])
376
+ >>> job2 = self.submit('echo "hi from $SLURM_JOBID"', depends=[job1])
377
+ >>> job3 = self.submit('echo "hi from $SLURM_JOBID"', depends=[job1, job2])
378
+ >>> self.write()
379
+ >>> self.print_commands()
380
+ >>> # xdoctest: +REQUIRES(--run)
381
+ >>> if not self.is_available():
382
+ >>> self.run()
383
+
345
384
  Example:
346
385
  >>> from cmd_queue.slurm_queue import * # NOQA
347
386
  >>> self = SlurmQueue()
@@ -384,6 +423,11 @@ class SlurmQueue(base_queue.Queue):
384
423
  self.unused_kwargs = kwargs
385
424
  self.queue_id = name + '-' + stamp + '-' + ub.hash_data(uuid.uuid4())[0:8]
386
425
  self.dpath = ub.Path.appdir('cmd_queue/slurm') / self.queue_id
426
+ if 0:
427
+ # hack for submission on different systems, probably dont want to
428
+ # do this.
429
+ self.dpath = self.dpath.shrinkuser(home='$HOME')
430
+
387
431
  self.log_dpath = self.dpath / 'logs'
388
432
  self.fpath = self.dpath / (self.queue_id + '.sh')
389
433
  self.shell = shell
@@ -395,6 +439,37 @@ class SlurmQueue(base_queue.Queue):
395
439
  def __nice__(self):
396
440
  return self.queue_id
397
441
 
442
+ @classmethod
443
+ def _slurm_checks(cls):
444
+ status = {}
445
+ info = {}
446
+ info['squeue_fpath'] = ub.find_exe('squeue')
447
+ status['has_squeue'] = bool(info['squeue_fpath'])
448
+ status['slurmd_running'] = False
449
+ import psutil
450
+ for p in psutil.process_iter():
451
+ if p.name() == 'slurmd':
452
+ status['slurmd_running'] = True
453
+ info['slurmd_info'] = {
454
+ 'pid': p.pid,
455
+ 'name': p.name(),
456
+ 'status': p.status(),
457
+ 'create_time': p.create_time(),
458
+ }
459
+ break
460
+ status['squeue_working'] = (ub.cmd('squeue')['ret'] == 0)
461
+
462
+ sinfo = ub.cmd('sinfo --json')
463
+ status['sinfo_working'] = False
464
+ if sinfo['ret'] == 0:
465
+ status['sinfo_working'] = True
466
+ import json
467
+ sinfo_out = json.loads(sinfo['out'])
468
+ has_working_nodes = not all(
469
+ node['state'] == 'down'
470
+ for node in sinfo_out['nodes'])
471
+ status['has_working_nodes'] = has_working_nodes
472
+
398
473
  @classmethod
399
474
  def is_available(cls):
400
475
  """
@@ -407,15 +482,23 @@ class SlurmQueue(base_queue.Queue):
407
482
  squeue_working = (ub.cmd('squeue')['ret'] == 0)
408
483
  if squeue_working:
409
484
  # Check if nodes are available or down
410
- sinfo = ub.cmd('sinfo --json')
411
- if sinfo['ret'] == 0:
412
- import json
413
- sinfo_out = json.loads(sinfo['out'])
414
- has_working_nodes = not all(
415
- node['state'] == 'down'
416
- for node in sinfo_out['nodes'])
417
- if has_working_nodes:
418
- return True
485
+ # note: the --json command is not available in
486
+ # slurm-wlm 19.05.5, but it is in slurm-wlm 21.08.5
487
+ sinfo_version_str = ub.cmd('sinfo --version').stdout.strip().split(' ')[1]
488
+ sinfo_major_version = int(sinfo_version_str.split('.')[0])
489
+ if sinfo_major_version < 21:
490
+ # Dont check in this case
491
+ return True
492
+ else:
493
+ sinfo = ub.cmd('sinfo --json')
494
+ if sinfo['ret'] == 0:
495
+ import json
496
+ sinfo_out = json.loads(sinfo['out'])
497
+ has_working_nodes = not all(
498
+ node['state'] == 'down'
499
+ for node in sinfo_out['nodes'])
500
+ if has_working_nodes:
501
+ return True
419
502
  return False
420
503
 
421
504
  def submit(self, command, **kwargs):
@@ -520,6 +603,10 @@ class SlurmQueue(base_queue.Queue):
520
603
  info = ub.cmd('squeue --format="%i %P %j %u %t %M %D %R"')
521
604
  stream = io.StringIO(info['out'])
522
605
  df = pd.read_csv(stream, sep=' ')
606
+
607
+ # Only include job names that this queue created
608
+ job_names = [job.name for job in self.jobs]
609
+ df = df[df['NAME'].isin(job_names)]
523
610
  jobid_history.update(df['JOBID'])
524
611
 
525
612
  num_running = (df['ST'] == 'R').sum()
@@ -700,4 +787,9 @@ sbatch \
700
787
  squeue
701
788
 
702
789
 
790
+
791
+ References:
792
+ https://stackoverflow.com/questions/74164136/slurm-accessing-stdout-stderr-location-of-a-completed-job
793
+
794
+
703
795
  """
cmd_queue/slurmify.py ADDED
@@ -0,0 +1,116 @@
1
+ r"""
2
+ Helper script to wrap a command with sbatch, but using a more srun like syntax.
3
+
4
+ .. code:: bash
5
+
6
+ python -m cmd_queue.slurmify \
7
+ --jobname="my_job" \
8
+ --depends=None \
9
+ --gpus=1 \
10
+ --mem=16GB \
11
+ --cpus_per_task=5 \
12
+ --ntasks=1 \
13
+ --ntasks-per-node=1 \
14
+ --partition=community \
15
+ -- \
16
+ python -c 'import sys; print("hello world"); sys.exit(0)'
17
+ """
18
+ #!/usr/bin/env python3
19
+ import scriptconfig as scfg
20
+ import ubelt as ub
21
+
22
+
23
+ class SlurmifyCLI(scfg.DataConfig):
24
+ __command__ = 'slurmify'
25
+
26
+ jobname = scfg.Value(None, help='for submit, this is the name of the new job')
27
+ depends = scfg.Value(None, help='comma separated jobnames to depend on')
28
+
29
+ command = scfg.Value(None, type=str, position=1, nargs='*', help=ub.paragraph(
30
+ '''
31
+ Specifies the bash command to queue.
32
+ Care must be taken when specifying this argument. If specifying as a
33
+ key/value pair argument, it is important to quote and escape the bash
34
+ command properly. A more convinient way to specify this command is as
35
+ a positional argument. End all of the options to this CLI with `--` and
36
+ then specify your full command.
37
+ '''))
38
+
39
+ gpus = scfg.Value(None, help='a comma separated list of the gpu numbers to spread across. tmux backend only.')
40
+ workers = scfg.Value(1, help='number of concurrent queues for the tmux backend.')
41
+
42
+ mem = scfg.Value(None, help='')
43
+ partition = scfg.Value(1, help='slurm partition')
44
+
45
+ ntasks = scfg.Value(None, help='')
46
+ ntasks_per_node = scfg.Value(None, help='')
47
+ cpus_per_task = scfg.Value(None, help='')
48
+
49
+ @classmethod
50
+ def main(cls, cmdline=1, **kwargs):
51
+ """
52
+ Example:
53
+ >>> # xdoctest: +SKIP
54
+ >>> from cmd_queue.slurmify import * # NOQA
55
+ >>> cmdline = 0
56
+ >>> kwargs = dict()
57
+ >>> cls = SlurmifyCLI
58
+ >>> cls.main(cmdline=cmdline, **kwargs)
59
+ """
60
+ import rich
61
+ from rich.markup import escape
62
+ config = cls.cli(cmdline=cmdline, data=kwargs, strict=True)
63
+ rich.print('config = ' + escape(ub.urepr(config, nl=1)))
64
+
65
+ # import json
66
+ # Run a new CLI queue
67
+ row = {'type': 'command', 'command': config['command']}
68
+ if config.jobname:
69
+ row['name'] = config.jobname
70
+ if config.depends:
71
+ row['depends'] = config.depends
72
+
73
+ import cmd_queue
74
+ queue = cmd_queue.Queue.create(
75
+ size=max(1, config['workers']),
76
+ backend='slurm',
77
+ name='slurmified',
78
+ gpus=config['gpus'],
79
+ mem=config['mem'],
80
+ partition=config['partition'],
81
+ ntasks=config['ntasks'],
82
+ ntasks_per_node=config['ntasks_per_node'],
83
+ )
84
+ try:
85
+ bash_command = row['command']
86
+ if isinstance(bash_command, list):
87
+ if len(bash_command) == 1:
88
+ # hack
89
+ import shlex
90
+ if shlex.quote(bash_command[0]) == bash_command[0]:
91
+ bash_command = bash_command[0]
92
+ else:
93
+ bash_command = shlex.quote(bash_command[0])
94
+ else:
95
+ import shlex
96
+ bash_command = ' '.join([shlex.quote(str(p)) for p in bash_command])
97
+ submitkw = ub.udict(row) & {'name', 'depends'}
98
+ queue.submit(bash_command, log=False, **submitkw)
99
+ except Exception:
100
+ print('row = {}'.format(ub.urepr(row, nl=1)))
101
+ raise
102
+ queue.print_commands()
103
+
104
+ # config.cli_queue_fpath.write_text(json.dumps(row))
105
+ # 'sbatch --job-name="test_job1" --output="$HOME/.cache/slurm/logs/job-%j-%x.out" --wrap=""
106
+
107
+ __cli__ = SlurmifyCLI
108
+
109
+ if __name__ == '__main__':
110
+ """
111
+
112
+ CommandLine:
113
+ python ~/code/cmd_queue/cmd_queue/slurmify.py
114
+ python -m cmd_queue.slurmify
115
+ """
116
+ __cli__.main()
cmd_queue/tmux_queue.py CHANGED
@@ -1055,4 +1055,38 @@ if 0:
1055
1055
  tmux kill-session -t my_session_id
1056
1056
 
1057
1057
  tmux new-session -d -s my_session_id -e "MYVAR1" -- "bash"
1058
+
1059
+
1060
+
1061
+ #### to start a tmux session with 4 panes
1062
+ tmux new-session -d -s my_session_id1 "bash"
1063
+ tmux send -t my_session_id1 "tmux split-window -h -t 0" Enter
1064
+ tmux send -t my_session_id1 "tmux split-window -v -t 0" Enter
1065
+ tmux send -t my_session_id1 "tmux split-window -v -t 2" Enter
1066
+
1067
+ # Now send a command to each pane
1068
+ tmux send -t my_session_id1 "tmux select-pane -t 0" Enter
1069
+ tmux send -t my_session_id1 "echo pane0" Enter
1070
+ tmux send -t my_session_id1 "tmux select-pane -t 1" Enter
1071
+ tmux send -t my_session_id1 "echo pane1" Enter
1072
+ tmux send -t my_session_id1 "tmux select-pane -t 2" Enter
1073
+ tmux send -t my_session_id1 "echo pane2" Enter
1074
+ tmux send -t my_session_id1 "tmux select-pane -t 3" Enter
1075
+ tmux send -t my_session_id1 "echo pane3" Enter
1076
+
1077
+ # https://stackoverflow.com/questions/54954177/how-to-write-a-tmux-script-so-that-it-automatically-split-windows-and-opens-a-se
1078
+ # https://tmuxcheatsheet.com/
1079
+ # https://gist.github.com/Starefossen/5955406
1080
+
1081
+ # List the bindings
1082
+ tmux list-keys
1083
+
1084
+ # Can arange the splits in a session via a preset layout
1085
+ # Preset layouts are:
1086
+ # even-horizontal, even-vertical, main-horizontal, main-vertical, or tiled.
1087
+ tmux select-layout -t "${SESSION_NAME}" even-vertical
1088
+
1089
+ # switch to an existing session
1090
+ tmux switch -t "${SESSION_NAME}"
1091
+
1058
1092
  """