experimaestro 1.5.4__py3-none-any.whl → 1.6.0__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 (62) hide show
  1. experimaestro/__init__.py +0 -1
  2. experimaestro/__main__.py +3 -423
  3. experimaestro/cli/__init__.py +312 -0
  4. experimaestro/{filter.py → cli/filter.py} +4 -4
  5. experimaestro/cli/jobs.py +261 -0
  6. experimaestro/click.py +0 -35
  7. experimaestro/connectors/ssh.py +26 -7
  8. experimaestro/core/objects.py +13 -6
  9. experimaestro/core/types.py +8 -3
  10. experimaestro/experiments/cli.py +97 -63
  11. experimaestro/experiments/configuration.py +7 -1
  12. experimaestro/launcherfinder/__init__.py +1 -1
  13. experimaestro/launcherfinder/base.py +2 -18
  14. experimaestro/launcherfinder/registry.py +22 -129
  15. experimaestro/launchers/direct.py +0 -47
  16. experimaestro/launchers/slurm/base.py +3 -1
  17. experimaestro/notifications.py +24 -8
  18. experimaestro/run.py +21 -3
  19. experimaestro/scheduler/base.py +26 -15
  20. experimaestro/scheduler/workspace.py +26 -8
  21. experimaestro/scriptbuilder.py +5 -1
  22. experimaestro/settings.py +43 -5
  23. experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
  24. experimaestro/tests/test_findlauncher.py +1 -1
  25. experimaestro/tests/test_ssh.py +7 -0
  26. experimaestro/tests/test_tags.py +35 -0
  27. experimaestro/tokens.py +8 -8
  28. experimaestro/utils/resources.py +5 -1
  29. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/METADATA +4 -5
  30. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/RECORD +33 -59
  31. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/WHEEL +1 -1
  32. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/entry_points.txt +0 -4
  33. experimaestro/launchers/slurm/cli.py +0 -29
  34. experimaestro/launchers/slurm/configuration.py +0 -597
  35. experimaestro/scheduler/environment.py +0 -94
  36. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  37. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  38. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  39. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  40. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  41. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  42. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  43. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  44. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  45. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  46. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  47. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  48. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  49. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  50. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  51. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  52. experimaestro/server/data/favicon.ico +0 -0
  53. experimaestro/server/data/index.css +0 -22844
  54. experimaestro/server/data/index.css.map +0 -1
  55. experimaestro/server/data/index.html +0 -27
  56. experimaestro/server/data/index.js +0 -100947
  57. experimaestro/server/data/index.js.map +0 -1
  58. experimaestro/server/data/login.html +0 -22
  59. experimaestro/server/data/manifest.json +0 -15
  60. experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
  61. experimaestro/utils/yaml.py +0 -202
  62. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/LICENSE +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}.pid").is_file():
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
- elif (self.path / f"{self.scriptname}.failed").is_file():
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,261 @@
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
+ from .filter import createFilter, JobInformation
54
+ from experimaestro.scheduler import JobState
55
+
56
+ _filter = createFilter(filter) if filter else lambda x: True
57
+
58
+ # Get all jobs from experiments
59
+ job2xp = {}
60
+
61
+ path = workspace.path
62
+ for p in (path / "xp").glob("*"):
63
+ for job in p.glob("jobs/*/*"):
64
+ job_path = job.resolve()
65
+ if job_path.is_dir():
66
+ *_, scriptname = job_path.parent.name.rsplit(".", 1)
67
+ job2xp.setdefault(scriptname, set()).add(p.name)
68
+
69
+ if (p / "jobs.bak").is_dir():
70
+ cprint(f" Experiment {p.name} has not finished yet", "red")
71
+ if (not perform) and (kill or clean):
72
+ cprint(" Preventing kill/clean (use --force if you want to)", "yellow")
73
+ kill = False
74
+ clean = False
75
+
76
+ # Now, process jobs
77
+ for job in path.glob("jobs/*/*"):
78
+ info = None
79
+ p = job.resolve()
80
+ if p.is_dir():
81
+ *_, scriptname = p.parent.name.rsplit(".", 1)
82
+ xps = job2xp.get(scriptname, set())
83
+ if experiment and experiment not in xps:
84
+ continue
85
+
86
+ info = JobInformation(p, scriptname)
87
+ job_str = (
88
+ (str(job.resolve()) if fullpath else f"{job.parent.name}/{job.name}")
89
+ + " "
90
+ + ",".join(xps)
91
+ )
92
+
93
+ if filter:
94
+ if not _filter(info):
95
+ continue
96
+
97
+ if info.state is None:
98
+ print(colored(f"NODIR {job_str}", "red"), end="")
99
+ elif info.state.running():
100
+ if kill:
101
+ if perform:
102
+ process = info.getprocess()
103
+ if process is None:
104
+ cprint(
105
+ "internal error – no process could be retrieved",
106
+ "red",
107
+ )
108
+ else:
109
+ cprint(f"KILLING {process}", "light_red")
110
+ process.kill()
111
+ else:
112
+ print("KILLING (not performing)", process)
113
+ print(
114
+ colored(f"{info.state.name:8}{job_str}", "yellow"),
115
+ end="",
116
+ )
117
+ elif info.state == JobState.DONE:
118
+ print(
119
+ colored(f"DONE {job_str}", "green"),
120
+ end="",
121
+ )
122
+ elif info.state == JobState.ERROR:
123
+ print(colored(f"FAIL {job_str}", "red"), end="")
124
+ else:
125
+ print(
126
+ colored(f"{info.state.name:8}{job_str}", "red"),
127
+ end="",
128
+ )
129
+
130
+ else:
131
+ if not ready:
132
+ continue
133
+ print(colored(f"READY {job_path}", "yellow"), end="")
134
+
135
+ if tags:
136
+ print(f""" {" ".join(f"{k}={v}" for k, v in info.tags.items())}""")
137
+ else:
138
+ print()
139
+
140
+ if clean and info.state and info.state.finished():
141
+ if perform:
142
+ cprint("Cleaning...", "red")
143
+ rmtree(p)
144
+ else:
145
+ cprint("Cleaning... (not performed)", "red")
146
+ print()
147
+
148
+
149
+ @click.option("--experiment", default=None, help="Restrict to this experiment")
150
+ @click.option("--tags", is_flag=True, help="Show tags")
151
+ @click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
152
+ @click.option("--filter", default="", help="Filter expression")
153
+ @click.option("--fullpath", is_flag=True, help="Prints full paths")
154
+ @jobs.command()
155
+ @click.pass_context
156
+ def list(
157
+ ctx,
158
+ experiment: str,
159
+ filter: str,
160
+ tags: bool,
161
+ ready: bool,
162
+ fullpath: bool,
163
+ ):
164
+ process(
165
+ ctx.obj.workspace,
166
+ experiment=experiment,
167
+ filter=filter,
168
+ tags=tags,
169
+ ready=ready,
170
+ fullpath=fullpath,
171
+ )
172
+
173
+
174
+ @click.option("--experiment", default=None, help="Restrict to this experiment")
175
+ @click.option("--tags", is_flag=True, help="Show tags")
176
+ @click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
177
+ @click.option("--filter", default="", help="Filter expression")
178
+ @click.option("--perform", is_flag=True, help="Really perform the killing")
179
+ @click.option("--fullpath", is_flag=True, help="Prints full paths")
180
+ @jobs.command()
181
+ @click.pass_context
182
+ def kill(
183
+ ctx,
184
+ experiment: str,
185
+ filter: str,
186
+ tags: bool,
187
+ ready: bool,
188
+ fullpath: bool,
189
+ perform: bool,
190
+ ):
191
+ process(
192
+ ctx.obj.workspace,
193
+ experiment=experiment,
194
+ filter=filter,
195
+ tags=tags,
196
+ ready=ready,
197
+ kill=True,
198
+ perform=perform,
199
+ fullpath=fullpath,
200
+ )
201
+
202
+
203
+ @click.option("--experiment", default=None, help="Restrict to this experiment")
204
+ @click.option("--tags", is_flag=True, help="Show tags")
205
+ @click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
206
+ @click.option("--filter", default="", help="Filter expression")
207
+ @click.option("--perform", is_flag=True, help="Really perform the cleaning")
208
+ @click.option("--fullpath", is_flag=True, help="Prints full paths")
209
+ @jobs.command()
210
+ @click.pass_context
211
+ def clean(
212
+ ctx,
213
+ experiment: str,
214
+ filter: str,
215
+ tags: bool,
216
+ ready: bool,
217
+ fullpath: bool,
218
+ perform: bool,
219
+ ):
220
+ process(
221
+ ctx.obj.workspace,
222
+ experiment=experiment,
223
+ filter=filter,
224
+ tags=tags,
225
+ ready=ready,
226
+ clean=True,
227
+ perform=perform,
228
+ fullpath=fullpath,
229
+ )
230
+
231
+
232
+ @click.argument("jobid", type=str)
233
+ @click.option(
234
+ "--follow", "-f", help="Use tail instead of less to follow changes", is_flag=True
235
+ )
236
+ @click.option("--std", help="Follow stdout instead of stderr", is_flag=True)
237
+ @jobs.command()
238
+ @click.pass_context
239
+ def log(ctx, jobid: str, follow: bool, std: bool):
240
+ task_name, task_hash = jobid.split("/")
241
+ _, name = task_name.rsplit(".", 1)
242
+ path = (
243
+ ctx.obj.workspace.path
244
+ / "jobs"
245
+ / task_name
246
+ / task_hash
247
+ / f"""{name}.{'out' if std else 'err'}"""
248
+ )
249
+ if follow:
250
+ subprocess.run(["tail", "-f", path])
251
+ else:
252
+ subprocess.run(["less", "-r", path])
253
+
254
+
255
+ @click.argument("jobid", type=str)
256
+ @jobs.command()
257
+ @click.pass_context
258
+ def path(ctx, jobid: str):
259
+ task_name, task_hash = jobid.split("/")
260
+ path = ctx.obj.workspace.path / "jobs" / task_name / task_hash
261
+ print(path)
experimaestro/click.py CHANGED
@@ -1,6 +1,4 @@
1
1
  import click
