experimaestro 1.6.1__py3-none-any.whl → 1.15.2__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.
- experimaestro/__init__.py +14 -3
- experimaestro/annotations.py +13 -3
- experimaestro/cli/filter.py +19 -5
- experimaestro/cli/jobs.py +12 -5
- experimaestro/commandline.py +3 -7
- experimaestro/connectors/__init__.py +27 -12
- experimaestro/connectors/local.py +19 -10
- experimaestro/connectors/ssh.py +1 -1
- experimaestro/core/arguments.py +35 -3
- experimaestro/core/callbacks.py +52 -0
- experimaestro/core/context.py +8 -9
- experimaestro/core/identifier.py +301 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +364 -716
- 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 +61 -6
- experimaestro/experiments/cli.py +79 -29
- experimaestro/experiments/configuration.py +3 -0
- experimaestro/generators.py +6 -1
- experimaestro/ipc.py +4 -1
- experimaestro/launcherfinder/parser.py +8 -3
- experimaestro/launcherfinder/registry.py +29 -10
- experimaestro/launcherfinder/specs.py +49 -10
- experimaestro/launchers/slurm/base.py +51 -13
- experimaestro/mkdocs/__init__.py +1 -1
- experimaestro/notifications.py +2 -1
- experimaestro/run.py +3 -1
- experimaestro/scheduler/base.py +114 -6
- experimaestro/scheduler/dynamic_outputs.py +184 -0
- experimaestro/scheduler/state.py +75 -0
- experimaestro/scheduler/workspace.py +2 -1
- experimaestro/scriptbuilder.py +13 -2
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
- experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
- experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
- experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
- experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
- experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
- experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
- experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +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/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro/server/data/favicon.ico +0 -0
- experimaestro/server/data/index.css +22963 -0
- experimaestro/server/data/index.css.map +1 -0
- experimaestro/server/data/index.html +27 -0
- experimaestro/server/data/index.js +101770 -0
- experimaestro/server/data/index.js.map +1 -0
- experimaestro/server/data/login.html +22 -0
- experimaestro/server/data/manifest.json +15 -0
- experimaestro/settings.py +2 -2
- 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 +16 -4
- experimaestro/tests/restart.py +9 -4
- experimaestro/tests/tasks/all.py +23 -10
- experimaestro/tests/tasks/foreign.py +2 -4
- experimaestro/tests/test_dependencies.py +0 -6
- experimaestro/tests/test_experiment.py +73 -0
- experimaestro/tests/test_findlauncher.py +11 -4
- experimaestro/tests/test_forward.py +5 -5
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +114 -99
- experimaestro/tests/test_instance.py +6 -21
- experimaestro/tests/test_objects.py +20 -4
- experimaestro/tests/test_param.py +60 -22
- experimaestro/tests/test_serializers.py +24 -64
- experimaestro/tests/test_tags.py +5 -11
- experimaestro/tests/test_tasks.py +10 -23
- experimaestro/tests/test_tokens.py +3 -2
- experimaestro/tests/test_types.py +20 -17
- experimaestro/tests/test_validation.py +48 -91
- experimaestro/tokens.py +16 -5
- experimaestro/typingutils.py +8 -8
- experimaestro/utils/asyncio.py +6 -2
- experimaestro/utils/multiprocessing.py +44 -0
- experimaestro/utils/resources.py +7 -3
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/METADATA +27 -34
- experimaestro-1.15.2.dist-info/RECORD +159 -0
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/WHEEL +1 -1
- experimaestro-1.6.1.dist-info/RECORD +0 -122
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/entry_points.txt +0 -0
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info/licenses}/LICENSE +0 -0
experimaestro/__init__.py
CHANGED
|
@@ -27,7 +27,15 @@ from .annotations import (
|
|
|
27
27
|
# Method
|
|
28
28
|
config_only,
|
|
29
29
|
)
|
|
30
|
-
from .core.serialization import
|
|
30
|
+
from .core.serialization import (
|
|
31
|
+
load,
|
|
32
|
+
save,
|
|
33
|
+
state_dict,
|
|
34
|
+
from_state_dict,
|
|
35
|
+
from_task_dir,
|
|
36
|
+
serialize,
|
|
37
|
+
deserialize,
|
|
38
|
+
)
|
|
31
39
|
from .core.arguments import (
|
|
32
40
|
# Types
|
|
33
41
|
Param,
|
|
@@ -36,15 +44,17 @@ from .core.arguments import (
|
|
|
36
44
|
DataPath,
|
|
37
45
|
Annotated,
|
|
38
46
|
Constant,
|
|
47
|
+
field,
|
|
39
48
|
# Annotations helpers
|
|
40
49
|
help,
|
|
41
50
|
default,
|
|
42
51
|
)
|
|
43
|
-
from .generators import pathgenerator
|
|
52
|
+
from .generators import pathgenerator, PathGenerator
|
|
44
53
|
from .core.objects import (
|
|
45
54
|
Config,
|
|
46
55
|
copyconfig,
|
|
47
56
|
setmeta,
|
|
57
|
+
DependentMarker,
|
|
48
58
|
Task,
|
|
49
59
|
LightweightTask,
|
|
50
60
|
ObjectStore,
|
|
@@ -53,8 +63,9 @@ from .core.context import SerializationContext
|
|
|
53
63
|
from .core.serializers import SerializationLWTask, PathSerializationLWTask
|
|
54
64
|
from .core.types import Any, SubmitHook
|
|
55
65
|
from .launchers import Launcher
|
|
56
|
-
from .scheduler.workspace import Workspace, RunMode
|
|
57
66
|
from .scheduler import Scheduler, experiment, FailedExperiment
|
|
67
|
+
from .scheduler.workspace import Workspace, RunMode
|
|
68
|
+
from .scheduler.state import get_experiment
|
|
58
69
|
from .notifications import progress, tqdm
|
|
59
70
|
from .checkers import Choices
|
|
60
71
|
from .xpmutils import DirectoryContext
|
experimaestro/annotations.py
CHANGED
|
@@ -8,7 +8,7 @@ import experimaestro.core.objects as objects
|
|
|
8
8
|
import experimaestro.core.types as types
|
|
9
9
|
from experimaestro.generators import PathGenerator
|
|
10
10
|
|
|
11
|
-
from .core.arguments import Argument as CoreArgument
|
|
11
|
+
from .core.arguments import Argument as CoreArgument, field
|
|
12
12
|
from .core.objects import Config
|
|
13
13
|
from .core.types import Any, Identifier, TypeProxy, Type, ObjectType
|
|
14
14
|
from .utils import logger
|
|
@@ -134,12 +134,22 @@ class param:
|
|
|
134
134
|
self.type = Type.fromType(type) if type else None
|
|
135
135
|
self.help = help
|
|
136
136
|
self.ignored = ignored
|
|
137
|
-
self.default = default
|
|
138
137
|
self.required = required
|
|
139
|
-
self.generator = None
|
|
140
138
|
self.checker = checker
|
|
141
139
|
self.constant = constant
|
|
142
140
|
|
|
141
|
+
self.generator = None
|
|
142
|
+
self.default = None
|
|
143
|
+
|
|
144
|
+
# Set default or generator
|
|
145
|
+
if isinstance(default, field):
|
|
146
|
+
if default.default is not None:
|
|
147
|
+
self.default = default
|
|
148
|
+
elif default.default_factory is not None:
|
|
149
|
+
self.generator = default.default_factory
|
|
150
|
+
else:
|
|
151
|
+
self.default = default
|
|
152
|
+
|
|
143
153
|
def __call__(self, tp):
|
|
144
154
|
# Don't annotate in task mode
|
|
145
155
|
tp.__getxpmtype__().addAnnotation(self)
|
experimaestro/cli/filter.py
CHANGED
|
@@ -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]:
|
|
@@ -27,6 +34,13 @@ class JobInformation:
|
|
|
27
34
|
if (self.path / f"{self.scriptname}.failed").is_file():
|
|
28
35
|
return JobState.ERROR
|
|
29
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
|
|
30
44
|
return JobState.RUNNING
|
|
31
45
|
else:
|
|
32
46
|
return None
|
|
@@ -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:
|
experimaestro/cli/jobs.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# flake8: noqa: T201
|
|
2
|
+
import asyncio
|
|
2
3
|
import subprocess
|
|
3
4
|
from typing import Optional
|
|
4
5
|
from shutil import rmtree
|
|
@@ -49,6 +50,7 @@ def process(
|
|
|
49
50
|
filter="",
|
|
50
51
|
perform=False,
|
|
51
52
|
fullpath=False,
|
|
53
|
+
check=False,
|
|
52
54
|
):
|
|
53
55
|
from .filter import createFilter, JobInformation
|
|
54
56
|
from experimaestro.scheduler import JobState
|
|
@@ -63,13 +65,14 @@ def process(
|
|
|
63
65
|
for job in p.glob("jobs/*/*"):
|
|
64
66
|
job_path = job.resolve()
|
|
65
67
|
if job_path.is_dir():
|
|
66
|
-
|
|
67
|
-
job2xp.setdefault(scriptname, set()).add(p.name)
|
|
68
|
+
job2xp.setdefault(job_path.name, set()).add(p.name)
|
|
68
69
|
|
|
69
70
|
if (p / "jobs.bak").is_dir():
|
|
70
71
|
cprint(f" Experiment {p.name} has not finished yet", "red")
|
|
71
72
|
if (not perform) and (kill or clean):
|
|
72
|
-
cprint(
|
|
73
|
+
cprint(
|
|
74
|
+
" Preventing kill/clean (use --perform if you want to)", "yellow"
|
|
75
|
+
)
|
|
73
76
|
kill = False
|
|
74
77
|
clean = False
|
|
75
78
|
|
|
@@ -79,11 +82,11 @@ def process(
|
|
|
79
82
|
p = job.resolve()
|
|
80
83
|
if p.is_dir():
|
|
81
84
|
*_, scriptname = p.parent.name.rsplit(".", 1)
|
|
82
|
-
xps = job2xp.get(
|
|
85
|
+
xps = job2xp.get(job.name, set())
|
|
83
86
|
if experiment and experiment not in xps:
|
|
84
87
|
continue
|
|
85
88
|
|
|
86
|
-
info = JobInformation(p, scriptname)
|
|
89
|
+
info = JobInformation(p, scriptname, check=check)
|
|
87
90
|
job_str = (
|
|
88
91
|
(str(job.resolve()) if fullpath else f"{job.parent.name}/{job.name}")
|
|
89
92
|
+ " "
|
|
@@ -151,6 +154,7 @@ def process(
|
|
|
151
154
|
@click.option("--ready", is_flag=True, help="Include tasks which are not yet scheduled")
|
|
152
155
|
@click.option("--filter", default="", help="Filter expression")
|
|
153
156
|
@click.option("--fullpath", is_flag=True, help="Prints full paths")
|
|
157
|
+
@click.option("--no-check", is_flag=True, help="Check that running jobs")
|
|
154
158
|
@jobs.command()
|
|
155
159
|
@click.pass_context
|
|
156
160
|
def list(
|
|
@@ -160,6 +164,7 @@ def list(
|
|
|
160
164
|
tags: bool,
|
|
161
165
|
ready: bool,
|
|
162
166
|
fullpath: bool,
|
|
167
|
+
no_check: bool,
|
|
163
168
|
):
|
|
164
169
|
process(
|
|
165
170
|
ctx.obj.workspace,
|
|
@@ -168,6 +173,7 @@ def list(
|
|
|
168
173
|
tags=tags,
|
|
169
174
|
ready=ready,
|
|
170
175
|
fullpath=fullpath,
|
|
176
|
+
check=not no_check,
|
|
171
177
|
)
|
|
172
178
|
|
|
173
179
|
|
|
@@ -187,6 +193,7 @@ def kill(
|
|
|
187
193
|
ready: bool,
|
|
188
194
|
fullpath: bool,
|
|
189
195
|
perform: bool,
|
|
196
|
+
check: bool,
|
|
190
197
|
):
|
|
191
198
|
process(
|
|
192
199
|
ctx.obj.workspace,
|
experimaestro/commandline.py
CHANGED
|
@@ -276,12 +276,6 @@ class CommandLineJob(Job):
|
|
|
276
276
|
|
|
277
277
|
scriptbuilder = self.launcher.scriptbuilder()
|
|
278
278
|
self.path.mkdir(parents=True, exist_ok=True)
|
|
279
|
-
donepath = self.donepath
|
|
280
|
-
|
|
281
|
-
# Check again if done (now that we have locked)
|
|
282
|
-
if not overwrite and donepath.is_file():
|
|
283
|
-
logger.info("Job %s is already done", self)
|
|
284
|
-
return JobState.DONE
|
|
285
279
|
|
|
286
280
|
# Now we can write the script
|
|
287
281
|
scriptbuilder.lockfiles.append(self.lockpath)
|
|
@@ -293,15 +287,17 @@ class CommandLineJob(Job):
|
|
|
293
287
|
if self._process:
|
|
294
288
|
return self._process
|
|
295
289
|
|
|
290
|
+
# Prepare the files to be run
|
|
296
291
|
scriptPath = self.prepare()
|
|
297
292
|
|
|
293
|
+
# OK, now starts the process
|
|
298
294
|
logger.info("Starting job %s", self.jobpath)
|
|
299
295
|
processbuilder = self.launcher.processbuilder()
|
|
300
296
|
processbuilder.environ = self.environ
|
|
301
297
|
processbuilder.command.append(self.launcher.connector.resolve(scriptPath))
|
|
302
298
|
processbuilder.stderr = Redirect.file(self.stderr)
|
|
303
299
|
processbuilder.stdout = Redirect.file(self.stdout)
|
|
304
|
-
self._process = processbuilder.start()
|
|
300
|
+
self._process = processbuilder.start(True)
|
|
305
301
|
|
|
306
302
|
with self.pidpath.open("w") as fp:
|
|
307
303
|
json.dump(self._process.tospec(), fp)
|
|
@@ -9,7 +9,8 @@ This module contains :
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import enum
|
|
12
|
-
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict, Mapping, Type, Union, Optional
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from experimaestro.utils import logger
|
|
15
16
|
from experimaestro.locking import Lock
|
|
@@ -86,12 +87,12 @@ class Process:
|
|
|
86
87
|
@staticmethod
|
|
87
88
|
def fromDefinition(connector: "Connector", definition: Dict[str, Any]) -> "Process":
|
|
88
89
|
"""Retrieves a process from a serialized definition"""
|
|
89
|
-
|
|
90
|
+
handler_type = definition["type"]
|
|
91
|
+
handler = Process.handler(handler_type)
|
|
92
|
+
assert handler is not None, f"No handler of type {handler_type}"
|
|
90
93
|
try:
|
|
91
94
|
return handler.fromspec(connector, definition)
|
|
92
95
|
except Exception as e:
|
|
93
|
-
import logging
|
|
94
|
-
|
|
95
96
|
logging.exception("Could not retrieve job from specification")
|
|
96
97
|
raise e
|
|
97
98
|
|
|
@@ -101,7 +102,11 @@ class Process:
|
|
|
101
102
|
if Process.HANDLERS is None:
|
|
102
103
|
Process.HANDLERS = {}
|
|
103
104
|
for ep in pkg_resources.iter_entry_points(group="experimaestro.process"):
|
|
104
|
-
|
|
105
|
+
logging.debug("Adding process handler for type %s", ep.name)
|
|
106
|
+
handler = ep.load()
|
|
107
|
+
Process.HANDLERS[ep.name] = handler
|
|
108
|
+
if handler is None:
|
|
109
|
+
logging.error("Handler of type %s is null", ep.name)
|
|
105
110
|
|
|
106
111
|
return Process.HANDLERS.get(key, None)
|
|
107
112
|
|
|
@@ -109,18 +114,25 @@ class Process:
|
|
|
109
114
|
"""Wait until the process finishes and returns the error code"""
|
|
110
115
|
raise NotImplementedError(f"Not implemented: {self.__class__}.wait")
|
|
111
116
|
|
|
112
|
-
async def aio_state(self) -> ProcessState:
|
|
113
|
-
"""Returns the job state
|
|
117
|
+
async def aio_state(self, timeout: float | None = None) -> ProcessState:
|
|
118
|
+
"""Returns the job state
|
|
119
|
+
|
|
120
|
+
:param timeout: maximum waiting time for a refresh
|
|
121
|
+
"""
|
|
114
122
|
raise NotImplementedError(f"Not implemented: {self.__class__}.aio_state")
|
|
115
123
|
|
|
116
124
|
async def aio_isrunning(self):
|
|
117
125
|
"""True is the process is truly running (I/O)"""
|
|
118
126
|
return (await self.aio_state()).running
|
|
119
127
|
|
|
120
|
-
async def aio_code(self):
|
|
121
|
-
"""Returns a future containing the returned code
|
|
128
|
+
async def aio_code(self) -> Optional[int]:
|
|
129
|
+
"""Returns a future containing the returned code
|
|
130
|
+
|
|
131
|
+
Returns None if the process has already finished – and no information is
|
|
132
|
+
known about the process.
|
|
133
|
+
"""
|
|
122
134
|
code = await asyncThreadcheck("aio_code", self.wait)
|
|
123
|
-
logger.debug("Got
|
|
135
|
+
logger.debug("Got return code %s for %s", code, self)
|
|
124
136
|
return code
|
|
125
137
|
|
|
126
138
|
def kill(self):
|
|
@@ -145,8 +157,11 @@ class ProcessBuilder:
|
|
|
145
157
|
self.environ: Mapping[str, str] = {}
|
|
146
158
|
self.command = []
|
|
147
159
|
|
|
148
|
-
def start(self) -> Process:
|
|
149
|
-
"""Start the process
|
|
160
|
+
def start(self, task_mode: bool = False) -> Process:
|
|
161
|
+
"""Start the process
|
|
162
|
+
|
|
163
|
+
:param task_mode: True if the process is a job script
|
|
164
|
+
"""
|
|
150
165
|
raise NotImplementedError("Method not implemented in %s" % self.__class__)
|
|
151
166
|
|
|
152
167
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""All classes related to localhost management
|
|
2
|
-
"""
|
|
1
|
+
"""All classes related to localhost management"""
|
|
3
2
|
|
|
4
3
|
import subprocess
|
|
4
|
+
from typing import Optional
|
|
5
5
|
from pathlib import Path, WindowsPath, PosixPath
|
|
6
6
|
import os
|
|
7
7
|
import threading
|
|
@@ -29,15 +29,17 @@ class PsutilProcess(Process):
|
|
|
29
29
|
def __init__(self, pid: int):
|
|
30
30
|
self._process = psutil.Process(pid)
|
|
31
31
|
|
|
32
|
-
def wait(self) -> int:
|
|
32
|
+
def wait(self) -> Optional[int]:
|
|
33
33
|
logger.debug("Waiting (psutil) for process with PID %s", self._process.pid)
|
|
34
34
|
code = self._process.wait()
|
|
35
35
|
logger.debug(
|
|
36
|
-
"Finished to wait (psutil) for process with PID %s",
|
|
36
|
+
"Finished to wait (psutil) for process with PID %s: code %s",
|
|
37
|
+
self._process.pid,
|
|
38
|
+
code,
|
|
37
39
|
)
|
|
38
40
|
return code
|
|
39
41
|
|
|
40
|
-
async def aio_state(self):
|
|
42
|
+
async def aio_state(self, timeout: float | None = None) -> ProcessState:
|
|
41
43
|
if self._process.is_running():
|
|
42
44
|
return ProcessState.RUNNING
|
|
43
45
|
return ProcessState.FINISHED
|
|
@@ -57,11 +59,13 @@ class LocalProcess(Process):
|
|
|
57
59
|
logger.debug("Waiting (python) for process with PID %s", self._process.pid)
|
|
58
60
|
code = self._process.wait()
|
|
59
61
|
logger.debug(
|
|
60
|
-
"Finished to wait (python) for process with PID %s",
|
|
62
|
+
"Finished to wait (python) for process with PID %s: %s",
|
|
63
|
+
self._process.pid,
|
|
64
|
+
code,
|
|
61
65
|
)
|
|
62
66
|
return code
|
|
63
67
|
|
|
64
|
-
async def aio_state(self):
|
|
68
|
+
async def aio_state(self, timeout: float | None = None) -> ProcessState:
|
|
65
69
|
code = self._process.poll()
|
|
66
70
|
if code is None:
|
|
67
71
|
return ProcessState.RUNNING
|
|
@@ -102,8 +106,11 @@ def getstream(redirect: Redirect, write: bool):
|
|
|
102
106
|
|
|
103
107
|
|
|
104
108
|
class LocalProcessBuilder(ProcessBuilder):
|
|
105
|
-
def start(self):
|
|
106
|
-
"""Start the process
|
|
109
|
+
def start(self, task_mode=False):
|
|
110
|
+
"""Start the process
|
|
111
|
+
|
|
112
|
+
:param task_mode: just ignored
|
|
113
|
+
"""
|
|
107
114
|
stdin = getstream(self.stdin, False)
|
|
108
115
|
stdout = getstream(self.stdout, True)
|
|
109
116
|
stderr = getstream(self.stderr, True)
|
|
@@ -194,7 +201,9 @@ class LocalConnector(Connector):
|
|
|
194
201
|
return LocalProcessBuilder()
|
|
195
202
|
|
|
196
203
|
def resolve(self, path: Path, basepath: Path = None) -> str:
|
|
197
|
-
assert isinstance(path, PosixPath) or isinstance(
|
|
204
|
+
assert isinstance(path, PosixPath) or isinstance(
|
|
205
|
+
path, WindowsPath
|
|
206
|
+
), f"Unrecognized path {type(path)}"
|
|
198
207
|
if not basepath:
|
|
199
208
|
return str(path.absolute())
|
|
200
209
|
try:
|
experimaestro/connectors/ssh.py
CHANGED
experimaestro/core/arguments.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Management of the arguments (params, options, etc) associated with the XPM objects"""
|
|
2
2
|
|
|
3
|
-
from typing import Optional, TypeVar, TYPE_CHECKING
|
|
3
|
+
from typing import Optional, TypeVar, TYPE_CHECKING, Callable, Any
|
|
4
4
|
from experimaestro.typingutils import get_optional
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import sys
|
|
@@ -75,11 +75,24 @@ class Argument:
|
|
|
75
75
|
self.constant = constant
|
|
76
76
|
self.ignored = self.type.ignore if ignored is None else ignored
|
|
77
77
|
self.required = required
|
|
78
|
-
self.default = default
|
|
79
|
-
self.generator = generator
|
|
80
78
|
self.objecttype = None
|
|
81
79
|
self.is_data = is_data
|
|
82
80
|
|
|
81
|
+
self.generator = generator
|
|
82
|
+
self.default = None
|
|
83
|
+
self.ignore_generated = False
|
|
84
|
+
|
|
85
|
+
if default is not None:
|
|
86
|
+
assert self.generator is None, "generator and default are exclusive options"
|
|
87
|
+
if isinstance(default, field):
|
|
88
|
+
self.ignore_generated = default.ignore_generated
|
|
89
|
+
if default.default is not None:
|
|
90
|
+
self.default = default.default
|
|
91
|
+
elif default.default_factory is not None:
|
|
92
|
+
self.generator = default.default_factory
|
|
93
|
+
else:
|
|
94
|
+
self.default = default
|
|
95
|
+
|
|
83
96
|
assert (
|
|
84
97
|
not self.constant or self.default is not None
|
|
85
98
|
), "Cannot be constant without default"
|
|
@@ -170,6 +183,25 @@ DataPath = Annotated[Path, dataHint]
|
|
|
170
183
|
"""Annotates a path that should be kept to restore an object to its state"""
|
|
171
184
|
|
|
172
185
|
|
|
186
|
+
class field:
|
|
187
|
+
"""Extra information for a given experimaestro field (param or meta)"""
|
|
188
|
+
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
default: Any = None,
|
|
193
|
+
default_factory: Callable = None,
|
|
194
|
+
ignore_generated=False,
|
|
195
|
+
):
|
|
196
|
+
assert not (
|
|
197
|
+
(default is not None) and (default_factory is not None)
|
|
198
|
+
), "default and default_factory are mutually exclusive options"
|
|
199
|
+
|
|
200
|
+
self.default_factory = default_factory
|
|
201
|
+
self.default = default
|
|
202
|
+
self.ignore_generated = ignore_generated
|
|
203
|
+
|
|
204
|
+
|
|
173
205
|
class help(TypeAnnotation):
|
|
174
206
|
def __init__(self, text: str):
|
|
175
207
|
self.text = text
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Callable, ClassVar, Optional
|
|
4
|
+
from experimaestro.core.objects import ConfigInformation
|
|
5
|
+
from experimaestro.scheduler import Listener, Job, JobState, experiment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskEventListener(Listener):
|
|
9
|
+
INSTANCE: ClassVar[Optional["TaskEventListener"]] = None
|
|
10
|
+
"""The general instance"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.lock = threading.Lock()
|
|
14
|
+
self.experiments: set[int] = set()
|
|
15
|
+
|
|
16
|
+
self._on_completed: defaultdict[int, Callable] = defaultdict(list)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def connect(xp: experiment):
|
|
20
|
+
_self = TaskEventListener.instance()
|
|
21
|
+
with _self.lock:
|
|
22
|
+
if id(xp) not in _self.experiments:
|
|
23
|
+
_self.experiments.add(id(xp))
|
|
24
|
+
xp.scheduler.addlistener(_self)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def instance():
|
|
28
|
+
if TaskEventListener.INSTANCE is None:
|
|
29
|
+
TaskEventListener.INSTANCE = TaskEventListener()
|
|
30
|
+
|
|
31
|
+
return TaskEventListener.INSTANCE
|
|
32
|
+
|
|
33
|
+
def job_state(self, job: Job):
|
|
34
|
+
if job.state == JobState.DONE:
|
|
35
|
+
with self.lock:
|
|
36
|
+
for callback in self._on_completed.get(id(job.config.__xpm__), []):
|
|
37
|
+
callback()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def on_completed(
|
|
41
|
+
config_information: ConfigInformation, callback: Callable[[], None]
|
|
42
|
+
):
|
|
43
|
+
instance = TaskEventListener.instance()
|
|
44
|
+
|
|
45
|
+
with instance.lock:
|
|
46
|
+
instance._on_completed[id(config_information)].append(callback)
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
config_information.job is not None
|
|
50
|
+
and config_information.job == JobState.DONE
|
|
51
|
+
):
|
|
52
|
+
callback()
|
experimaestro/core/context.py
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from pathlib import UnsupportedOperation
|
|
6
|
+
except ImportError:
|
|
7
|
+
UnsupportedOperation = OSError
|
|
3
8
|
import shutil
|
|
4
9
|
from typing import List, Optional, Protocol, Set, Union
|
|
5
10
|
import os
|
|
6
|
-
import sys
|
|
7
|
-
|
|
8
|
-
has_hardlink_to = sys.version_info.major == 3 and sys.version_info.minor >= 10
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
def shallow_copy(src_path: Path, dest_path: Path):
|
|
12
14
|
"""Copy a directory or file, trying to use hard links if possible"""
|
|
13
15
|
if src_path.is_file():
|
|
14
16
|
try:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
else:
|
|
18
|
-
dest_path.link_to(src_path)
|
|
19
|
-
except OSError:
|
|
17
|
+
dest_path.hardlink_to(src_path)
|
|
18
|
+
except (NotImplementedError, UnsupportedOperation, OSError):
|
|
20
19
|
shutil.copy(src_path, dest_path)
|
|
21
20
|
else:
|
|
22
21
|
if dest_path.exists():
|
|
@@ -74,7 +73,7 @@ class SerializationContext:
|
|
|
74
73
|
|
|
75
74
|
|
|
76
75
|
class SerializedPathLoader(Protocol):
|
|
77
|
-
def __call__(path: Union[Path, str, SerializedPath]) -> Path:
|
|
76
|
+
def __call__(self, path: Union[Path, str, SerializedPath]) -> Path:
|
|
78
77
|
"""Get a filesystem path from a relative path
|
|
79
78
|
|
|
80
79
|
:param path: The relative path
|