experimaestro 1.5.1__py3-none-any.whl → 2.0.0a8__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 experimaestro might be problematic. Click here for more details.

Files changed (118) hide show
  1. experimaestro/__init__.py +14 -4
  2. experimaestro/__main__.py +3 -423
  3. experimaestro/annotations.py +14 -4
  4. experimaestro/cli/__init__.py +311 -0
  5. experimaestro/{filter.py → cli/filter.py} +23 -9
  6. experimaestro/cli/jobs.py +268 -0
  7. experimaestro/cli/progress.py +269 -0
  8. experimaestro/click.py +0 -35
  9. experimaestro/commandline.py +3 -7
  10. experimaestro/connectors/__init__.py +29 -14
  11. experimaestro/connectors/local.py +19 -10
  12. experimaestro/connectors/ssh.py +27 -8
  13. experimaestro/core/arguments.py +45 -3
  14. experimaestro/core/callbacks.py +52 -0
  15. experimaestro/core/context.py +8 -9
  16. experimaestro/core/identifier.py +310 -0
  17. experimaestro/core/objects/__init__.py +44 -0
  18. experimaestro/core/{objects.py → objects/config.py} +399 -772
  19. experimaestro/core/objects/config_utils.py +58 -0
  20. experimaestro/core/objects/config_walk.py +151 -0
  21. experimaestro/core/objects.pyi +15 -45
  22. experimaestro/core/serialization.py +63 -9
  23. experimaestro/core/serializers.py +1 -8
  24. experimaestro/core/types.py +104 -66
  25. experimaestro/experiments/cli.py +154 -72
  26. experimaestro/experiments/configuration.py +10 -1
  27. experimaestro/generators.py +6 -1
  28. experimaestro/ipc.py +4 -1
  29. experimaestro/launcherfinder/__init__.py +1 -1
  30. experimaestro/launcherfinder/base.py +2 -18
  31. experimaestro/launcherfinder/parser.py +8 -3
  32. experimaestro/launcherfinder/registry.py +52 -140
  33. experimaestro/launcherfinder/specs.py +49 -10
  34. experimaestro/launchers/direct.py +0 -47
  35. experimaestro/launchers/slurm/base.py +54 -14
  36. experimaestro/mkdocs/__init__.py +1 -1
  37. experimaestro/mkdocs/base.py +6 -8
  38. experimaestro/notifications.py +38 -12
  39. experimaestro/progress.py +406 -0
  40. experimaestro/run.py +24 -3
  41. experimaestro/scheduler/__init__.py +18 -1
  42. experimaestro/scheduler/base.py +108 -808
  43. experimaestro/scheduler/dynamic_outputs.py +184 -0
  44. experimaestro/scheduler/experiment.py +387 -0
  45. experimaestro/scheduler/jobs.py +475 -0
  46. experimaestro/scheduler/signal_handler.py +32 -0
  47. experimaestro/scheduler/state.py +75 -0
  48. experimaestro/scheduler/workspace.py +27 -8
  49. experimaestro/scriptbuilder.py +18 -3
  50. experimaestro/server/__init__.py +36 -5
  51. experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  52. experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  53. experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  54. experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  55. experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  56. experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  57. experimaestro/server/data/index.css +5187 -5068
  58. experimaestro/server/data/index.css.map +1 -1
  59. experimaestro/server/data/index.js +68887 -68064
  60. experimaestro/server/data/index.js.map +1 -1
  61. experimaestro/settings.py +45 -5
  62. experimaestro/sphinx/__init__.py +7 -17
  63. experimaestro/taskglobals.py +7 -2
  64. experimaestro/tests/core/__init__.py +0 -0
  65. experimaestro/tests/core/test_generics.py +206 -0
  66. experimaestro/tests/definitions_types.py +5 -3
  67. experimaestro/tests/launchers/bin/sbatch +34 -7
  68. experimaestro/tests/launchers/bin/srun +5 -0
  69. experimaestro/tests/launchers/common.py +17 -5
  70. experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
  71. experimaestro/tests/restart.py +10 -5
  72. experimaestro/tests/tasks/all.py +23 -10
  73. experimaestro/tests/tasks/foreign.py +2 -4
  74. experimaestro/tests/test_checkers.py +2 -2
  75. experimaestro/tests/test_dependencies.py +11 -17
  76. experimaestro/tests/test_experiment.py +73 -0
  77. experimaestro/tests/test_file_progress.py +425 -0
  78. experimaestro/tests/test_file_progress_integration.py +477 -0
  79. experimaestro/tests/test_findlauncher.py +12 -5
  80. experimaestro/tests/test_forward.py +5 -5
  81. experimaestro/tests/test_generators.py +93 -0
  82. experimaestro/tests/test_identifier.py +182 -158
  83. experimaestro/tests/test_instance.py +19 -27
  84. experimaestro/tests/test_objects.py +13 -20
  85. experimaestro/tests/test_outputs.py +6 -6
  86. experimaestro/tests/test_param.py +68 -30
  87. experimaestro/tests/test_progress.py +4 -4
  88. experimaestro/tests/test_serializers.py +24 -64
  89. experimaestro/tests/test_ssh.py +7 -0
  90. experimaestro/tests/test_tags.py +50 -21
  91. experimaestro/tests/test_tasks.py +42 -51
  92. experimaestro/tests/test_tokens.py +11 -8
  93. experimaestro/tests/test_types.py +24 -21
  94. experimaestro/tests/test_validation.py +67 -110
  95. experimaestro/tests/token_reschedule.py +1 -1
  96. experimaestro/tokens.py +24 -13
  97. experimaestro/tools/diff.py +8 -1
  98. experimaestro/typingutils.py +20 -11
  99. experimaestro/utils/asyncio.py +6 -2
  100. experimaestro/utils/multiprocessing.py +44 -0
  101. experimaestro/utils/resources.py +11 -3
  102. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/METADATA +28 -36
  103. experimaestro-2.0.0a8.dist-info/RECORD +166 -0
  104. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/WHEEL +1 -1
  105. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/entry_points.txt +0 -4
  106. experimaestro/launchers/slurm/cli.py +0 -29
  107. experimaestro/launchers/slurm/configuration.py +0 -597
  108. experimaestro/scheduler/environment.py +0 -94
  109. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  110. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  111. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  112. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  113. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  114. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  115. experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
  116. experimaestro/utils/yaml.py +0 -202
  117. experimaestro-1.5.1.dist-info/RECORD +0 -148
  118. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,311 @@
