experimaestro 2.0.0b4__py3-none-any.whl → 2.0.0b17__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 +12 -5
- experimaestro/cli/__init__.py +393 -134
- experimaestro/cli/filter.py +48 -23
- experimaestro/cli/jobs.py +253 -71
- experimaestro/cli/refactor.py +1 -2
- experimaestro/commandline.py +7 -4
- experimaestro/connectors/__init__.py +9 -1
- experimaestro/connectors/local.py +43 -3
- experimaestro/core/arguments.py +18 -18
- experimaestro/core/identifier.py +11 -11
- experimaestro/core/objects/config.py +96 -39
- experimaestro/core/objects/config_walk.py +3 -3
- experimaestro/core/{subparameters.py → partial.py} +16 -16
- experimaestro/core/partial_lock.py +394 -0
- experimaestro/core/types.py +12 -15
- experimaestro/dynamic.py +290 -0
- experimaestro/experiments/__init__.py +6 -2
- experimaestro/experiments/cli.py +223 -52
- experimaestro/experiments/configuration.py +24 -0
- experimaestro/generators.py +5 -5
- experimaestro/ipc.py +118 -1
- experimaestro/launcherfinder/__init__.py +2 -2
- experimaestro/launcherfinder/registry.py +6 -7
- experimaestro/launcherfinder/specs.py +2 -9
- experimaestro/launchers/slurm/__init__.py +2 -2
- experimaestro/launchers/slurm/base.py +62 -0
- experimaestro/locking.py +957 -1
- experimaestro/notifications.py +89 -201
- experimaestro/progress.py +63 -366
- experimaestro/rpyc.py +0 -2
- experimaestro/run.py +29 -2
- experimaestro/scheduler/__init__.py +8 -1
- experimaestro/scheduler/base.py +650 -53
- experimaestro/scheduler/dependencies.py +20 -16
- experimaestro/scheduler/experiment.py +764 -169
- experimaestro/scheduler/interfaces.py +338 -96
- experimaestro/scheduler/jobs.py +58 -20
- experimaestro/scheduler/remote/__init__.py +31 -0
- experimaestro/scheduler/remote/adaptive_sync.py +265 -0
- experimaestro/scheduler/remote/client.py +928 -0
- experimaestro/scheduler/remote/protocol.py +282 -0
- experimaestro/scheduler/remote/server.py +447 -0
- experimaestro/scheduler/remote/sync.py +144 -0
- experimaestro/scheduler/services.py +186 -35
- experimaestro/scheduler/state_provider.py +811 -2157
- experimaestro/scheduler/state_status.py +1247 -0
- experimaestro/scheduler/transient.py +31 -0
- experimaestro/scheduler/workspace.py +1 -1
- experimaestro/scheduler/workspace_state_provider.py +1273 -0
- experimaestro/scriptbuilder.py +4 -4
- experimaestro/settings.py +36 -0
- experimaestro/tests/conftest.py +33 -5
- experimaestro/tests/connectors/bin/executable.py +1 -1
- experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
- experimaestro/tests/launchers/bin/test.py +1 -0
- experimaestro/tests/launchers/test_slurm.py +9 -9
- experimaestro/tests/partial_reschedule.py +46 -0
- experimaestro/tests/restart.py +3 -3
- experimaestro/tests/restart_main.py +1 -0
- experimaestro/tests/scripts/notifyandwait.py +1 -0
- experimaestro/tests/task_partial.py +38 -0
- experimaestro/tests/task_tokens.py +2 -2
- experimaestro/tests/tasks/test_dynamic.py +6 -6
- experimaestro/tests/test_dependencies.py +3 -3
- experimaestro/tests/test_deprecated.py +15 -15
- experimaestro/tests/test_dynamic_locking.py +317 -0
- experimaestro/tests/test_environment.py +24 -14
- experimaestro/tests/test_experiment.py +171 -36
- experimaestro/tests/test_identifier.py +25 -25
- experimaestro/tests/test_identifier_stability.py +3 -5
- experimaestro/tests/test_multitoken.py +2 -4
- experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
- experimaestro/tests/test_partial_paths.py +81 -138
- experimaestro/tests/test_pre_experiment.py +219 -0
- experimaestro/tests/test_progress.py +2 -8
- experimaestro/tests/test_remote_state.py +1132 -0
- experimaestro/tests/test_stray_jobs.py +261 -0
- experimaestro/tests/test_tasks.py +1 -2
- experimaestro/tests/test_token_locking.py +52 -67
- experimaestro/tests/test_tokens.py +5 -6
- experimaestro/tests/test_transient.py +225 -0
- experimaestro/tests/test_workspace_state_provider.py +768 -0
- experimaestro/tests/token_reschedule.py +1 -3
- experimaestro/tests/utils.py +2 -7
- experimaestro/tokens.py +227 -372
- experimaestro/tools/diff.py +1 -0
- experimaestro/tools/documentation.py +4 -5
- experimaestro/tools/jobs.py +1 -2
- experimaestro/tui/app.py +459 -1895
- experimaestro/tui/app.tcss +162 -0
- experimaestro/tui/dialogs.py +172 -0
- experimaestro/tui/log_viewer.py +253 -3
- experimaestro/tui/messages.py +137 -0
- experimaestro/tui/utils.py +54 -0
- experimaestro/tui/widgets/__init__.py +23 -0
- experimaestro/tui/widgets/experiments.py +468 -0
- experimaestro/tui/widgets/global_services.py +238 -0
- experimaestro/tui/widgets/jobs.py +972 -0
- experimaestro/tui/widgets/log.py +156 -0
- experimaestro/tui/widgets/orphans.py +363 -0
- experimaestro/tui/widgets/runs.py +185 -0
- experimaestro/tui/widgets/services.py +314 -0
- experimaestro/tui/widgets/stray_jobs.py +528 -0
- experimaestro/utils/__init__.py +1 -1
- experimaestro/utils/environment.py +105 -22
- experimaestro/utils/fswatcher.py +124 -0
- experimaestro/utils/jobs.py +1 -2
- experimaestro/utils/jupyter.py +1 -2
- experimaestro/utils/logging.py +72 -0
- experimaestro/version.py +2 -2
- experimaestro/webui/__init__.py +9 -0
- experimaestro/webui/app.py +117 -0
- experimaestro/{server → webui}/data/index.css +66 -11
- experimaestro/webui/data/index.css.map +1 -0
- experimaestro/{server → webui}/data/index.js +82763 -87217
- experimaestro/webui/data/index.js.map +1 -0
- experimaestro/webui/routes/__init__.py +5 -0
- experimaestro/webui/routes/auth.py +53 -0
- experimaestro/webui/routes/proxy.py +117 -0
- experimaestro/webui/server.py +200 -0
- experimaestro/webui/state_bridge.py +152 -0
- experimaestro/webui/websocket.py +413 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +8 -9
- experimaestro-2.0.0b17.dist-info/RECORD +219 -0
- experimaestro/cli/progress.py +0 -269
- experimaestro/scheduler/state.py +0 -75
- experimaestro/scheduler/state_db.py +0 -388
- experimaestro/scheduler/state_sync.py +0 -834
- experimaestro/server/__init__.py +0 -467
- experimaestro/server/data/index.css.map +0 -1
- experimaestro/server/data/index.js.map +0 -1
- experimaestro/tests/test_cli_jobs.py +0 -615
- experimaestro/tests/test_file_progress.py +0 -425
- experimaestro/tests/test_file_progress_integration.py +0 -477
- experimaestro/tests/test_state_db.py +0 -434
- experimaestro-2.0.0b4.dist-info/RECORD +0 -181
- /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
- /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
- /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
- /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
- /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
- /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
- /experimaestro/{server → webui}/data/favicon.ico +0 -0
- /experimaestro/{server → webui}/data/index.html +0 -0
- /experimaestro/{server → webui}/data/login.html +0 -0
- /experimaestro/{server → webui}/data/manifest.json +0 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
from enum import Enum
|
|
3
|
-
import functools
|
|
4
3
|
import logging
|
|
5
4
|
import threading
|
|
6
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, Optional, Set, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from experimaestro.scheduler.interfaces import BaseService
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from experimaestro.scheduler.experiment import Experiment
|
|
7
12
|
|
|
8
13
|
logger = logging.getLogger(__name__)
|
|
9
14
|
|
|
@@ -11,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
|
11
16
|
class ServiceListener:
|
|
12
17
|
"""A service listener"""
|
|
13
18
|
|
|
14
|
-
def service_state_changed(service):
|
|
19
|
+
def service_state_changed(self, service):
|
|
15
20
|
pass
|
|
16
21
|
|
|
17
22
|
|
|
@@ -28,7 +33,7 @@ class ServiceState(Enum):
|
|
|
28
33
|
STOPPING = 3
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
class Service:
|
|
36
|
+
class Service(BaseService):
|
|
32
37
|
"""An experiment service
|
|
33
38
|
|
|
34
39
|
Services can be associated with an experiment. They send
|
|
@@ -46,42 +51,138 @@ class Service:
|
|
|
46
51
|
self._listeners: Set[ServiceListener] = set()
|
|
47
52
|
self._listeners_lock = threading.Lock()
|
|
48
53
|
|
|
54
|
+
def set_experiment(self, xp: "Experiment") -> None:
|
|
55
|
+
"""Called when the service is added to an experiment.
|
|
56
|
+
|
|
57
|
+
Override this method to access the experiment context (e.g., workdir).
|
|
58
|
+
The default implementation does nothing.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
xp: The experiment this service is being added to.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
49
65
|
def state_dict(self) -> dict:
|
|
50
|
-
"""Return
|
|
66
|
+
"""Return parameters needed to recreate this service.
|
|
51
67
|
|
|
52
|
-
Subclasses should override this to
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
Subclasses should override this to return constructor arguments.
|
|
69
|
+
Path values are automatically serialized and restored (with
|
|
70
|
+
translation for remote monitoring).
|
|
71
|
+
|
|
72
|
+
Example::
|
|
73
|
+
|
|
74
|
+
def state_dict(self):
|
|
75
|
+
return {
|
|
76
|
+
"log_dir": self.log_dir, # Path is auto-handled
|
|
77
|
+
"name": self.name,
|
|
78
|
+
}
|
|
55
79
|
|
|
56
80
|
Returns:
|
|
57
|
-
Dict with
|
|
81
|
+
Dict with constructor kwargs.
|
|
82
|
+
"""
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
def full_state_dict(self) -> dict:
|
|
86
|
+
"""Serialize service to dictionary for JSON serialization.
|
|
87
|
+
|
|
88
|
+
Overrides BaseService.full_state_dict() to properly serialize Path objects.
|
|
58
89
|
"""
|
|
59
90
|
return {
|
|
60
|
-
"
|
|
91
|
+
"service_id": self.id,
|
|
92
|
+
"description": self.description(),
|
|
93
|
+
"class": f"{self.__class__.__module__}.{self.__class__.__name__}",
|
|
94
|
+
"state_dict": self.serialize_state_dict(self.state_dict()),
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
@staticmethod
|
|
64
|
-
def
|
|
98
|
+
def serialize_state_dict(data: dict) -> dict:
|
|
99
|
+
"""Serialize a state_dict, converting Path objects to serializable format.
|
|
100
|
+
|
|
101
|
+
This is called automatically when storing services. Path values are
|
|
102
|
+
converted to {"__path__": "/path/string"} format.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
data: Raw state_dict from service (should include __class__)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Serializable dictionary with paths converted
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def serialize_value(v):
|
|
112
|
+
if isinstance(v, Path):
|
|
113
|
+
return {"__path__": str(v)}
|
|
114
|
+
elif isinstance(v, dict):
|
|
115
|
+
return {k: serialize_value(val) for k, val in v.items()}
|
|
116
|
+
elif isinstance(v, (list, tuple)):
|
|
117
|
+
return [serialize_value(item) for item in v]
|
|
118
|
+
else:
|
|
119
|
+
return v
|
|
120
|
+
|
|
121
|
+
return {k: serialize_value(v) for k, v in data.items()}
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def from_state_dict(
|
|
125
|
+
service_class: str,
|
|
126
|
+
data: dict,
|
|
127
|
+
path_translator: Optional[Callable[[str], Path]] = None,
|
|
128
|
+
) -> "Service":
|
|
65
129
|
"""Recreate a service from a state dictionary.
|
|
66
130
|
|
|
67
131
|
Args:
|
|
68
|
-
|
|
132
|
+
service_class: Fully qualified class name (e.g., "module.ClassName")
|
|
133
|
+
data: Dictionary from :meth:`state_dict` (may be serialized)
|
|
134
|
+
path_translator: Optional function to translate remote paths to local.
|
|
135
|
+
Used by remote clients to map paths to local cache.
|
|
69
136
|
|
|
70
137
|
Returns:
|
|
71
138
|
A new Service instance, or raises if the class cannot be loaded.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If __unserializable__ is True or class cannot be loaded
|
|
72
142
|
"""
|
|
73
143
|
import importlib
|
|
74
144
|
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
-
raise ValueError(
|
|
145
|
+
# Check if service is marked as unserializable
|
|
146
|
+
if data.get("__unserializable__"):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Service cannot be recreated: {data.get('__reason__', 'unknown reason')}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if not service_class:
|
|
152
|
+
raise ValueError("Missing service_class")
|
|
78
153
|
|
|
79
|
-
module_name, class_name =
|
|
154
|
+
module_name, class_name = service_class.rsplit(".", 1)
|
|
80
155
|
module = importlib.import_module(module_name)
|
|
81
156
|
cls = getattr(module, class_name)
|
|
82
157
|
|
|
83
|
-
#
|
|
84
|
-
|
|
158
|
+
# Build kwargs, detecting and translating paths automatically (handles nested)
|
|
159
|
+
def deserialize_value(v):
|
|
160
|
+
if isinstance(v, dict):
|
|
161
|
+
if "__path__" in v:
|
|
162
|
+
# Serialized path - deserialize with optional translation
|
|
163
|
+
path_str = v["__path__"]
|
|
164
|
+
if path_translator:
|
|
165
|
+
return path_translator(path_str)
|
|
166
|
+
else:
|
|
167
|
+
return Path(path_str)
|
|
168
|
+
else:
|
|
169
|
+
return {
|
|
170
|
+
k: deserialize_value(val)
|
|
171
|
+
for k, val in v.items()
|
|
172
|
+
if not k.startswith("__")
|
|
173
|
+
}
|
|
174
|
+
elif isinstance(v, list):
|
|
175
|
+
return [deserialize_value(item) for item in v]
|
|
176
|
+
else:
|
|
177
|
+
return v
|
|
178
|
+
|
|
179
|
+
kwargs = {}
|
|
180
|
+
for k, v in data.items():
|
|
181
|
+
if k.startswith("__"):
|
|
182
|
+
continue # Skip special keys
|
|
183
|
+
kwargs[k] = deserialize_value(v)
|
|
184
|
+
|
|
185
|
+
logger.debug("Creating %s with kwargs: %s", cls.__name__, kwargs)
|
|
85
186
|
return cls(**kwargs)
|
|
86
187
|
|
|
87
188
|
def add_listener(self, listener: ServiceListener):
|
|
@@ -158,6 +259,8 @@ class WebService(Service):
|
|
|
158
259
|
self.url = None
|
|
159
260
|
self.thread = None
|
|
160
261
|
self._stop_event = threading.Event()
|
|
262
|
+
self._start_lock = threading.Lock()
|
|
263
|
+
self._running_event: Optional[threading.Event] = None
|
|
161
264
|
|
|
162
265
|
def should_stop(self) -> bool:
|
|
163
266
|
"""Check if the service should stop.
|
|
@@ -173,21 +276,46 @@ class WebService(Service):
|
|
|
173
276
|
"""Get the URL of this web service, starting it if needed.
|
|
174
277
|
|
|
175
278
|
If the service is not running, this method will start it and
|
|
176
|
-
block until the URL is available.
|
|
279
|
+
block until the URL is available. If the service is already
|
|
280
|
+
starting or running, returns the existing URL.
|
|
177
281
|
|
|
178
282
|
:return: The URL where this service can be accessed
|
|
283
|
+
:raises RuntimeError: If called while service is stopping
|
|
179
284
|
"""
|
|
180
|
-
|
|
181
|
-
self.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
self.
|
|
285
|
+
with self._start_lock:
|
|
286
|
+
if self.state == ServiceState.STOPPING:
|
|
287
|
+
raise RuntimeError("Cannot start service while it is stopping")
|
|
288
|
+
|
|
289
|
+
if self.state == ServiceState.RUNNING:
|
|
290
|
+
logger.debug("Service already running, returning existing URL")
|
|
291
|
+
return self.url
|
|
292
|
+
|
|
293
|
+
if self.state == ServiceState.STOPPED:
|
|
294
|
+
logger.info(
|
|
295
|
+
"Starting service %s (id=%s)", self.__class__.__name__, id(self)
|
|
296
|
+
)
|
|
297
|
+
self._stop_event.clear()
|
|
298
|
+
self.state = ServiceState.STARTING
|
|
299
|
+
self._running_event = threading.Event()
|
|
300
|
+
self.serve()
|
|
301
|
+
else:
|
|
302
|
+
logger.info(
|
|
303
|
+
"Service %s (id=%s) already starting, waiting for it",
|
|
304
|
+
self.__class__.__name__,
|
|
305
|
+
id(self),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# State is STARTING - wait for it to be ready
|
|
309
|
+
running_event = self._running_event
|
|
310
|
+
|
|
311
|
+
# Wait outside the lock to avoid blocking other callers
|
|
312
|
+
if running_event:
|
|
313
|
+
running_event.wait()
|
|
314
|
+
# Set state to RUNNING (this will notify listeners)
|
|
315
|
+
with self._start_lock:
|
|
316
|
+
if self.state == ServiceState.STARTING:
|
|
317
|
+
self.state = ServiceState.RUNNING
|
|
185
318
|
|
|
186
|
-
# Wait until the server is ready
|
|
187
|
-
self.running.wait()
|
|
188
|
-
self.state = ServiceState.RUNNING
|
|
189
|
-
|
|
190
|
-
# Returns the URL
|
|
191
319
|
return self.url
|
|
192
320
|
|
|
193
321
|
def stop(self, timeout: float = 2.0):
|
|
@@ -199,10 +327,21 @@ class WebService(Service):
|
|
|
199
327
|
|
|
200
328
|
:param timeout: Seconds to wait for graceful shutdown before forcing
|
|
201
329
|
"""
|
|
202
|
-
|
|
203
|
-
|
|
330
|
+
with self._start_lock:
|
|
331
|
+
if self.state == ServiceState.STOPPED:
|
|
332
|
+
return
|
|
204
333
|
|
|
205
|
-
|
|
334
|
+
if self.state == ServiceState.STARTING:
|
|
335
|
+
# Wait for service to finish starting before stopping
|
|
336
|
+
running_event = self._running_event
|
|
337
|
+
else:
|
|
338
|
+
running_event = None
|
|
339
|
+
|
|
340
|
+
self.state = ServiceState.STOPPING
|
|
341
|
+
|
|
342
|
+
# Wait for starting to complete if needed (outside lock to avoid deadlock)
|
|
343
|
+
if running_event is not None:
|
|
344
|
+
running_event.wait()
|
|
206
345
|
|
|
207
346
|
# Signal the service to stop
|
|
208
347
|
self._stop_event.set()
|
|
@@ -215,8 +354,10 @@ class WebService(Service):
|
|
|
215
354
|
if self.thread.is_alive():
|
|
216
355
|
self._force_stop_thread()
|
|
217
356
|
|
|
218
|
-
self.
|
|
219
|
-
|
|
357
|
+
with self._start_lock:
|
|
358
|
+
self.url = None
|
|
359
|
+
self._running_event = None
|
|
360
|
+
self.state = ServiceState.STOPPED
|
|
220
361
|
|
|
221
362
|
def _force_stop_thread(self):
|
|
222
363
|
"""Attempt to forcefully stop the service thread.
|
|
@@ -254,12 +395,22 @@ class WebService(Service):
|
|
|
254
395
|
This method creates a daemon thread that calls :meth:`_serve`.
|
|
255
396
|
"""
|
|
256
397
|
self.thread = threading.Thread(
|
|
257
|
-
target=
|
|
398
|
+
target=self._serve_wrapper,
|
|
258
399
|
name=f"service[{self.id}]",
|
|
259
400
|
)
|
|
260
401
|
self.thread.daemon = True
|
|
261
402
|
self.thread.start()
|
|
262
403
|
|
|
404
|
+
def _serve_wrapper(self):
|
|
405
|
+
"""Wrapper for _serve that handles state transitions."""
|
|
406
|
+
running_event = self._running_event
|
|
407
|
+
try:
|
|
408
|
+
self._serve(running_event)
|
|
409
|
+
finally:
|
|
410
|
+
# Ensure the event is set even if _serve fails
|
|
411
|
+
if running_event and not running_event.is_set():
|
|
412
|
+
running_event.set()
|
|
413
|
+
|
|
263
414
|
@abc.abstractmethod
|
|
264
415
|
def _serve(self, running: threading.Event):
|
|
265
416
|
"""Start the web server (implement in subclasses).
|