experimaestro 1.11.1__py3-none-any.whl → 2.0.0b4__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 +10 -11
- experimaestro/annotations.py +167 -206
- experimaestro/cli/__init__.py +140 -16
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/progress.py +269 -0
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +22 -3
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +192 -37
- experimaestro/core/identifier.py +127 -12
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +702 -285
- experimaestro/core/objects/config_walk.py +24 -6
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +198 -83
- experimaestro/exceptions.py +26 -0
- experimaestro/experiments/cli.py +107 -25
- experimaestro/generators.py +50 -9
- experimaestro/huggingface.py +3 -1
- experimaestro/launcherfinder/parser.py +29 -0
- experimaestro/launcherfinder/registry.py +3 -3
- experimaestro/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +75 -16
- experimaestro/progress.py +404 -0
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +504 -959
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +582 -0
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +485 -0
- experimaestro/scheduler/services.py +186 -12
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +1 -1
- experimaestro/scheduler/state_db.py +388 -0
- experimaestro/scheduler/state_provider.py +2345 -0
- experimaestro/scheduler/state_sync.py +834 -0
- experimaestro/scheduler/workspace.py +52 -10
- experimaestro/scriptbuilder.py +7 -0
- experimaestro/server/__init__.py +153 -32
- experimaestro/server/data/index.css +0 -125
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +194 -58
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +47 -6
- experimaestro/sphinx/__init__.py +3 -3
- experimaestro/taskglobals.py +20 -0
- experimaestro/tests/conftest.py +80 -0
- experimaestro/tests/core/test_generics.py +2 -2
- experimaestro/tests/identifier_stability.json +45 -0
- experimaestro/tests/launchers/bin/sacct +6 -2
- experimaestro/tests/launchers/bin/sbatch +4 -2
- experimaestro/tests/launchers/common.py +2 -2
- experimaestro/tests/launchers/test_slurm.py +80 -0
- experimaestro/tests/restart.py +1 -1
- experimaestro/tests/tasks/all.py +7 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_experiment.py +3 -3
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +520 -169
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +16 -21
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +314 -30
- experimaestro/tests/test_outputs.py +8 -8
- experimaestro/tests/test_param.py +22 -26
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +2 -50
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -60
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +151 -15
- experimaestro/tests/test_tasks.py +137 -160
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +25 -19
- experimaestro/tests/test_types.py +133 -11
- experimaestro/tests/test_validation.py +19 -19
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +5 -3
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +8 -1
- experimaestro/tui/__init__.py +8 -0
- experimaestro/tui/app.py +2303 -0
- experimaestro/tui/app.tcss +353 -0
- experimaestro/tui/log_viewer.py +228 -0
- experimaestro/typingutils.py +11 -2
- experimaestro/utils/__init__.py +23 -0
- experimaestro/utils/environment.py +148 -0
- experimaestro/utils/git.py +129 -0
- experimaestro/utils/resources.py +1 -1
- experimaestro/version.py +34 -0
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
- experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
- experimaestro/compat.py +0 -6
- experimaestro/core/objects.pyi +0 -225
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +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/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro-1.11.1.dist-info/RECORD +0 -158
- experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
|
@@ -2,10 +2,14 @@ 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
|
|
5
|
+
from typing import Optional
|
|
6
6
|
from experimaestro.settings import WorkspaceSettings, Settings
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
# Current workspace version
|
|
10
|
+
WORKSPACE_VERSION = 0
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
class RunMode(str, Enum):
|
|
10
14
|
NORMAL = "normal"
|
|
11
15
|
"""Normal run"""
|
|
@@ -18,17 +22,16 @@ class RunMode(str, Enum):
|
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
class Workspace:
|
|
21
|
-
"""
|
|
25
|
+
"""Workspace environment for experiments
|
|
22
26
|
|
|
23
|
-
This
|
|
24
|
-
|
|
27
|
+
This is a simple container for workspace settings, environment, and configuration.
|
|
28
|
+
Multiple Workspace instances can exist for the same path - the singleton pattern
|
|
29
|
+
is handled by WorkspaceStateProvider which manages the database per workspace path.
|
|
25
30
|
"""
|
|
26
31
|
|
|
27
32
|
CURRENT = None
|
|
28
33
|
settings: "Settings"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"""Creates a workspace for experiments"""
|
|
34
|
+
workspace_settings: "WorkspaceSettings"
|
|
32
35
|
|
|
33
36
|
def __init__(
|
|
34
37
|
self,
|
|
@@ -37,6 +40,14 @@ class Workspace:
|
|
|
37
40
|
launcher=None,
|
|
38
41
|
run_mode: RunMode = None,
|
|
39
42
|
):
|
|
43
|
+
"""Initialize workspace environment
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
settings: Global settings
|
|
47
|
+
workspace_settings: Workspace-specific settings
|
|
48
|
+
launcher: Default launcher for this workspace
|
|
49
|
+
run_mode: Run mode for experiments in this workspace
|
|
50
|
+
"""
|
|
40
51
|
self.settings = settings
|
|
41
52
|
self.workspace_settings = workspace_settings
|
|
42
53
|
|
|
@@ -44,21 +55,47 @@ class Workspace:
|
|
|
44
55
|
self.notificationURL: Optional[str] = None
|
|
45
56
|
if isinstance(path, Path):
|
|
46
57
|
path = path.absolute()
|
|
58
|
+
else:
|
|
59
|
+
path = Path(path).absolute()
|
|
47
60
|
self.path = path
|
|
48
61
|
self.run_mode = run_mode
|
|
49
62
|
self.python_path = []
|
|
63
|
+
|
|
50
64
|
from ..launchers import Launcher
|
|
51
65
|
|
|
52
66
|
self.launcher = launcher or Launcher.get(path)
|
|
53
67
|
|
|
54
68
|
self.env = ChainMap({}, workspace_settings.env, settings.env)
|
|
55
69
|
|
|
70
|
+
# Reference counting for nested context managers
|
|
71
|
+
self._ref_count = 0
|
|
72
|
+
|
|
56
73
|
def __enter__(self):
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
# Increment reference count
|
|
75
|
+
self._ref_count += 1
|
|
76
|
+
|
|
77
|
+
# Only initialize on first entry
|
|
78
|
+
if self._ref_count == 1:
|
|
79
|
+
# Check if a different workspace is already active
|
|
80
|
+
if Workspace.CURRENT is not None and Workspace.CURRENT.path != self.path:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
f"Cannot activate workspace at {self.path} - "
|
|
83
|
+
f"workspace at {Workspace.CURRENT.path} is already active. "
|
|
84
|
+
"Multiple workspaces are not yet supported."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.old_workspace = Workspace.CURRENT
|
|
88
|
+
Workspace.CURRENT = self
|
|
89
|
+
|
|
90
|
+
return self
|
|
59
91
|
|
|
60
92
|
def __exit__(self, *args):
|
|
61
|
-
|
|
93
|
+
# Decrement reference count
|
|
94
|
+
self._ref_count -= 1
|
|
95
|
+
|
|
96
|
+
# Only cleanup on last exit
|
|
97
|
+
if self._ref_count == 0:
|
|
98
|
+
Workspace.CURRENT = self.old_workspace
|
|
62
99
|
|
|
63
100
|
@cached_property
|
|
64
101
|
def alt_workspaces(self):
|
|
@@ -79,6 +116,11 @@ class Workspace:
|
|
|
79
116
|
"""Folder for jobs"""
|
|
80
117
|
return self.path / "jobs"
|
|
81
118
|
|
|
119
|
+
@property
|
|
120
|
+
def partialspath(self):
|
|
121
|
+
"""Folder for partial job directories (shared checkpoints, etc.)"""
|
|
122
|
+
return self.path / "partials"
|
|
123
|
+
|
|
82
124
|
@property
|
|
83
125
|
def experimentspath(self):
|
|
84
126
|
"""Folder for experiments"""
|
experimaestro/scriptbuilder.py
CHANGED
|
@@ -126,6 +126,13 @@ class PythonScriptBuilder:
|
|
|
126
126
|
for path in job.python_path:
|
|
127
127
|
out.write(f""" sys.path.insert(0, "{shquote(str(path))}")\n""")
|
|
128
128
|
|
|
129
|
+
# Write launcher info code (for remaining_time support)
|
|
130
|
+
launcher_info_code = job.launcher.launcher_info_code()
|
|
131
|
+
if launcher_info_code:
|
|
132
|
+
out.write("\n")
|
|
133
|
+
out.write(launcher_info_code)
|
|
134
|
+
out.write("\n")
|
|
135
|
+
|
|
129
136
|
out.write(
|
|
130
137
|
f""" TaskRunner("{shquote(connector.resolve(scriptpath))}","""
|
|
131
138
|
""" lockfiles).run()\n"""
|
experimaestro/server/__init__.py
CHANGED
|
@@ -5,10 +5,12 @@ import platform
|
|
|
5
5
|
import socket
|
|
6
6
|
import uuid
|
|
7
7
|
from experimaestro.scheduler.base import Job
|
|
8
|
-
import
|
|
8
|
+
import sys
|
|
9
9
|
import http
|
|
10
10
|
import threading
|
|
11
|
-
from typing import Optional, Tuple
|
|
11
|
+
from typing import Optional, Tuple, ClassVar
|
|
12
|
+
|
|
13
|
+
from importlib.resources import files
|
|
12
14
|
from experimaestro.scheduler import Scheduler, Listener as BaseListener
|
|
13
15
|
from experimaestro.scheduler.services import Service, ServiceListener
|
|
14
16
|
from experimaestro.settings import ServerSettings
|
|
@@ -46,6 +48,9 @@ def job_details(job):
|
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def job_create(job: Job):
|
|
51
|
+
# Get experiment IDs from job.experiments list
|
|
52
|
+
experiment_ids = [xp.workdir.name for xp in job.experiments]
|
|
53
|
+
|
|
49
54
|
return {
|
|
50
55
|
"jobId": job.identifier,
|
|
51
56
|
"taskId": job.name,
|
|
@@ -53,28 +58,50 @@ def job_create(job: Job):
|
|
|
53
58
|
"status": job.state.name.lower(),
|
|
54
59
|
"tags": list(job.tags.items()),
|
|
55
60
|
"progress": progress_state(job),
|
|
61
|
+
"experimentIds": experiment_ids, # Add experiment IDs
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
class Listener(BaseListener, ServiceListener):
|
|
60
|
-
def __init__(self,
|
|
61
|
-
self.scheduler = scheduler
|
|
66
|
+
def __init__(self, socketio, state_provider):
|
|
62
67
|
self.socketio = socketio
|
|
63
|
-
self.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
self.state_provider = state_provider
|
|
69
|
+
|
|
70
|
+
# Try to get the scheduler (if one is running for active experiments)
|
|
71
|
+
# Otherwise we're in monitoring mode and don't need scheduler events
|
|
72
|
+
try:
|
|
73
|
+
from experimaestro.scheduler import Scheduler
|
|
74
|
+
|
|
75
|
+
# Check if a scheduler instance exists (would be created if experiments are running)
|
|
76
|
+
if Scheduler._instance is not None:
|
|
77
|
+
self.scheduler = Scheduler._instance
|
|
78
|
+
self.scheduler.addlistener(self)
|
|
79
|
+
self.services = {}
|
|
80
|
+
# Initialize services from all registered experiments
|
|
81
|
+
for xp in self.scheduler.experiments.values():
|
|
82
|
+
for service in xp.services.values():
|
|
83
|
+
self.service_add(service)
|
|
84
|
+
else:
|
|
85
|
+
# No scheduler running - monitoring mode
|
|
86
|
+
self.scheduler = None
|
|
87
|
+
self.services = {}
|
|
88
|
+
except Exception:
|
|
89
|
+
# Scheduler not available - monitoring mode
|
|
90
|
+
self.scheduler = None
|
|
91
|
+
self.services = {}
|
|
67
92
|
|
|
68
93
|
def job_submitted(self, job):
|
|
69
94
|
self.socketio.emit("job.add", job_create(job))
|
|
70
95
|
|
|
71
96
|
def job_state(self, job):
|
|
97
|
+
experiment_ids = [xp.workdir.name for xp in job.experiments]
|
|
72
98
|
self.socketio.emit(
|
|
73
99
|
"job.update",
|
|
74
100
|
{
|
|
75
101
|
"jobId": job.identifier,
|
|
76
102
|
"status": job.state.name.lower(),
|
|
77
103
|
"progress": progress_state(job),
|
|
104
|
+
"experimentIds": experiment_ids,
|
|
78
105
|
},
|
|
79
106
|
)
|
|
80
107
|
|
|
@@ -143,13 +170,14 @@ def proxy_response(base_url: str, request: Request, path: str):
|
|
|
143
170
|
return flask_response
|
|
144
171
|
|
|
145
172
|
|
|
173
|
+
# flake8: noqa: C901
|
|
146
174
|
def start_app(server: "Server"):
|
|
147
175
|
logging.debug("Starting Flask server...")
|
|
148
176
|
app = Flask("experimaestro")
|
|
149
177
|
|
|
150
178
|
logging.debug("Starting Flask server (SocketIO)...")
|
|
151
179
|
socketio = SocketIO(app, path="/api", async_mode="gevent")
|
|
152
|
-
listener = Listener(server.
|
|
180
|
+
listener = Listener(socketio, server.state_provider)
|
|
153
181
|
|
|
154
182
|
logging.debug("Starting Flask server (setting up socketio)...")
|
|
155
183
|
|
|
@@ -159,13 +187,48 @@ def start_app(server: "Server"):
|
|
|
159
187
|
raise ConnectionRefusedError("invalid token")
|
|
160
188
|
|
|
161
189
|
@socketio.on("refresh")
|
|
162
|
-
def handle_refresh():
|
|
163
|
-
for
|
|
164
|
-
|
|
190
|
+
def handle_refresh(experiment_id=None):
|
|
191
|
+
"""Refresh jobs for an experiment (or all experiments if None)"""
|
|
192
|
+
if experiment_id:
|
|
193
|
+
# Refresh specific experiment
|
|
194
|
+
jobs = listener.state_provider.get_jobs(experiment_id)
|
|
195
|
+
for job_data in jobs:
|
|
196
|
+
emit("job.add", job_data)
|
|
197
|
+
else:
|
|
198
|
+
# Refresh all experiments
|
|
199
|
+
if listener.scheduler:
|
|
200
|
+
# Active experiments: get jobs from scheduler
|
|
201
|
+
for job in listener.scheduler.jobs.values():
|
|
202
|
+
emit("job.add", job_create(job))
|
|
203
|
+
else:
|
|
204
|
+
# Monitoring mode: get jobs from WorkspaceStateProvider
|
|
205
|
+
for exp in listener.state_provider.get_experiments():
|
|
206
|
+
exp_id = exp["experiment_id"]
|
|
207
|
+
jobs = listener.state_provider.get_jobs(exp_id)
|
|
208
|
+
for job_data in jobs:
|
|
209
|
+
emit("job.add", job_data)
|
|
210
|
+
|
|
211
|
+
@socketio.on("experiments")
|
|
212
|
+
def handle_experiments():
|
|
213
|
+
"""List all experiments"""
|
|
214
|
+
experiments = listener.state_provider.get_experiments()
|
|
215
|
+
for exp in experiments:
|
|
216
|
+
emit("experiment.add", exp)
|
|
165
217
|
|
|
166
218
|
@socketio.on("job.details")
|
|
167
|
-
def handle_details(
|
|
168
|
-
|
|
219
|
+
def handle_details(data):
|
|
220
|
+
"""Get job details - expects {experimentId, jobId} or just jobId (backward compat)"""
|
|
221
|
+
# Backward compatibility: if data is a string, treat it as jobId
|
|
222
|
+
if isinstance(data, str):
|
|
223
|
+
jobid = data
|
|
224
|
+
if listener.scheduler:
|
|
225
|
+
emit("job.update", job_details(listener.scheduler.jobs[jobid]))
|
|
226
|
+
else:
|
|
227
|
+
experiment_id = data.get("experimentId")
|
|
228
|
+
job_id = data.get("jobId")
|
|
229
|
+
job_data = listener.state_provider.get_job(experiment_id, job_id)
|
|
230
|
+
if job_data:
|
|
231
|
+
emit("job.update", job_data)
|
|
169
232
|
|
|
170
233
|
@socketio.on("services")
|
|
171
234
|
def handle_services_list():
|
|
@@ -180,14 +243,26 @@ def start_app(server: "Server"):
|
|
|
180
243
|
)
|
|
181
244
|
|
|
182
245
|
@socketio.on("job.kill")
|
|
183
|
-
def handle_job_kill(
|
|
184
|
-
job
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
246
|
+
def handle_job_kill(data):
|
|
247
|
+
"""Kill a job - expects {experimentId, jobId} or just jobId (backward compat)"""
|
|
248
|
+
# Backward compatibility: if data is a string, treat it as jobId
|
|
249
|
+
if isinstance(data, str):
|
|
250
|
+
jobid = data
|
|
251
|
+
if listener.scheduler:
|
|
252
|
+
job = listener.scheduler.jobs[jobid]
|
|
253
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
254
|
+
job.aio_process(), listener.scheduler.loop
|
|
255
|
+
)
|
|
256
|
+
process = future.result()
|
|
257
|
+
if process is not None:
|
|
258
|
+
process.kill()
|
|
259
|
+
else:
|
|
260
|
+
experiment_id = data.get("experimentId")
|
|
261
|
+
job_id = data.get("jobId")
|
|
262
|
+
try:
|
|
263
|
+
listener.state_provider.kill_job(experiment_id, job_id)
|
|
264
|
+
except NotImplementedError:
|
|
265
|
+
logging.warning("kill_job not supported for this state provider")
|
|
191
266
|
|
|
192
267
|
logging.debug("Starting Flask server (setting up routes)...")
|
|
193
268
|
|
|
@@ -197,7 +272,15 @@ def start_app(server: "Server"):
|
|
|
197
272
|
if not path:
|
|
198
273
|
return redirect(f"/services/{service}/", http.HTTPStatus.PERMANENT_REDIRECT)
|
|
199
274
|
|
|
200
|
-
|
|
275
|
+
# Get service from all registered experiments
|
|
276
|
+
scheduler = Scheduler.instance()
|
|
277
|
+
service_obj = None
|
|
278
|
+
for xp in scheduler.experiments.values():
|
|
279
|
+
service_obj = xp.services.get(service, None)
|
|
280
|
+
if service_obj:
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
service = service_obj
|
|
201
284
|
if service is None:
|
|
202
285
|
return Response(f"Service {service} not found", http.HTTPStatus.NOT_FOUND)
|
|
203
286
|
|
|
@@ -210,7 +293,8 @@ def start_app(server: "Server"):
|
|
|
210
293
|
progress = float(request.args.get("progress", 0.0))
|
|
211
294
|
|
|
212
295
|
try:
|
|
213
|
-
|
|
296
|
+
scheduler = Scheduler.instance()
|
|
297
|
+
scheduler.jobs[jobid].set_progress(
|
|
214
298
|
level,
|
|
215
299
|
progress,
|
|
216
300
|
request.args.get("desc", None),
|
|
@@ -256,10 +340,17 @@ def start_app(server: "Server"):
|
|
|
256
340
|
|
|
257
341
|
datapath = "data/%s" % path
|
|
258
342
|
logging.debug("Looking for %s", datapath)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
package_files = files("experimaestro.server")
|
|
346
|
+
resource_file = package_files / datapath
|
|
347
|
+
if resource_file.is_file():
|
|
348
|
+
mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
|
|
349
|
+
content = resource_file.read_bytes()
|
|
350
|
+
return Response(content, mimetype=mimetype)
|
|
351
|
+
except (FileNotFoundError, KeyError):
|
|
352
|
+
pass
|
|
353
|
+
|
|
263
354
|
return Response("Page not found", status=404)
|
|
264
355
|
|
|
265
356
|
# Start the app
|
|
@@ -292,7 +383,36 @@ def start_app(server: "Server"):
|
|
|
292
383
|
|
|
293
384
|
|
|
294
385
|
class Server:
|
|
295
|
-
|
|
386
|
+
_instance: ClassVar[Optional["Server"]] = None
|
|
387
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def instance(settings: ServerSettings = None, state_provider=None) -> "Server":
|
|
391
|
+
"""Get or create the global server instance
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
settings: Server settings (optional)
|
|
395
|
+
state_provider: WorkspaceStateProvider instance (required)
|
|
396
|
+
"""
|
|
397
|
+
if Server._instance is None:
|
|
398
|
+
with Server._lock:
|
|
399
|
+
if Server._instance is None:
|
|
400
|
+
if settings is None:
|
|
401
|
+
from experimaestro.settings import get_settings
|
|
402
|
+
|
|
403
|
+
settings = get_settings().server
|
|
404
|
+
|
|
405
|
+
# State provider is required - it should be passed explicitly
|
|
406
|
+
if state_provider is None:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
"state_provider parameter is required. "
|
|
409
|
+
"Get it via WorkspaceStateProvider.get_instance(workspace.path)"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
Server._instance = Server(settings, state_provider)
|
|
413
|
+
return Server._instance
|
|
414
|
+
|
|
415
|
+
def __init__(self, settings: ServerSettings, state_provider):
|
|
296
416
|
if settings.autohost == "fqdn":
|
|
297
417
|
settings.host = socket.getfqdn()
|
|
298
418
|
logging.info("Auto host name (fqdn): %s", settings.host)
|
|
@@ -307,8 +427,8 @@ class Server:
|
|
|
307
427
|
|
|
308
428
|
self.host = settings.host or "127.0.0.1"
|
|
309
429
|
self.port = settings.port
|
|
310
|
-
self.scheduler = scheduler
|
|
311
430
|
self.token = settings.token or uuid.uuid4().hex
|
|
431
|
+
self.state_provider = state_provider
|
|
312
432
|
self.instance = None
|
|
313
433
|
self.running = False
|
|
314
434
|
self.cv_running = threading.Condition()
|
|
@@ -331,13 +451,14 @@ class Server:
|
|
|
331
451
|
pass
|
|
332
452
|
|
|
333
453
|
def start(self):
|
|
334
|
-
"""Start the websocket server in a
|
|
454
|
+
"""Start the websocket server in a daemon thread"""
|
|
335
455
|
logging.info("Starting the web server")
|
|
336
456
|
|
|
337
457
|
# Avoids clutering
|
|
338
458
|
logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
|
|
339
459
|
|
|
340
|
-
self.thread = threading.Thread(target=start_app, args=(self,))
|
|
460
|
+
self.thread = threading.Thread(target=start_app, args=(self,), daemon=True)
|
|
461
|
+
self.thread.start()
|
|
341
462
|
|
|
342
463
|
# Wait until we really started
|
|
343
464
|
while True:
|
|
@@ -22753,131 +22753,6 @@ readers do not read off random characters that represent icons */
|
|
|
22753
22753
|
font-weight: bold;
|
|
22754
22754
|
}
|
|
22755
22755
|
|
|
22756
|
-
@font-face {
|
|
22757
|
-
font-family: "Material Icons";
|
|
22758
|
-
font-style: normal;
|
|
22759
|
-
font-weight: 400;
|
|
22760
|
-
font-display: block;
|
|
22761
|
-
src: url(/0c35d18bf06992036b69.woff2) format("woff2"), url(/4d73cb90e394b34b7670.woff) format("woff");
|
|
22762
|
-
}
|
|
22763
|
-
.material-icons {
|
|
22764
|
-
font-family: "Material Icons";
|
|
22765
|
-
font-weight: normal;
|
|
22766
|
-
font-style: normal;
|
|
22767
|
-
font-size: 24px;
|
|
22768
|
-
line-height: 1;
|
|
22769
|
-
letter-spacing: normal;
|
|
22770
|
-
text-transform: none;
|
|
22771
|
-
display: inline-block;
|
|
22772
|
-
white-space: nowrap;
|
|
22773
|
-
word-wrap: normal;
|
|
22774
|
-
direction: ltr;
|
|
22775
|
-
-webkit-font-smoothing: antialiased;
|
|
22776
|
-
-moz-osx-font-smoothing: grayscale;
|
|
22777
|
-
text-rendering: optimizeLegibility;
|
|
22778
|
-
font-feature-settings: "liga";
|
|
22779
|
-
}
|
|
22780
|
-
|
|
22781
|
-
@font-face {
|
|
22782
|
-
font-family: "Material Icons Outlined";
|
|
22783
|
-
font-style: normal;
|
|
22784
|
-
font-weight: 400;
|
|
22785
|
-
font-display: block;
|
|
22786
|
-
src: url(/6f420cf17cc0d7676fad.woff2) format("woff2"), url(/f882956fd323fd322f31.woff) format("woff");
|
|
22787
|
-
}
|
|
22788
|
-
.material-icons-outlined {
|
|
22789
|
-
font-family: "Material Icons Outlined";
|
|
22790
|
-
font-weight: normal;
|
|
22791
|
-
font-style: normal;
|
|
22792
|
-
font-size: 24px;
|
|
22793
|
-
line-height: 1;
|
|
22794
|
-
letter-spacing: normal;
|
|
22795
|
-
text-transform: none;
|
|
22796
|
-
display: inline-block;
|
|
22797
|
-
white-space: nowrap;
|
|
22798
|
-
word-wrap: normal;
|
|
22799
|
-
direction: ltr;
|
|
22800
|
-
-webkit-font-smoothing: antialiased;
|
|
22801
|
-
-moz-osx-font-smoothing: grayscale;
|
|
22802
|
-
text-rendering: optimizeLegibility;
|
|
22803
|
-
font-feature-settings: "liga";
|
|
22804
|
-
}
|
|
22805
|
-
|
|
22806
|
-
@font-face {
|
|
22807
|
-
font-family: "Material Icons Round";
|
|
22808
|
-
font-style: normal;
|
|
22809
|
-
font-weight: 400;
|
|
22810
|
-
font-display: block;
|
|
22811
|
-
src: url(/c380809fd3677d7d6903.woff2) format("woff2"), url(/5d681e2edae8c60630db.woff) format("woff");
|
|
22812
|
-
}
|
|
22813
|
-
.material-icons-round {
|
|
22814
|
-
font-family: "Material Icons Round";
|
|
22815
|
-
font-weight: normal;
|
|
22816
|
-
font-style: normal;
|
|
22817
|
-
font-size: 24px;
|
|
22818
|
-
line-height: 1;
|
|
22819
|
-
letter-spacing: normal;
|
|
22820
|
-
text-transform: none;
|
|
22821
|
-
display: inline-block;
|
|
22822
|
-
white-space: nowrap;
|
|
22823
|
-
word-wrap: normal;
|
|
22824
|
-
direction: ltr;
|
|
22825
|
-
-webkit-font-smoothing: antialiased;
|
|
22826
|
-
-moz-osx-font-smoothing: grayscale;
|
|
22827
|
-
text-rendering: optimizeLegibility;
|
|
22828
|
-
font-feature-settings: "liga";
|
|
22829
|
-
}
|
|
22830
|
-
|
|
22831
|
-
@font-face {
|
|
22832
|
-
font-family: "Material Icons Sharp";
|
|
22833
|
-
font-style: normal;
|
|
22834
|
-
font-weight: 400;
|
|
22835
|
-
font-display: block;
|
|
22836
|
-
src: url(/219aa9140e099e6c72ed.woff2) format("woff2"), url(/3a4004a46a653d4b2166.woff) format("woff");
|
|
22837
|
-
}
|
|
22838
|
-
.material-icons-sharp {
|
|
22839
|
-
font-family: "Material Icons Sharp";
|
|
22840
|
-
font-weight: normal;
|
|
22841
|
-
font-style: normal;
|
|
22842
|
-
font-size: 24px;
|
|
22843
|
-
line-height: 1;
|
|
22844
|
-
letter-spacing: normal;
|
|
22845
|
-
text-transform: none;
|
|
22846
|
-
display: inline-block;
|
|
22847
|
-
white-space: nowrap;
|
|
22848
|
-
word-wrap: normal;
|
|
22849
|
-
direction: ltr;
|
|
22850
|
-
-webkit-font-smoothing: antialiased;
|
|
22851
|
-
-moz-osx-font-smoothing: grayscale;
|
|
22852
|
-
text-rendering: optimizeLegibility;
|
|
22853
|
-
font-feature-settings: "liga";
|
|
22854
|
-
}
|
|
22855
|
-
|
|
22856
|
-
@font-face {
|
|
22857
|
-
font-family: "Material Icons Two Tone";
|
|
22858
|
-
font-style: normal;
|
|
22859
|
-
font-weight: 400;
|
|
22860
|
-
font-display: block;
|
|
22861
|
-
src: url(/4ef4218c522f1eb6b5b1.woff2) format("woff2"), url(/3baa5b8f3469222b822d.woff) format("woff");
|
|
22862
|
-
}
|
|
22863
|
-
.material-icons-two-tone {
|
|
22864
|
-
font-family: "Material Icons Two Tone";
|
|
22865
|
-
font-weight: normal;
|
|
22866
|
-
font-style: normal;
|
|
22867
|
-
font-size: 24px;
|
|
22868
|
-
line-height: 1;
|
|
22869
|
-
letter-spacing: normal;
|
|
22870
|
-
text-transform: none;
|
|
22871
|
-
display: inline-block;
|
|
22872
|
-
white-space: nowrap;
|
|
22873
|
-
word-wrap: normal;
|
|
22874
|
-
direction: ltr;
|
|
22875
|
-
-webkit-font-smoothing: antialiased;
|
|
22876
|
-
-moz-osx-font-smoothing: grayscale;
|
|
22877
|
-
text-rendering: optimizeLegibility;
|
|
22878
|
-
font-feature-settings: "liga";
|
|
22879
|
-
}
|
|
22880
|
-
|
|
22881
22756
|
body {
|
|
22882
22757
|
font-size: 1rem;
|
|
22883
22758
|
}
|