1
+ # flake8: noqa: T201
2
+ import sys
3
+ from typing import Set, Optional
4
+ import pkg_resources
5
+ from itertools import chain
6
+ from shutil import rmtree
7
+ import click
8
+ import logging
9
+ from functools import cached_property, update_wrapper
10
+ from pathlib import Path
11
+ import subprocess
12
+ from termcolor import cprint
13
+
14
+ import experimaestro
15
+ from experimaestro.experiments.cli import experiments_cli
16
+ import experimaestro.launcherfinder.registry as launcher_registry
17
+ from experimaestro.settings import find_workspace
18
+
19
+ # --- Command line main options
20
+ logging.basicConfig(level=logging.INFO)
21
+
22
+
23
+ def check_xp_path(ctx, self, path: Path):
24
+ if not (path / ".__experimaestro__").is_file():
25
+ cprint(f"{path} is not an experimaestro working directory", "red")
26
+ for path in path.parents:
27
+ if (path / ".__experimaestro__").is_file():
28
+ cprint(f"{path} could be the folder you want", "green")
29
+ if click.confirm("Do you want to use this folder?"):
30
+ return path
31
+ sys.exit(1)
32
+
33
+ return path
34
+
35
+
36
+ class RunConfig:
37
+ def __init__(self):
38
+ self.traceback = False
39
+
40
+
41
+ def pass_cfg(f):
42
+ """Pass configuration information"""
43
+
44
+ @click.pass_context
45
+ def new_func(ctx, *args, **kwargs):
46
+ return ctx.invoke(f, ctx.obj, *args, **kwargs)
47
+
48
+ return update_wrapper(new_func, f)
49
+
50
+
51
+ @click.group()
52
+ @click.option("--quiet", is_flag=True, help="Be quiet")
53
+ @click.option("--debug", is_flag=True, help="Be even more verbose (implies traceback)")
54
+ @click.option(
55
+ "--traceback", is_flag=True, help="Display traceback if an exception occurs"
56
+ )
57
+ @click.pass_context
58
+ def cli(ctx, quiet, debug, traceback):
59
+ if quiet:
60
+ logging.getLogger().setLevel(logging.WARN)
61
+ elif debug:
62
+ logging.getLogger().setLevel(logging.DEBUG)
63
+
64
+ ctx.obj = RunConfig()
65
+ ctx.obj.traceback = traceback
66
+
67
+
68
+ # Adds the run-experiment command
69
+ cli.add_command(experiments_cli, "run-experiment")
70
+
71
+
72
+ @cli.command(help="Get version")
73
+ def version():
74
+ print(experimaestro.__version__)
75
+
76
+
77
+ @click.argument("parameters", type=Path)
78
+ @cli.command(context_settings={"allow_extra_args": True})
79
+ def run(parameters):
80
+ """Run a task"""
81
+
82
+ from experimaestro.run import run as do_run
83
+
84
+ do_run(parameters)
85
+
86
+
87
+ @click.argument("path2", type=Path)
88
+ @click.argument("path1", type=Path)
89
+ @cli.command(context_settings={"allow_extra_args": True})
90
+ def parameters_difference(path1, path2):
91
+ """Compute the difference between two configurations"""
92
+
93
+ from experimaestro.tools.diff import diff
94
+
95
+ diff(path1, path2)
96
+
97
+
98
+ @click.option(
99
+ "--clean", is_flag=True, help="Remove the socket file and its enclosing directory"
100
+ )
101
+ @click.argument("unix-path", type=Path)
102
+ @cli.command()
103
+ def rpyc_server(unix_path, clean):
104
+ """Start an rPyC server"""
105
+ from experimaestro.rpyc import start_server
106
+
107
+ start_server(unix_path, clean=clean)
108
+
109
+
110
+ @cli.group()
111
+ def deprecated():
112
+ """Manage identifier changes"""
113
+ pass
114
+
115
+
116
+ @click.option("--fix", is_flag=True, help="Generate links to new IDs")
117
+ @click.option("--cleanup", is_flag=True, help="Remove symbolic links and move folders")
118
+ @click.argument("path", type=Path, callback=check_xp_path)
119
+ @deprecated.command(name="list")
120
+ def deprecated_list(path: Path, fix: bool, cleanup: bool):
121
+ """List deprecated jobs"""
122
+ from experimaestro.tools.jobs import fix_deprecated
123
+
124
+ if cleanup and not fix:
125
+ logging.warning("Ignoring --cleanup since we are not fixing old IDs")
126
+ fix_deprecated(path, fix, cleanup)
127
+
128
+
129
+ @click.argument("path", type=Path, callback=check_xp_path)
130
+ @deprecated.command()
131
+ def diff(path: Path):
132
+ """Show the reason of the identifier change for a job"""
133
+ from experimaestro.tools.jobs import load_job
134
+ from experimaestro import Config
135
+
136
+ _, job = load_job(path / "params.json", discard_id=False)
137
+ _, new_job = load_job(path / "params.json")
138
+
139
+ def check(path: str, value, new_value, done: Set[int]):
140
+ if isinstance(value, Config):
141
+ if id(value) in done:
142
+ return
143
+ done.add(id(value))
144
+
145
+ old_id = value.__xpm__.identifier.all.hex()
146
+ new_id = new_value.__xpm__.identifier.all.hex()
147
+
148
+ if new_id != old_id:
149
+ print(f"{path} differ: {new_id} vs {old_id}")
150
+
151
+ for arg in value.__xpmtype__.arguments.values():
152
+ arg_value = getattr(value, arg.name)
153
+ arg_newvalue = getattr(new_value, arg.name)
154
+ check(f"{path}/{arg.name}", arg_value, arg_newvalue, done)
155
+
156
+ elif isinstance(value, list):
157
+ for ix, (array_value, array_newvalue) in enumerate(zip(value, new_value)):
158
+ check(f"{path}.{ix}", array_value, array_newvalue, done)
159
+
160
+ elif isinstance(value, dict):
161
+ for key, dict_value in value.items():
162
+ check(f"{path}.{key}", dict_value, new_value[key], done)
163
+
164
+ check(".", job, new_job, set())
165
+
166
+
167
+ @click.option("--show-all", is_flag=True, help="Show even not orphans")
168
+ @click.option(
169
+ "--ignore-old", is_flag=True, help="Ignore old jobs for unfinished experiments"
170
+ )
171
+ @click.option("--clean", is_flag=True, help="Prune the orphan folders")
172
+ @click.option("--size", is_flag=True, help="Show size of each folder")
173
+ @click.argument("path", type=Path, callback=check_xp_path)
174
+ @cli.command()
175
+ def orphans(path: Path, clean: bool, size: bool, show_all: bool, ignore_old: bool):
176
+ """Check for tasks that are not part of an experimental plan"""
177
+
178
+ jobspath = path / "jobs"
179
+
180
+ def getjobs(path: Path):
181
+ return ((str(p.relative_to(path)), p) for p in path.glob("*/*") if p.is_dir())
182
+
183
+ def show(key: str, prefix=""):
184
+ if size:
185
+ print(
186
+ prefix,
187
+ subprocess.check_output(["du", "-hs", key], cwd=jobspath)
188
+ .decode("utf-8")
189
+ .strip(),
190
+ sep=None,
191
+ )
192
+ else:
193
+ print(prefix, key, sep=None)
194
+
195
+ for p in (path / "xp").glob("*/jobs.bak"):
196
+ logging.warning("Experiment %s has not completed successfully", p.parent.name)
197
+
198
+ # Retrieve the jobs within expedriments (jobs and jobs.bak folder within experiments)
199
+ xpjobs = set()
200
+ if ignore_old:
201
+ paths = (path / "xp").glob("*/jobs")
202
+ else:
203
+ paths = chain((path / "xp").glob("*/jobs"), (path / "xp").glob("*/jobs.bak"))
204
+
205
+ for p in paths:
206
+ if p.is_dir():
207
+ for relpath, path in getjobs(p):
208
+ xpjobs.add(relpath)
209
+
210
+ # Now, look at stored jobs
211
+ found = 0
212
+ for key, jobpath in getjobs(jobspath):
213
+ if key not in xpjobs:
214
+ show(key)
215
+ if clean:
216
+ logging.info("Removing data in %s", jobpath)
217
+ rmtree(jobpath)
218
+ else:
219
+ if show_all:
220
+ show(key, prefix="[not orphan] ")
221
+ found += 1
222
+
223
+ print(f"{found} jobs are not orphans")
224
+
225
+
226
+ def arg_split(ctx, param, value):
227
+ # split columns by ',' and remove whitespace
228
+ return set(c.strip() for c in value.split(","))
229
+
230
+
231
+ @click.option("--skip", default=set(), callback=arg_split)
232
+ @click.argument("package", type=str)
233
+ @click.argument("objects", type=Path)
234
+ @cli.command()
235
+ def check_documentation(objects, package, skip):
236
+ """Check that all the configuration and tasks are documented within a
237
+ package, relying on the sphinx objects.inv file"""
238
+ from experimaestro.tools.documentation import documented_from_objects, undocumented
239
+
240
+ documented = documented_from_objects(objects)
241
+ errors, configs = undocumented([package], documented, skip)
242
+ for config in configs:
243
+ cprint(f"{config.__module__}.{config.__qualname__}", "red")
244
+
245
+ if errors > 0 or configs:
246
+ sys.exit(1)
247
+
248
+
249
+ @click.option("--config", type=Path, help="Show size of each folder")
250
+ @click.argument("spec", type=str)
251
+ @cli.command()
252
+ def find_launchers(config: Optional[Path], spec: str):
253
+ """Find launchers matching a specification"""
254
+ if config is not None:
255
+ launcher_registry.LauncherRegistry.set_config_dir(config)
256
+
257
+ print(launcher_registry.find_launcher(spec))
258
+
259
+
260
+ class Launchers(click.MultiCommand):
261
+ """Connectors commands"""
262
+
263
+ @cached_property
264
+ def commands(self):
265
+ map = {}
266
+ for ep in pkg_resources.iter_entry_points(f"experimaestro.{self.name}"):
267
+ if get_cli := getattr(ep.load(), "get_cli", None):
268
+ map[ep.name] = get_cli()
269
+ return map
270
+
271
+ def list_commands(self, ctx):
272
+ return self.commands.keys()
273
+
274
+ def get_command(self, ctx, name):
275
+ return self.commands[name]
276
+
277
+
278
+ cli.add_command(Launchers("launchers", help="Launcher specific commands"))
279
+ cli.add_command(Launchers("connectors", help="Connector specific commands"))
280
+ cli.add_command(Launchers("tokens", help="Token specific commands"))
281
+
282
+ # Import and add progress commands
283
+ from .progress import progress as progress_cli
284
+
285
+ cli.add_command(progress_cli)
286
+
287
+ # Import and add jobs commands
288
+ from .jobs import jobs as jobs_cli
289
+
290
+ cli.add_command(jobs_cli)
291
+
292
+
293
+ @cli.group()
294
+ @click.option("--workdir", type=Path, default=None)
295
+ @click.option("--workspace", type=str, default=None)
296
+ @click.pass_context
297
+ def experiments(ctx, workdir, workspace):
298
+ """Manage experiments"""
299
+ ws = find_workspace(workdir=workdir, workspace=workspace)
300
+ path = check_xp_path(None, None, ws.path)
301
+ ctx.obj = path
302
+
303
+
304
+ @experiments.command()
305
+ @pass_cfg
306
+ def list(workdir: Path):
307
+ for p in (workdir / "xp").iterdir():
308
+ if (p / "jobs.bak").exists():
309
+ cprint(f"[unfinished] {p.name}", "yellow")
310
+ else:
311
+ cprint(p.name, "cyan")
@@ -1,20 +1,27 @@
1
+ import asyncio
2
+ import logging
1
3
  from typing import Any, Callable, Dict, List, Optional
