experimaestro 1.5.6__py3-none-any.whl → 1.5.7__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.
- experimaestro/__main__.py +3 -423
- experimaestro/cli/__init__.py +312 -0
- experimaestro/{filter.py → cli/filter.py} +4 -4
- experimaestro/cli/jobs.py +245 -0
- experimaestro/core/objects.py +6 -3
- experimaestro/experiments/cli.py +3 -22
- experimaestro/launchers/slurm/base.py +3 -1
- experimaestro/notifications.py +24 -8
- experimaestro/run.py +0 -1
- experimaestro/scheduler/base.py +0 -5
- experimaestro/settings.py +29 -1
- experimaestro/tests/test_tags.py +35 -0
- {experimaestro-1.5.6.dist-info → experimaestro-1.5.7.dist-info}/METADATA +1 -1
- {experimaestro-1.5.6.dist-info → experimaestro-1.5.7.dist-info}/RECORD +17 -15
- {experimaestro-1.5.6.dist-info → experimaestro-1.5.7.dist-info}/LICENSE +0 -0
- {experimaestro-1.5.6.dist-info → experimaestro-1.5.7.dist-info}/WHEEL +0 -0
- {experimaestro-1.5.6.dist-info → experimaestro-1.5.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
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 pass_cfg(f):
|
|
24
|
+
"""Pass configuration information"""
|
|
25
|
+
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def new_func(ctx, *args, **kwargs):
|
|
28
|
+
return ctx.invoke(f, ctx.obj, *args, **kwargs)
|
|
29
|
+
|
|
30
|
+
return update_wrapper(new_func, f)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_xp_path(ctx, self, path: Path):
|
|
34
|
+
if not (path / ".__experimaestro__").is_file():
|
|
35
|
+
cprint(f"{path} is not an experimaestro working directory", "red")
|
|
36
|
+
for path in path.parents:
|
|
37
|
+
if (path / ".__experimaestro__").is_file():
|
|
38
|
+
cprint(f"{path} could be the folder you want", "green")
|
|
39
|
+
if click.confirm("Do you want to use this folder?"):
|
|
40
|
+
return path
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RunConfig:
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self.traceback = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pass_cfg(f):
|
|
52
|
+
"""Pass configuration information"""
|
|
53
|
+
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def new_func(ctx, *args, **kwargs):
|
|
56
|
+
return ctx.invoke(f, ctx.obj, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return update_wrapper(new_func, f)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.group()
|
|
62
|
+
@click.option("--quiet", is_flag=True, help="Be quiet")
|
|
63
|
+
@click.option("--debug", is_flag=True, help="Be even more verbose (implies traceback)")
|
|
64
|
+
@click.option(
|
|
65
|
+
"--traceback", is_flag=True, help="Display traceback if an exception occurs"
|
|
66
|
+
)
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def cli(ctx, quiet, debug, traceback):
|
|
69
|
+
if quiet:
|
|
70
|
+
logging.getLogger().setLevel(logging.WARN)
|
|
71
|
+
elif debug:
|
|
72
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
73
|
+
|
|
74
|
+
ctx.obj = RunConfig()
|
|
75
|
+
ctx.obj.traceback = traceback
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Adds the run-experiment command
|
|
79
|
+
cli.add_command(experiments_cli, "run-experiment")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cli.command(help="Get version")
|
|
83
|
+
def version():
|
|
84
|
+
print(experimaestro.__version__)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@click.argument("parameters", type=Path)
|
|
88
|
+
@cli.command(context_settings={"allow_extra_args": True})
|
|
89
|
+
def run(parameters):
|
|
90
|
+
"""Run a task"""
|
|
91
|
+
|
|
92
|
+
from experimaestro.run import run as do_run
|
|
93
|
+
|
|
94
|
+
do_run(parameters)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@click.argument("path2", type=Path)
|
|
98
|
+
@click.argument("path1", type=Path)
|
|
99
|
+
@cli.command(context_settings={"allow_extra_args": True})
|
|
100
|
+
def parameters_difference(path1, path2):
|
|
101
|
+
"""Compute the difference between two configurations"""
|
|
102
|
+
|
|
103
|
+
from experimaestro.tools.diff import diff
|
|
104
|
+
|
|
105
|
+
diff(path1, path2)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@click.option(
|
|
109
|
+
"--clean", is_flag=True, help="Remove the socket file and its enclosing directory"
|
|
110
|
+
)
|
|
111
|
+
@click.argument("unix-path", type=Path)
|
|
112
|
+
@cli.command()
|
|
113
|
+
def rpyc_server(unix_path, clean):
|
|
114
|
+
"""Start an rPyC server"""
|
|
115
|
+
from experimaestro.rpyc import start_server
|
|
116
|
+
|
|
117
|
+
start_server(unix_path, clean=clean)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@cli.group()
|
|
121
|
+
def deprecated():
|
|
122
|
+
"""Manage identifier changes"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@click.option("--fix", is_flag=True, help="Generate links to new IDs")
|
|
127
|
+
@click.option("--cleanup", is_flag=True, help="Remove symbolic links and move folders")
|
|
128
|
+
@click.argument("path", type=Path, callback=check_xp_path)
|
|
129
|
+
@deprecated.command(name="list")
|
|
130
|
+
def deprecated_list(path: Path, fix: bool, cleanup: bool):
|
|
131
|
+
"""List deprecated jobs"""
|
|
132
|
+
from experimaestro.tools.jobs import fix_deprecated
|
|
133
|
+
|
|
134
|
+
if cleanup and not fix:
|
|
135
|
+
logging.warning("Ignoring --cleanup since we are not fixing old IDs")
|
|
136
|
+
fix_deprecated(path, fix, cleanup)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@click.argument("path", type=Path, callback=check_xp_path)
|
|
140
|
+
@deprecated.command()
|
|
141
|
+
def diff(path: Path):
|
|
142
|
+
"""Show the reason of the identifier change for a job"""
|
|
143
|
+
from experimaestro.tools.jobs import load_job
|
|
144
|
+
from experimaestro import Config
|
|
145
|
+
from experimaestro.core.objects import ConfigWalkContext
|
|
146
|
+
|
|
147
|
+
_, job = load_job(path / "params.json", discard_id=False)
|
|
148
|
+
_, new_job = load_job(path / "params.json")
|
|
149
|
+
|
|
150
|
+
def check(path: str, value, new_value, done: Set[int]):
|
|
151
|
+
if isinstance(value, Config):
|
|
152
|
+
if id(value) in done:
|
|
153
|
+
return
|
|
154
|
+
done.add(id(value))
|
|
155
|
+
|
|
156
|
+
old_id = value.__xpm__.identifier.all.hex()
|
|
157
|
+
new_id = new_value.__xpm__.identifier.all.hex()
|
|
158
|
+
|
|
159
|
+
if new_id != old_id:
|
|
160
|
+
print(f"{path} differ: {new_id} vs {old_id}")
|
|
161
|
+
|
|
162
|
+
for arg in value.__xpmtype__.arguments.values():
|
|
163
|
+
arg_value = getattr(value, arg.name)
|
|
164
|
+
arg_newvalue = getattr(new_value, arg.name)
|
|
165
|
+
check(f"{path}/{arg.name}", arg_value, arg_newvalue, done)
|
|
166
|
+
|
|
167
|
+
elif isinstance(value, list):
|
|
168
|
+
for ix, (array_value, array_newvalue) in enumerate(zip(value, new_value)):
|
|
169
|
+
check(f"{path}.{ix}", array_value, array_newvalue, done)
|
|
170
|
+
|
|
171
|
+
elif isinstance(value, dict):
|
|
172
|
+
for key, dict_value in value.items():
|
|
173
|
+
check(f"{path}.{key}", dict_value, new_value[key], done)
|
|
174
|
+
|
|
175
|
+
check(".", job, new_job, set())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@click.option("--show-all", is_flag=True, help="Show even not orphans")
|
|
179
|
+
@click.option(
|
|
180
|
+
"--ignore-old", is_flag=True, help="Ignore old jobs for unfinished experiments"
|
|
181
|
+
)
|
|
182
|
+
@click.option("--clean", is_flag=True, help="Prune the orphan folders")
|
|
183
|
+
@click.option("--size", is_flag=True, help="Show size of each folder")
|
|
184
|
+
@click.argument("path", type=Path, callback=check_xp_path)
|
|
185
|
+
@cli.command()
|
|
186
|
+
def orphans(path: Path, clean: bool, size: bool, show_all: bool, ignore_old: bool):
|
|
187
|
+
"""Check for tasks that are not part of an experimental plan"""
|
|
188
|
+
|
|
189
|
+
jobspath = path / "jobs"
|
|
190
|
+
|
|
191
|
+
def getjobs(path: Path):
|
|
192
|
+
return ((str(p.relative_to(path)), p) for p in path.glob("*/*") if p.is_dir())
|
|
193
|
+
|
|
194
|
+
def show(key: str, prefix=""):
|
|
195
|
+
if size:
|
|
196
|
+
print(
|
|
197
|
+
prefix,
|
|
198
|
+
subprocess.check_output(["du", "-hs", key], cwd=jobspath)
|
|
199
|
+
.decode("utf-8")
|
|
200
|
+
.strip(),
|
|
201
|
+
sep=None,
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
print(prefix, key, sep=None)
|
|
205
|
+
|
|
206
|
+
for p in (path / "xp").glob("*/jobs.bak"):
|
|
207
|
+
logging.warning("Experiment %s has not completed successfully", p.parent.name)
|
|
208
|
+
|
|
209
|
+
# Retrieve the jobs within expedriments (jobs and jobs.bak folder within experiments)
|
|
210
|
+
xpjobs = set()
|
|
211
|
+
if ignore_old:
|
|
212
|
+
paths = (path / "xp").glob("*/jobs")
|
|
213
|
+
else:
|
|
214
|
+
paths = chain((path / "xp").glob("*/jobs"), (path / "xp").glob("*/jobs.bak"))
|
|
215
|
+
|
|
216
|
+
for p in paths:
|
|
217
|
+
if p.is_dir():
|
|
218
|
+
for relpath, path in getjobs(p):
|
|
219
|
+
xpjobs.add(relpath)
|
|
220
|
+
|
|
221
|
+
# Now, look at stored jobs
|
|
222
|
+
found = 0
|
|
223
|
+
for key, jobpath in getjobs(jobspath):
|
|
224
|
+
if key not in xpjobs:
|
|
225
|
+
show(key)
|
|
226
|
+
if clean:
|
|
227
|
+
logging.info("Removing data in %s", jobpath)
|
|
228
|
+
rmtree(jobpath)
|
|
229
|
+
else:
|
|
230
|
+
if show_all:
|
|
231
|
+
show(key, prefix="[not orphan] ")
|
|
232
|
+
found += 1
|
|
233
|
+
|
|
234
|
+
print(f"{found} jobs are not orphans")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def arg_split(ctx, param, value):
|
|
238
|
+
# split columns by ',' and remove whitespace
|
|
239
|
+
return set(c.strip() for c in value.split(","))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@click.option("--skip", default=set(), callback=arg_split)
|
|
243
|
+
@click.argument("package", type=str)
|
|
244
|
+
@click.argument("objects", type=Path)
|
|
245
|
+
@cli.command()
|
|
246
|
+
def check_documentation(objects, package, skip):
|
|
247
|
+
"""Check that all the configuration and tasks are documented within a
|
|
248
|
+
package, relying on the sphinx objects.inv file"""
|
|
249
|
+
from experimaestro.tools.documentation import documented_from_objects, undocumented
|
|
250
|
+
|
|
251
|
+
documented = documented_from_objects(objects)
|
|
252
|
+
errors, configs = undocumented([package], documented, skip)
|
|
253
|
+
for config in configs:
|
|
254
|
+
cprint(f"{config.__module__}.{config.__qualname__}", "red")
|
|
255
|
+
|
|
256
|
+
if errors > 0 or configs:
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@click.option("--config", type=Path, help="Show size of each folder")
|
|
261
|
+
@click.argument("spec", type=str)
|
|
262
|
+
@cli.command()
|
|
263
|
+
def find_launchers(config: Optional[Path], spec: str):
|
|
264
|
+
"""Find launchers matching a specification"""
|
|
265
|
+
if config is not None:
|
|
266
|
+
launcher_registry.LauncherRegistry.set_config_dir(config)
|
|
267
|
+
|
|
268
|
+
print(launcher_registry.find_launcher(spec))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class Launchers(click.MultiCommand):
|
|
272
|
+
"""Connectors commands"""
|
|
273
|
+
|
|
274
|
+
@cached_property
|
|
275
|
+
def commands(self):
|
|
276
|
+
map = {}
|
|
277
|
+
for ep in pkg_resources.iter_entry_points(f"experimaestro.{self.name}"):
|
|
278
|
+
if get_cli := getattr(ep.load(), "get_cli", None):
|
|
279
|
+
map[ep.name] = get_cli()
|
|
280
|
+
return map
|
|
281
|
+
|
|
282
|
+
def list_commands(self, ctx):
|
|
283
|
+
return self.commands.keys()
|
|
284
|
+
|
|
285
|
+
def get_command(self, ctx, name):
|
|
286
|
+
return self.commands[name]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
cli.add_command(Launchers("launchers", help="Launcher specific commands"))
|
|
290
|
+
cli.add_command(Launchers("connectors", help="Connector specific commands"))
|
|
291
|
+
cli.add_command(Launchers("tokens", help="Token specific commands"))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@cli.group()
|
|
295
|
+
@click.option("--workdir", type=Path, default=None)
|
|
296
|
+
@click.option("--workspace", type=str, default=None)
|
|
297
|
+
@click.pass_context
|
|
298
|
+
def experiments(ctx, workdir, workspace):
|
|
299
|
+
"""Manage experiments"""
|
|
300
|
+
ws = find_workspace(workdir=workdir, workspace=workspace)
|
|
301
|
+
path = check_xp_path(None, None, ws.path)
|
|
302
|
+
ctx.obj = path
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@experiments.command()
|
|
306
|
+
@pass_cfg
|
|
307
|
+
def list(workdir: Path):
|
|
308
|
+
for p in (workdir / "xp").iterdir():
|
|
309
|
+
if (p / "jobs.bak").exists():
|
|
310
|
+
cprint(f"[unfinished] {p.name}", "yellow")
|
|
311
|
+
else:
|
|
312
|
+
cprint(p.name, "cyan")
|
|
@@ -22,12 +22,12 @@ class JobInformation:
|
|
|
22
22
|
|
|
23
23
|
@cached_property
|
|
24
24
|
def state(self) -> Optional[JobState]:
|
|
25
|
-
if (self.path / f"{self.scriptname}.
|
|
26
|
-
return JobState.RUNNING
|
|
27
|
-
elif (self.path / f"{self.scriptname}.done").is_file():
|
|
25
|
+
if (self.path / f"{self.scriptname}.done").is_file():
|
|
28
26
|
return JobState.DONE
|
|
29
|
-
|
|
27
|
+
if (self.path / f"{self.scriptname}.failed").is_file():
|
|
30
28
|
return JobState.ERROR
|
|
29
|
+
if (self.path / f"{self.scriptname}.pid").is_file():
|
|
30
|
+
return JobState.RUNNING
|
|
31
31
|
else:
|
|
32
32
|
return None
|
|
33
33
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# flake8: noqa: T201
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from shutil import rmtree
|
|
5
|
+
import click
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from termcolor import colored, cprint
|
|
8
|
+
|
|
9
|
+
from experimaestro.settings import find_workspace
|
|
10
|
+
from . import check_xp_path, cli
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.option("--workspace", default="", help="Experimaestro workspace")
|
|
14
|
+
@click.option("--workdir", type=Path, default=None)
|
|
15
|
+
@cli.group()
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def jobs(
|
|
18
|
+
ctx,
|
|
19
|
+
workdir: Optional[Path],
|
|
20
|
+
workspace: Optional[str],
|
|
21
|
+
):
|
|
22
|
+
"""Job control: list, kill and clean
|
|
23
|
+
|
|
24
|
+
The job filter is a boolean expression where tags (alphanumeric)
|
|
25
|
+
and special job information (@state for job state, @name for job full
|
|
26
|
+
name) can be compared to a given value (using '~' for regex matching,
|
|
27
|
+
'=', 'not in', or 'in')
|
|
28
|
+
|
|
29
|
+
For instance,
|
|
30
|
+
|
|
31
|
+
model = "bm25" and mode in ["a", b"] and @state = "RUNNING"
|
|
32
|
+
|
|
33
|
+
selects jobs where the tag model is "bm25", the tag mode is either
|
|
34
|
+
"a" or "b", and the state is running.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
ws = ctx.obj.workspace = find_workspace(workdir=workdir, workspace=workspace)
|
|
38
|
+
check_xp_path(ctx, None, ws.path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def process(
|
|
42
|
+
workspace,
|
|
43
|
+
*,
|
|
44
|
+
experiment="",
|
|
45
|
+
tags="",
|
|
46
|
+
ready=False,
|
|
47
|
+
clean=False,
|
|
48
|
+
kill=False,
|
|
49
|
+
filter="",
|
|
50
|
+
perform=False,
|
|
51
|
+
fullpath=False,
|
|
52
|
+
):
|
|
53
|
+
path = workspace.path
|
|
54
|
+
for p in (path / "xp").glob("*"):
|
|
55
|
+
if experiment and p.name != experiment:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
from .filter import createFilter, JobInformation
|
|
59
|
+
from experimaestro.scheduler import JobState
|
|
60
|
+
|
|
61
|
+
_filter = createFilter(filter) if filter else lambda x: True
|
|
62
|
+
|
|
63
|
+
print(f"* Experiment {p.name}")
|
|
64
|
+
if (p / "jobs.bak").is_dir():
|
|
65
|
+
cprint(" Experiment has not finished yet", "red")
|
|
66
|
+
if not perform and (kill or clean):
|
|
67
|
+
cprint(" Preventing kill/clean (use --force if you want to)", "yellow")
|
|
68
|
+
kill = False
|
|
69
|
+
clean = False
|
|
70
|
+
print()
|
|
71
|
+
|
|
72
|
+
for job in p.glob("jobs/*/*"):
|
|
73
|
+
info = None
|
|
74
|
+
p = job.resolve()
|
|
75
|
+
if p.is_dir():
|
|
76
|
+
*_, scriptname = p.parent.name.rsplit(".", 1)
|
|
77
|
+
|
|
78
|
+
info = JobInformation(p, scriptname)
|
|
79
|
+
job_path = (
|
|
80
|
+
str(job.resolve()) if fullpath else f"{job.parent.name}/{job.name}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if filter:
|
|
84
|
+
if not _filter(info):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if info.state is None:
|
|
88
|
+
print(colored(f"NODIR {job_path}", "red"), end="")
|
|
89
|
+
elif info.state.running():
|
|
90
|
+
if kill:
|
|
91
|
+
if perform:
|
|
92
|
+
process = info.getprocess()
|
|
93
|
+
print("KILLING", process)
|
|
94
|
+
process.kill()
|
|
95
|
+
else:
|
|
96
|
+
print("KILLING (not performing)", process)
|
|
97
|
+
print(
|
|
98
|
+
colored(f"{info.state.name:8}{job_path}", "yellow"),
|
|
99
|
+
end="",
|
|
100
|
+
)
|
|
101
|
+
elif info.state == JobState.DONE:
|
|
102
|
+
print(
|
|
103
|
+
colored(f"DONE {job_path}", "green"),
|
|
104
|
+
end="",
|
|
105
|
+
)
|
|
106
|
+
elif info.state == JobState.ERROR:
|
|
107
|
+
print(colored(f"FAIL {job_path}", "red"), end="")
|
|
108
|
+
else:
|
|
109
|
+
print(
|
|
110
|
+
colored(f"{info.state.name:8}{job_path}", "red"),
|
|
111
|
+
end="",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
if not ready:
|
|
116
|
+
continue
|
|
117
|
+
print(colored(f"READY {job_path}", "yellow"), end="")
|
|
118
|
+
|
|
119
|
+
if tags:
|
|
120
|
+
print(f""" {" ".join(f"{k}={v}" for k, v in info.tags.items())}""")
|
|
121
|
+
else:
|
|
122
|
+
print()
|
|
123
|
+
|
|
124
|
+
if clean and info.state and info.state.finished():
|
|
125
|
+
if perform:
|
|
126
|
+
cprint("Cleaning...", "red")
|
|
127
|
+
rmtree(p)
|
|
128
|
+
else:
|
|
129
|
+
cprint("Cleaning... (not performed)", "red")
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@click.option("--experiment", default=None, help="Restrict to this experiment")
|
|
134
|
+
@click.option("--tags", is_flag=True, help="Show tags")
|
|
135
|
+
@click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
|
|
136
|
+
@click.option("--filter", default="", help="Filter expression")
|
|
137
|
+
@click.option("--fullpath", is_flag=True, help="Prints full paths")
|
|
138
|
+
@jobs.command()
|
|
139
|
+
@click.pass_context
|
|
140
|
+
def list(
|
|
141
|
+
ctx,
|
|
142
|
+
experiment: str,
|
|
143
|
+
filter: str,
|
|
144
|
+
tags: bool,
|
|
145
|
+
ready: bool,
|
|
146
|
+
fullpath: bool,
|
|
147
|
+
):
|
|
148
|
+
process(
|
|
149
|
+
ctx.obj.workspace,
|
|
150
|
+
experiment=experiment,
|
|
151
|
+
filter=filter,
|
|
152
|
+
tags=tags,
|
|
153
|
+
ready=ready,
|
|
154
|
+
fullpath=fullpath,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@click.option("--experiment", default=None, help="Restrict to this experiment")
|
|
159
|
+
@click.option("--tags", is_flag=True, help="Show tags")
|
|
160
|
+
@click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
|
|
161
|
+
@click.option("--filter", default="", help="Filter expression")
|
|
162
|
+
@click.option("--perform", is_flag=True, help="Really perform the killing")
|
|
163
|
+
@click.option("--fullpath", is_flag=True, help="Prints full paths")
|
|
164
|
+
@jobs.command()
|
|
165
|
+
@click.pass_context
|
|
166
|
+
def kill(
|
|
167
|
+
ctx,
|
|
168
|
+
experiment: str,
|
|
169
|
+
filter: str,
|
|
170
|
+
tags: bool,
|
|
171
|
+
ready: bool,
|
|
172
|
+
fullpath: bool,
|
|
173
|
+
perform: bool,
|
|
174
|
+
):
|
|
175
|
+
process(
|
|
176
|
+
ctx.obj.workspace,
|
|
177
|
+
experiment=experiment,
|
|
178
|
+
filter=filter,
|
|
179
|
+
tags=tags,
|
|
180
|
+
ready=ready,
|
|
181
|
+
kill=True,
|
|
182
|
+
perform=perform,
|
|
183
|
+
fullpath=fullpath,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@click.option("--experiment", default=None, help="Restrict to this experiment")
|
|
188
|
+
@click.option("--tags", is_flag=True, help="Show tags")
|
|
189
|
+
@click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
|
|
190
|
+
@click.option("--filter", default="", help="Filter expression")
|
|
191
|
+
@click.option("--perform", is_flag=True, help="Really perform the cleaning")
|
|
192
|
+
@click.option("--fullpath", is_flag=True, help="Prints full paths")
|
|
193
|
+
@jobs.command()
|
|
194
|
+
@click.pass_context
|
|
195
|
+
def clean(
|
|
196
|
+
ctx,
|
|
197
|
+
experiment: str,
|
|
198
|
+
filter: str,
|
|
199
|
+
tags: bool,
|
|
200
|
+
ready: bool,
|
|
201
|
+
fullpath: bool,
|
|
202
|
+
perform: bool,
|
|
203
|
+
):
|
|
204
|
+
process(
|
|
205
|
+
ctx.obj.workspace,
|
|
206
|
+
experiment=experiment,
|
|
207
|
+
filter=filter,
|
|
208
|
+
tags=tags,
|
|
209
|
+
ready=ready,
|
|
210
|
+
clean=True,
|
|
211
|
+
perform=perform,
|
|
212
|
+
fullpath=fullpath,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@click.argument("jobid", type=str)
|
|
217
|
+
@click.option(
|
|
218
|
+
"--follow", "-f", help="Use tail instead of less to follow changes", is_flag=True
|
|
219
|
+
)
|
|
220
|
+
@click.option("--std", help="Follow stdout instead of stderr", is_flag=True)
|
|
221
|
+
@jobs.command()
|
|
222
|
+
@click.pass_context
|
|
223
|
+
def log(ctx, jobid: str, follow: bool, std: bool):
|
|
224
|
+
task_name, task_hash = jobid.split("/")
|
|
225
|
+
_, name = task_name.rsplit(".", 1)
|
|
226
|
+
path = (
|
|
227
|
+
ctx.obj.workspace.path
|
|
228
|
+
/ "jobs"
|
|
229
|
+
/ task_name
|
|
230
|
+
/ task_hash
|
|
231
|
+
/ f"""{name}.{'out' if std else 'err'}"""
|
|
232
|
+
)
|
|
233
|
+
if follow:
|
|
234
|
+
subprocess.run(["tail", "-f", path])
|
|
235
|
+
else:
|
|
236
|
+
subprocess.run(["less", "-r", path])
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@click.argument("jobid", type=str)
|
|
240
|
+
@jobs.command()
|
|
241
|
+
@click.pass_context
|
|
242
|
+
def path(ctx, jobid: str):
|
|
243
|
+
task_name, task_hash = jobid.split("/")
|
|
244
|
+
path = ctx.obj.workspace.path / "jobs" / task_name / task_hash
|
|
245
|
+
print(path)
|
experimaestro/core/objects.py
CHANGED
|
@@ -916,14 +916,14 @@ class ConfigInformation:
|
|
|
916
916
|
|
|
917
917
|
# --- Submit the job
|
|
918
918
|
|
|
919
|
+
# Sets the init tasks
|
|
920
|
+
self.init_tasks = init_tasks
|
|
921
|
+
|
|
919
922
|
# Creates a new job
|
|
920
923
|
self.job = self.xpmtype.task(
|
|
921
924
|
self.pyobject, launcher=launcher, workspace=workspace, run_mode=run_mode
|
|
922
925
|
)
|
|
923
926
|
|
|
924
|
-
# Sets the init tasks
|
|
925
|
-
self.init_tasks = init_tasks
|
|
926
|
-
|
|
927
927
|
# Validate the object
|
|
928
928
|
job_context = JobContext(self.job)
|
|
929
929
|
self.validate_and_seal(job_context)
|
|
@@ -979,6 +979,9 @@ class ConfigInformation:
|
|
|
979
979
|
elif self.job.failedpath.is_file():
|
|
980
980
|
color = "light_red"
|
|
981
981
|
cprint(f"[failed] {s}", color, file=sys.stderr)
|
|
982
|
+
elif self.job.pidpath.is_file():
|
|
983
|
+
color = "blue"
|
|
984
|
+
cprint(f"[running] {s}", color, file=sys.stderr)
|
|
982
985
|
else:
|
|
983
986
|
color = "light_blue"
|
|
984
987
|
cprint(f"[not run] {s}", color, file=sys.stderr)
|
experimaestro/experiments/cli.py
CHANGED
|
@@ -11,7 +11,7 @@ import yaml
|
|
|
11
11
|
from experimaestro import LauncherRegistry, RunMode, experiment
|
|
12
12
|
from experimaestro.experiments.configuration import ConfigurationBase
|
|
13
13
|
from experimaestro.exceptions import HandledException
|
|
14
|
-
from experimaestro.settings import
|
|
14
|
+
from experimaestro.settings import find_workspace
|
|
15
15
|
from omegaconf import OmegaConf, SCMode
|
|
16
16
|
from termcolor import cprint
|
|
17
17
|
|
|
@@ -231,27 +231,8 @@ def experiments_cli( # noqa: C901
|
|
|
231
231
|
)
|
|
232
232
|
|
|
233
233
|
# Define the workspace
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if workspace:
|
|
237
|
-
ws_env = get_workspace(workspace)
|
|
238
|
-
if ws_env is None:
|
|
239
|
-
raise RuntimeError("No workspace named %s", workspace)
|
|
240
|
-
|
|
241
|
-
logging.info("Using workspace %s", ws_env.id)
|
|
242
|
-
if workdir:
|
|
243
|
-
# Overrides working directory
|
|
244
|
-
logging.info(" override working directory: %s", workdir)
|
|
245
|
-
ws_env.path = workdir
|
|
246
|
-
else:
|
|
247
|
-
workdir = ws_env.path
|
|
248
|
-
elif workdir:
|
|
249
|
-
logging.info("Using workdir %s", workdir)
|
|
250
|
-
ws_env = workdir
|
|
251
|
-
else:
|
|
252
|
-
ws_env = get_workspace()
|
|
253
|
-
assert ws_env is not None, "No workdir or workspace defined, and no default"
|
|
254
|
-
logging.info("Using default workspace %s", ws_env.id)
|
|
234
|
+
ws_env = find_workspace(workdir=workdir, workspace=workspace)
|
|
235
|
+
workdir = ws_env.path
|
|
255
236
|
|
|
256
237
|
logging.info("Using working directory %s", str(workdir.resolve()))
|
|
257
238
|
|
|
@@ -262,7 +262,9 @@ class SlurmProcessBuilder(ProcessBuilder):
|
|
|
262
262
|
addstream(builder.command, "-i", self.stdin)
|
|
263
263
|
|
|
264
264
|
builder.command.extend(self.command)
|
|
265
|
-
logger.info(
|
|
265
|
+
logger.info(
|
|
266
|
+
"slurm sbatch command: %s", " ".join(f'"{s}"' for s in builder.command)
|
|
267
|
+
)
|
|
266
268
|
handler = OutputCaptureHandler()
|
|
267
269
|
builder.stdout = Redirect.pipe(handler)
|
|
268
270
|
builder.stderr = Redirect.inherit()
|