experimaestro 1.6.1__py3-none-any.whl → 1.7.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.
- experimaestro/__init__.py +3 -1
- experimaestro/annotations.py +13 -3
- experimaestro/cli/filter.py +3 -3
- experimaestro/cli/jobs.py +1 -1
- experimaestro/commandline.py +3 -7
- experimaestro/connectors/__init__.py +22 -10
- experimaestro/connectors/local.py +17 -8
- experimaestro/connectors/ssh.py +1 -1
- experimaestro/core/arguments.py +26 -3
- experimaestro/core/objects.py +90 -6
- experimaestro/core/objects.pyi +7 -1
- experimaestro/core/types.py +33 -2
- experimaestro/experiments/cli.py +21 -9
- experimaestro/generators.py +6 -1
- experimaestro/ipc.py +4 -1
- experimaestro/launcherfinder/registry.py +23 -5
- experimaestro/launchers/slurm/base.py +47 -9
- experimaestro/notifications.py +1 -1
- experimaestro/run.py +1 -1
- experimaestro/scheduler/base.py +102 -6
- experimaestro/scheduler/dynamic_outputs.py +184 -0
- experimaestro/scheduler/workspace.py +2 -1
- experimaestro/scriptbuilder.py +13 -2
- experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
- 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/50701fbb8177c2dde530.ttf +0 -0
- experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
- experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
- experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
- experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
- experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
- experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
- experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
- experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +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/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 +6 -3
- experimaestro/tests/tasks/all.py +16 -10
- experimaestro/tests/tasks/foreign.py +2 -4
- experimaestro/tests/test_forward.py +5 -5
- experimaestro/tests/test_identifier.py +61 -66
- experimaestro/tests/test_instance.py +3 -6
- experimaestro/tests/test_param.py +40 -22
- experimaestro/tests/test_tags.py +5 -11
- experimaestro/tests/test_tokens.py +3 -2
- experimaestro/tests/test_types.py +17 -14
- experimaestro/tests/test_validation.py +48 -91
- experimaestro/tokens.py +16 -5
- experimaestro/typingutils.py +7 -0
- experimaestro/utils/asyncio.py +6 -2
- experimaestro/utils/resources.py +7 -3
- {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/METADATA +3 -4
- experimaestro-1.7.0.dist-info/RECORD +154 -0
- {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/WHEEL +1 -1
- experimaestro-1.6.1.dist-info/RECORD +0 -122
- {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/LICENSE +0 -0
- {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Handles dynamic task outputs"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from watchdog.events import FileSystemEventHandler
|
|
14
|
+
|
|
15
|
+
from experimaestro.ipc import ipcom
|
|
16
|
+
from experimaestro.utils import logger
|
|
17
|
+
|
|
18
|
+
from .base import Job, experiment
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from experimaestro.core.objects import WatchedOutput
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaskOutputCallbackHandler:
|
|
25
|
+
def __init__(self, converter: Callable):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TaskOutputs(FileSystemEventHandler):
|
|
30
|
+
"""Represent and monitors dynamic outputs generated by one task"""
|
|
31
|
+
|
|
32
|
+
#: Global dictionary for handles
|
|
33
|
+
HANDLERS: dict[Path, "TaskOutputs"] = {}
|
|
34
|
+
|
|
35
|
+
#: Global lock to access current HANDLERS
|
|
36
|
+
LOCK = threading.Lock()
|
|
37
|
+
|
|
38
|
+
def create(job: Job):
|
|
39
|
+
with TaskOutputs.LOCK:
|
|
40
|
+
if instance := TaskOutputs.get(job.task_outputs_path, None):
|
|
41
|
+
return instance
|
|
42
|
+
|
|
43
|
+
instance = TaskOutputs(job.task_outputs_path)
|
|
44
|
+
TaskOutputs[job.task_outputs_path] = instance
|
|
45
|
+
return instance
|
|
46
|
+
|
|
47
|
+
def __init__(self, path: Path):
|
|
48
|
+
"""Monitors an event path"""
|
|
49
|
+
logger.debug("Watching dynamic task outputs in %s", path)
|
|
50
|
+
self.path = path
|
|
51
|
+
self.handle = None
|
|
52
|
+
self.count = 0
|
|
53
|
+
self.lock = threading.Lock()
|
|
54
|
+
self.listeners: dict[str, dict[Callable, set[Callable]]] = defaultdict(
|
|
55
|
+
lambda: defaultdict(set)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
#: The events registered so far
|
|
59
|
+
self.events = []
|
|
60
|
+
|
|
61
|
+
def __enter__(self):
|
|
62
|
+
"""Starts monitoring task outputs"""
|
|
63
|
+
self.job.task_outputs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
with self.lock:
|
|
65
|
+
if self.handle is None:
|
|
66
|
+
assert self.count == 0
|
|
67
|
+
self.handle = ipcom().fswatch(self, self.path.parent, False)
|
|
68
|
+
self.count += 1
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, *args):
|
|
72
|
+
"""Stops monitoring task outputs"""
|
|
73
|
+
with self.lock:
|
|
74
|
+
self.count -= 1
|
|
75
|
+
if self.count == 0:
|
|
76
|
+
ipcom().fsunwatch(self.handle)
|
|
77
|
+
self.fh.close()
|
|
78
|
+
|
|
79
|
+
self.handle = None
|
|
80
|
+
self._fh = None
|
|
81
|
+
|
|
82
|
+
def watch_output(self, watched: "WatchedOutput"):
|
|
83
|
+
"""Add a new listener"""
|
|
84
|
+
key = f"{watched.config.__identifier__}/{watched.method_name}"
|
|
85
|
+
with self.lock:
|
|
86
|
+
# Process events so far
|
|
87
|
+
listener = self.listeners[key].get(watched.method, None)
|
|
88
|
+
if listener is None:
|
|
89
|
+
listener = TaskOutputCallbackHandler(watched.method)
|
|
90
|
+
|
|
91
|
+
# Register
|
|
92
|
+
self.listeners[key][watched.method].add(watched.callback)
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# --- Events
|
|
96
|
+
#
|
|
97
|
+
|
|
98
|
+
@cached_property
|
|
99
|
+
def fh(self):
|
|
100
|
+
if self._fh is None:
|
|
101
|
+
self._fh = self.path.open("rt")
|
|
102
|
+
return self._fh
|
|
103
|
+
|
|
104
|
+
def on_modified(self, event):
|
|
105
|
+
self.handle(Path(event.src_path))
|
|
106
|
+
|
|
107
|
+
def on_created(self, event):
|
|
108
|
+
self.handle(Path(event.src_path))
|
|
109
|
+
|
|
110
|
+
def handle(self, path: Path):
|
|
111
|
+
if path != self.path:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
with self.lock:
|
|
115
|
+
logger.debug("[TASK OUTPUT] Handling task output for %s", self.path)
|
|
116
|
+
|
|
117
|
+
while json_line := self.fh.readline():
|
|
118
|
+
# Read the event
|
|
119
|
+
event = json.loads(json_line)
|
|
120
|
+
logger.debug("Event: %s", event)
|
|
121
|
+
|
|
122
|
+
# FIXME: move elsewhere
|
|
123
|
+
# # Process the event
|
|
124
|
+
# event = self.config_method(
|
|
125
|
+
# self.job.config.__xpm__.mark_output,
|
|
126
|
+
# *event["args"],
|
|
127
|
+
# **event["kwargs"],
|
|
128
|
+
# )
|
|
129
|
+
|
|
130
|
+
self.events.append(event)
|
|
131
|
+
# self.job.scheduler.xp.taskOutputsWorker.add(self, event)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TaskOutputsWorker(threading.Thread):
|
|
135
|
+
"""This worker process dynamic output queue for one experiment"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, xp: experiment):
|
|
138
|
+
super().__init__(name="task outputs worker", daemon=True)
|
|
139
|
+
self.queue = queue.Queue()
|
|
140
|
+
self.xp = xp
|
|
141
|
+
|
|
142
|
+
def watch_output(self, watched: "WatchedOutput"):
|
|
143
|
+
"""Watch an output
|
|
144
|
+
|
|
145
|
+
:param watched: The watched output specification
|
|
146
|
+
"""
|
|
147
|
+
logger.debug("Registering task output listener %s", watched)
|
|
148
|
+
|
|
149
|
+
# path = watched.job.tasks_output_path
|
|
150
|
+
TaskOutputs.create(watched.job).watch_output(watched)
|
|
151
|
+
|
|
152
|
+
def add(self, watcher, event):
|
|
153
|
+
asyncio.run_coroutine_threadsafe(
|
|
154
|
+
self.xp.update_task_output_count(1),
|
|
155
|
+
self.xp.scheduler.loop,
|
|
156
|
+
).result()
|
|
157
|
+
self.queue.put((watcher, event))
|
|
158
|
+
|
|
159
|
+
def run(self):
|
|
160
|
+
logging.debug("Starting output listener queue")
|
|
161
|
+
while True:
|
|
162
|
+
# Get the next element in the queue
|
|
163
|
+
element = self.queue.get()
|
|
164
|
+
if element is None:
|
|
165
|
+
# end of processing
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
# Call all the listeners
|
|
169
|
+
logging.debug("Got one event: %s", element)
|
|
170
|
+
watcher, event = element
|
|
171
|
+
for listener in watcher.listeners:
|
|
172
|
+
try:
|
|
173
|
+
logger.debug("Calling listener [%s] with %s", listener, event)
|
|
174
|
+
listener(event)
|
|
175
|
+
logger.debug(
|
|
176
|
+
"[done] Calling listener [%s] with %s", listener, event
|
|
177
|
+
)
|
|
178
|
+
except Exception:
|
|
179
|
+
logging.exception("Exception while calling the listener")
|
|
180
|
+
self.queue.task_done()
|
|
181
|
+
|
|
182
|
+
asyncio.run_coroutine_threadsafe(
|
|
183
|
+
self.xp.update_task_output_count(-1), self.xp.scheduler.loop
|
|
184
|
+
).result()
|
|
@@ -2,7 +2,7 @@ from collections import ChainMap
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from functools import cached_property
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
5
|
+
from typing import Iterator, Optional
|
|
6
6
|
from experimaestro.settings import WorkspaceSettings, Settings
|
|
7
7
|
|
|
8
8
|
|
|
@@ -46,6 +46,7 @@ class Workspace:
|
|
|
46
46
|
path = path.absolute()
|
|
47
47
|
self.path = path
|
|
48
48
|
self.run_mode = run_mode
|
|
49
|
+
self.python_path = []
|
|
49
50
|
from ..launchers import Launcher
|
|
50
51
|
|
|
51
52
|
self.launcher = launcher or Launcher.get(path)
|
experimaestro/scriptbuilder.py
CHANGED
|
@@ -51,6 +51,8 @@ class PythonScriptBuilder:
|
|
|
51
51
|
self.lockfiles: List[Path] = []
|
|
52
52
|
self.notificationURL: Optional[str] = None
|
|
53
53
|
self.command: Optional[AbstractCommand] = None
|
|
54
|
+
|
|
55
|
+
# This is used to serialize the full process identifier on disk
|
|
54
56
|
self.processtype = "local"
|
|
55
57
|
|
|
56
58
|
def write(self, job: CommandLineJob):
|
|
@@ -63,7 +65,7 @@ class PythonScriptBuilder:
|
|
|
63
65
|
job {CommandLineJob} -- [description]
|
|
64
66
|
|
|
65
67
|
Returns:
|
|
66
|
-
|
|
68
|
+
str -- The script path on disk
|
|
67
69
|
"""
|
|
68
70
|
assert isinstance(
|
|
69
71
|
job, CommandLineJob
|
|
@@ -94,6 +96,7 @@ class PythonScriptBuilder:
|
|
|
94
96
|
out.write("# Experimaestro generated task\n\n")
|
|
95
97
|
out.write(
|
|
96
98
|
"""import logging\n"""
|
|
99
|
+
"""import sys\n"""
|
|
97
100
|
"""logging.basicConfig(level=logging.INFO, """
|
|
98
101
|
"""format='%(levelname)s:%(process)d:%(asctime)s [%(name)s] %(message)s', datefmt='%y-%m-%d %H:%M:%S')\n\n"""
|
|
99
102
|
)
|
|
@@ -112,9 +115,17 @@ class PythonScriptBuilder:
|
|
|
112
115
|
out.write(" ]\n")
|
|
113
116
|
|
|
114
117
|
for name, value in job.environ.items():
|
|
115
|
-
|
|
118
|
+
if name == "PYTHONPATH":
|
|
119
|
+
# Handles properly python path
|
|
120
|
+
for path in value.split(":"):
|
|
121
|
+
out.write(f""" sys.path.insert(0, "{shquote(path)}")\n""")
|
|
122
|
+
else:
|
|
123
|
+
out.write(f""" os.environ["{name}"] = "{shquote(value)}"\n""")
|
|
116
124
|
out.write("\n")
|
|
117
125
|
|
|
126
|
+
for path in job.python_path:
|
|
127
|
+
out.write(f""" sys.path.insert(0, "{shquote(str(path))}")\n""")
|
|
128
|
+
|
|
118
129
|
out.write(
|
|
119
130
|
f""" TaskRunner("{shquote(connector.resolve(scriptpath))}","""
|
|
120
131
|
""" lockfiles).run()\n"""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|