2
4
  import pyparsing as pp
3
5
  from pathlib import Path
4
6
  import json
5
7
  from experimaestro.compat import cached_property
6
- import regex
8
+ import re
7
9
  from experimaestro.scheduler import JobState
8
10
 
9
11
 
10
12
  class JobInformation:
11
- def __init__(self, path: Path, scriptname: str):
13
+ def __init__(self, path: Path, scriptname: str, check: bool = False):
12
14
  self.path = path
13
15
  self.scriptname = scriptname
16
+ self.check = check
14
17
 
15
18
  @cached_property
16
19
  def params(self):
17
- return json.loads((self.path / "params.json").read_text())
20
+ try:
21
+ return json.loads((self.path / "params.json").read_text())
22
+ except Exception:
23
+ logging.warning("Could not load params.json in %s", self.path)
24
+ return {"tags": {}}
18
25
 
19
26
  @cached_property
20
27
  def tags(self) -> List[str]:
@@ -22,12 +29,19 @@ class JobInformation:
22
29
 
23
30
  @cached_property
24
31
  def state(self) -> Optional[JobState]:
25
- if (self.path / f"{self.scriptname}.pid").is_file():
26
- return JobState.RUNNING
27
- elif (self.path / f"{self.scriptname}.done").is_file():
32
+ if (self.path / f"{self.scriptname}.done").is_file():
28
33
  return JobState.DONE