2
- from experimaestro import Environment
3
- from experimaestro.run import parse_commandline
4
2
 
5
3
  """Defines the task command line argument prefix for experimaestro-handled command lines"""
6
4
 
@@ -45,36 +43,3 @@ class forwardoption(metaclass=forwardoptionMetaclass):
45
43
  def __getattr__(self, key):
46
44
  """Access to a class field"""
47
45
  return forwardoption([key])
48
-
49
-
50
- def environment(name: str):
51
- def annotate(f):
52
- def callback_env(ctx, name, value):
53
- if value:
54
- assert name not in ctx.params, "Environment has already been set"
55
- else:
56
- return ctx.params.get(name, None)
57
- return Environment.get(value)
58
-
59
- def callback(ctx, param, value):
60
- if value:
61
- if name not in ctx.params:
62
- ctx.params[name] = Environment()
63
- ctx.params[name].workdir = value
64
-
65
- f = click.option(
66
- f"--{name}-workdir",
67
- type=str,
68
- callback=callback,
69
- expose_value=False,
70
- help="Experimaestro environment",
71
- )(f)
72
- f = click.option(
73
- f"--{name}",
74
- type=str,
75
- callback=callback_env,
76
- help="Experimaestro environment",
77
- )(f)
78
- return f
79
-
80
- return annotate
@@ -1,12 +1,15 @@
1
+ try:
2
+ from pathlib import Path, _posix_flavour
3
+ except ImportError:
4
+ # Avoids problem with python 3.12 where this module does not work
5
+ # anyways
6
+ _posix_flavour = None
7
+
1
8
  from dataclasses import dataclass
