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.
- experimaestro/__init__.py +14 -4
- experimaestro/__main__.py +3 -423
- experimaestro/annotations.py +14 -4
- experimaestro/cli/__init__.py +311 -0
- experimaestro/{filter.py → cli/filter.py} +23 -9
- experimaestro/cli/jobs.py +268 -0
- experimaestro/cli/progress.py +269 -0
- experimaestro/click.py +0 -35
- experimaestro/commandline.py +3 -7
- experimaestro/connectors/__init__.py +29 -14
- experimaestro/connectors/local.py +19 -10
- experimaestro/connectors/ssh.py +27 -8
- experimaestro/core/arguments.py +45 -3
- experimaestro/core/callbacks.py +52 -0
- experimaestro/core/context.py +8 -9
- experimaestro/core/identifier.py +310 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +399 -772
- experimaestro/core/objects/config_utils.py +58 -0
- experimaestro/core/objects/config_walk.py +151 -0
- experimaestro/core/objects.pyi +15 -45
- experimaestro/core/serialization.py +63 -9
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/types.py +104 -66
- experimaestro/experiments/cli.py +154 -72
- experimaestro/experiments/configuration.py +10 -1
- experimaestro/generators.py +6 -1
- experimaestro/ipc.py +4 -1
- experimaestro/launcherfinder/__init__.py +1 -1
- experimaestro/launcherfinder/base.py +2 -18
- experimaestro/launcherfinder/parser.py +8 -3
- experimaestro/launcherfinder/registry.py +52 -140
- experimaestro/launcherfinder/specs.py +49 -10
- experimaestro/launchers/direct.py +0 -47
- experimaestro/launchers/slurm/base.py +54 -14
- experimaestro/mkdocs/__init__.py +1 -1
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/notifications.py +38 -12
- experimaestro/progress.py +406 -0
- experimaestro/run.py +24 -3
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +108 -808
- experimaestro/scheduler/dynamic_outputs.py +184 -0
- experimaestro/scheduler/experiment.py +387 -0
- experimaestro/scheduler/jobs.py +475 -0
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +75 -0
- experimaestro/scheduler/workspace.py +27 -8
- experimaestro/scriptbuilder.py +18 -3
- experimaestro/server/__init__.py +36 -5
- experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
- experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
- experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
- experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
- experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
- experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
- experimaestro/server/data/index.css +5187 -5068
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +68887 -68064
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +45 -5
- experimaestro/sphinx/__init__.py +7 -17
- experimaestro/taskglobals.py +7 -2
- experimaestro/tests/core/__init__.py +0 -0
- experimaestro/tests/core/test_generics.py +206 -0
- experimaestro/tests/definitions_types.py +5 -3
- experimaestro/tests/launchers/bin/sbatch +34 -7
- experimaestro/tests/launchers/bin/srun +5 -0
- experimaestro/tests/launchers/common.py +17 -5
- experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
- experimaestro/tests/restart.py +10 -5
- experimaestro/tests/tasks/all.py +23 -10
- experimaestro/tests/tasks/foreign.py +2 -4
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_experiment.py +73 -0
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_findlauncher.py +12 -5
- experimaestro/tests/test_forward.py +5 -5
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +182 -158
- experimaestro/tests/test_instance.py +19 -27
- experimaestro/tests/test_objects.py +13 -20
- experimaestro/tests/test_outputs.py +6 -6
- experimaestro/tests/test_param.py +68 -30
- experimaestro/tests/test_progress.py +4 -4
- experimaestro/tests/test_serializers.py +24 -64
- experimaestro/tests/test_ssh.py +7 -0
- experimaestro/tests/test_tags.py +50 -21
- experimaestro/tests/test_tasks.py +42 -51
- experimaestro/tests/test_tokens.py +11 -8
- experimaestro/tests/test_types.py +24 -21
- experimaestro/tests/test_validation.py +67 -110
- experimaestro/tests/token_reschedule.py +1 -1
- experimaestro/tokens.py +24 -13
- experimaestro/tools/diff.py +8 -1
- experimaestro/typingutils.py +20 -11
- experimaestro/utils/asyncio.py +6 -2
- experimaestro/utils/multiprocessing.py +44 -0
- experimaestro/utils/resources.py +11 -3
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/METADATA +28 -36
- experimaestro-2.0.0a8.dist-info/RECORD +166 -0
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/WHEEL +1 -1
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/entry_points.txt +0 -4
- experimaestro/launchers/slurm/cli.py +0 -29
- experimaestro/launchers/slurm/configuration.py +0 -597
- experimaestro/scheduler/environment.py +0 -94
- experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
- experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
- experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
- experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
- experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
- experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
- experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
- experimaestro/utils/yaml.py +0 -202
- experimaestro-1.5.1.dist-info/RECORD +0 -148
- {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
|
|
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
|
-
|
|
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}.
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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)
|