29
- elif (self.path / f"{self.scriptname}.failed").is_file():
34
+ if (self.path / f"{self.scriptname}.failed").is_file():
30
35
  return JobState.ERROR
36
+ if (self.path / f"{self.scriptname}.pid").is_file():
37
+ if self.check:
38
+ if process := self.getprocess():
39
+ state = asyncio.run(process.aio_state(0))
40
+ if state is None or state.finished:
41
+ return JobState.ERROR
42
+ else:
43
+ return JobState.ERROR
44
+ return JobState.RUNNING
31
45
  else:
32
46
  return None
33
47
 
@@ -87,7 +101,7 @@ class NotInExpr(BaseInExpr):
87
101
  class RegexExpr:
88
102
  def __init__(self, tokens):
89
103
  self.var, expr = tokens
90
- self.regex = regex.compile(expr)
104
+ self.regex = re.compile(expr)
91
105
 
92
106
  def __repr__(self):
93
107
  return f"""REGEX[{self.varname}, {self.value}]"""
@@ -103,7 +117,7 @@ class RegexExpr:
103
117
  if not value:
104
118
  return False
105
119
 
106
- return self.regex.match(value)
120
+ return self.re.match(value)
107
121
 
108
122
 
109
123
  class ConstantString:
@@ -0,0 +1,268 @@
1
+ # flake8: noqa: T201
2
+ import asyncio
3
+ import subprocess
4
+ from typing import Optional
5
+ from shutil import rmtree
6
+ import click
7
+ from pathlib import Path
8
+ from termcolor import colored, cprint
9
+
10
+ from experimaestro.settings import find_workspace
11
+ from . import check_xp_path, cli
12
+
13
+
14
+ @click.option("--workspace", default="", help="Experimaestro workspace")
15
+ @click.option("--workdir", type=Path, default=None)
16
+ @cli.group()
17
+ @click.pass_context
18
+ def jobs(
19
+ ctx,
20
+ workdir: Optional[Path],
21
+ workspace: Optional[str],
22
+ ):
23
+ """Job control: list, kill and clean
24
+
25
+ The job filter is a boolean expression where tags (alphanumeric)
26
+ and special job information (@state for job state, @name for job full
27
+ name) can be compared to a given value (using '~' for regex matching,
28
+ '=', 'not in', or 'in')
29
+
30
+ For instance,
31
+
32
+ model = "bm25" and mode in ["a", b"] and @state = "RUNNING"
33
+
34
+ selects jobs where the tag model is "bm25", the tag mode is either
35
+ "a" or "b", and the state is running.
36
+
37
+ """
38
+ ws = ctx.obj.workspace = find_workspace(workdir=workdir, workspace=workspace)
39
+ check_xp_path(ctx, None, ws.path)
40
+
41
+
42
+ def process(
43
+ workspace,
44
+ *,
45
+ experiment="",
46
+ tags="",
47
+ ready=False,
48
+ clean=False,
49
+ kill=False,
50
+ filter="",
51
+ perform=False,
52
+ fullpath=False,
53
+ check=False,
54
+ ):
55
+ from .filter import createFilter, JobInformation
56
+ from experimaestro.scheduler import JobState
57
+
58
+ _filter = createFilter(filter) if filter else lambda x: True
59
+
60
+ # Get all jobs from experiments
61
+ job2xp = {}
62
+
63
+ path = workspace.path
64
+ for p in (path / "xp").glob("*"):
65
+ for job in p.glob("jobs/*/*"):
66
+ job_path = job.resolve()
67
+ if job_path.is_dir():
68
+ job2xp.setdefault(job_path.name, set()).add(p.name)
69
+
70
+ if (p / "jobs.bak").is_dir():
71
+ cprint(f" Experiment {p.name} has not finished yet", "red")
72
+ if (not perform) and (kill or clean):
73
+ cprint(
74
+ " Preventing kill/clean (use --perform if you want to)", "yellow"
75
+ )
76
+ kill = False
77
+ clean = False
78
+
79
+ # Now, process jobs
80
+ for job in path.glob("jobs/*/*"):
81
+ info = None
82
+ p = job.resolve()
83
+ if p.is_dir():
84
+ *_, scriptname = p.parent.name.rsplit(".", 1)
85
+ xps = job2xp.get(job.name, set())
86
+ if experiment and experiment not in xps:
87
+ continue
88
+
89
+ info = JobInformation(p, scriptname, check=check)
90
+ job_str = (
91
+ (str(job.resolve()) if fullpath else f"{job.parent.name}/{job.name}")
92
+ + " "
93
+ + ",".join(xps)
94
+ )
95
+
96
+ if filter:
97
+ if not _filter(info):
98
+ continue
99
+
100
+ if info.state is None:
101
+ print(colored(f"NODIR {job_str}", "red"), end="")
102
+ elif info.state.running():
103
+ if kill:
104
+ if perform:
105
+ process = info.getprocess()
106
+ if process is None:
107
+ cprint(
108
+ "internal error – no process could be retrieved",
109
+ "red",
110
+ )
111
+ else:
112
+ cprint(f"KILLING {process}", "light_red")
113
+ process.kill()
114
+ else:
115
+ print("KILLING (not performing)", process)
116
+ print(
117
+ colored(f"{info.state.name:8}{job_str}", "yellow"),
118
+ end="",
119
+ )
120
+ elif info.state == JobState.DONE:
121
+ print(
122
+ colored(f"DONE {job_str}", "green"),
123
+ end="",
124
+ )
125
+ elif info.state == JobState.ERROR:
126
+ print(colored(f"FAIL {job_str}", "red"), end="")
127
+ else:
128
+ print(
129
+ colored(f"{info.state.name:8}{job_str}", "red"),
130
+ end="",
131
+ )
132
+
133
+ else:
134
+ if not ready:
135
+ continue
136
+ print(colored(f"READY {job_path}", "yellow"), end="")
137
+
138
+ if tags:
139
+ print(f""" {" ".join(f"{k}={v}" for k, v in info.tags.items())}""")
140
+ else:
141
+ print()
142
+
143
+ if clean and info.state and info.state.finished():
144
+ if perform:
145
+ cprint("Cleaning...", "red")
146
+ rmtree(p)
147
+ else:
148
+ cprint("Cleaning... (not performed)", "red")
149
+ print()
150
+
151
+
152
+ @click.option("--experiment", default=None, help="Restrict to this experiment")
153
+ @click.option("--tags", is_flag=True, help="Show tags")
154
+ @click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
155
+ @click.option("--filter", default="", help="Filter expression")
156
+ @click.option("--fullpath", is_flag=True, help="Prints full paths")
157
+ @click.option("--no-check", is_flag=True, help="Check that running jobs")
158
+ @jobs.command()
159
+ @click.pass_context
160
+ def list(
161
+ ctx,
162
+ experiment: str,
163
+ filter: str,
164
+ tags: bool,
165
+ ready: bool,
166
+ fullpath: bool,
167
+ no_check: bool,
168
+ ):
169
+ process(
170
+ ctx.obj.workspace,
171
+ experiment=experiment,
172
+ filter=filter,
173
+ tags=tags,
174
+ ready=ready,
175
+ fullpath=fullpath,
176
+ check=not no_check,
177
+ )
178
+
179
+
180
+ @click.option("--experiment", default=None, help="Restrict to this experiment")
181
+ @click.option("--tags", is_flag=True, help="Show tags")
182
+ @click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
183
+ @click.option("--filter", default="", help="Filter expression")
184
+ @click.option("--perform", is_flag=True, help="Really perform the killing")
185
+ @click.option("--fullpath", is_flag=True, help="Prints full paths")
186
+ @jobs.command()
187
+ @click.pass_context
188
+ def kill(
189
+ ctx,
190
+ experiment: str,
191
+ filter: str,
192
+ tags: bool,
193
+ ready: bool,
194
+ fullpath: bool,
195
+ perform: bool,
196
+ check: bool,
197
+ ):
198
+ process(
199
+ ctx.obj.workspace,
200
+ experiment=experiment,
201
+ filter=filter,
202
+ tags=tags,
203
+ ready=ready,
204
+ kill=True,
205
+ perform=perform,
206
+ fullpath=fullpath,
207
+ )
208
+
209
+
210
+ @click.option("--experiment", default=None, help="Restrict to this experiment")
211
+ @click.option("--tags", is_flag=True, help="Show tags")
212
+ @click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
213
+ @click.option("--filter", default="", help="Filter expression")
214
+ @click.option("--perform", is_flag=True, help="Really perform the cleaning")
215
+ @click.option("--fullpath", is_flag=True, help="Prints full paths")
216
+ @jobs.command()
217
+ @click.pass_context
218
+ def clean(
219
+ ctx,
220
+ experiment: str,
221
+ filter: str,
222
+ tags: bool,
223
+ ready: bool,
224
+ fullpath: bool,
225
+ perform: bool,
226
+ ):
227
+ process(
228
+ ctx.obj.workspace,
229
+ experiment=experiment,
230
+ filter=filter,
231
+ tags=tags,
232
+ ready=ready,
233
+ clean=True,
234
+ perform=perform,
235
+ fullpath=fullpath,
236
+ )
237
+
238
+
239
+ @click.argument("jobid", type=str)
240
+ @click.option(
241
+ "--follow", "-f", help="Use tail instead of less to follow changes", is_flag=True
242
+ )
243
+ @click.option("--std", help="Follow stdout instead of stderr", is_flag=True)
244
+ @jobs.command()
245
+ @click.pass_context
246
+ def log(ctx, jobid: str, follow: bool, std: bool):
247
+ task_name, task_hash = jobid.split("/")
248
+ _, name = task_name.rsplit(".", 1)
249
+ path = (
250
+ ctx.obj.workspace.path
251
+ / "jobs"
252
+ / task_name
253
+ / task_hash
254
+ / f"""{name}.{'out' if std else 'err'}"""
255
+ )
256
+ if follow:
257
+ subprocess.run(["tail", "-f", path])
258
+ else:
259
+ subprocess.run(["less", "-r", path])
260
+
261
+
262
+ @click.argument("jobid", type=str)
263
+ @jobs.command()
264
+ @click.pass_context
265
+ def path(ctx, jobid: str):
266
+ task_name, task_hash = jobid.split("/")
267
+ path = ctx.obj.workspace.path / "jobs" / task_name / task_hash
268
+ print(path)