2
- from pathlib import Path, _posix_flavour
3
9
  import io
4
10
  import os
5
11
  import re
6
- from experimaestro.launcherfinder import LauncherRegistry, YAMLDataClass
7
- from fabric import Connection
8
- from invoke import Promise
9
- import invoke.exceptions
12
+ from experimaestro.launcherfinder import LauncherRegistry
10
13
  from urllib.parse import urlparse
11
14
  from itertools import chain
12
15
  from . import Connector
@@ -19,6 +22,22 @@ from . import (
19
22
  from experimaestro.locking import Lock
20
23
  from experimaestro.tokens import Token
21
24
 
25
+ try:
26
+ from fabric import Connection
27
+ from invoke import Promise
28
+ from invoke.exceptions import Failure
29
+ except Exception:
30
+ # Just define placeholders
31
+ class Connection:
32
+ pass
33
+
34
+ class Promise:
35
+ pass
36
+
37
+ class Failure(Exception):
38
+ pass
39
+
40
+
22
41
  # Might be wise to switch to https://github.com/marian-code/ssh-utilities
23
42
 
24
43
 
@@ -132,7 +151,7 @@ class SshPath(Path):
132
151
 
133
152
 
134
153
  @dataclass
135
- class SshConfiguration(YAMLDataClass):
154
+ class SshConfiguration:
136
155
  hostname: str
137
156
 
138
157
  def create(self, registry: LauncherRegistry):
@@ -172,7 +191,7 @@ class SshProcess(Process):
172
191
  def wait(self) -> int:
173
192
  try:
174
193
  self.promise.join()
175
- except invoke.exceptions.Failure:
194
+ except Failure:
176
195
  raise
177
196
 
178
197