experimaestro 2.0.0a8__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 +130 -5
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +20 -1
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +182 -46
- experimaestro/core/identifier.py +107 -6
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +542 -25
- experimaestro/core/objects/config_walk.py +20 -0
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +175 -38
- 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/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +63 -13
- experimaestro/progress.py +0 -2
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/base.py +489 -125
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +225 -30
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +216 -206
- experimaestro/scheduler/services.py +186 -12
- 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 +147 -57
- 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 +44 -5
- 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/test_slurm.py +80 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_file_progress_integration.py +1 -1
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_identifier.py +372 -41
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +3 -3
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +312 -5
- experimaestro/tests/test_outputs.py +2 -2
- experimaestro/tests/test_param.py +8 -12
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +0 -48
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -1
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +136 -0
- experimaestro/tests/test_tasks.py +107 -121
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +17 -13
- experimaestro/tests/test_types.py +123 -1
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +4 -2
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +1 -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/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-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +68 -38
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-2.0.0a8.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 -221
- 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-2.0.0a8.dist-info/RECORD +0 -166
- experimaestro-2.0.0a8.dist-info/entry_points.txt +0 -17
- {experimaestro-2.0.0a8.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
|
@@ -8,22 +8,9 @@ from experimaestro.scheduler.base import Job
|
|
|
8
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
12
|
|
|
13
|
-
|
|
14
|
-
from importlib.resources import files
|
|
15
|
-
|
|
16
|
-
pkg_resources = None
|
|
17
|
-
else:
|
|
18
|
-
try:
|
|
19
|
-
from importlib_resources import files
|
|
20
|
-
|
|
21
|
-
pkg_resources = None
|
|
22
|
-
except ImportError:
|
|
23
|
-
# Fallback to pkg_resources if importlib_resources not available
|
|
24
|
-
import pkg_resources
|
|
25
|
-
|
|
26
|
-
files = None
|
|
13
|
+
from importlib.resources import files
|
|
27
14
|
from experimaestro.scheduler import Scheduler, Listener as BaseListener
|
|
28
15
|
from experimaestro.scheduler.services import Service, ServiceListener
|
|
29
16
|
from experimaestro.settings import ServerSettings
|
|
@@ -61,6 +48,9 @@ def job_details(job):
|
|
|
61
48
|
|
|
62
49
|
|
|
63
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
|
+
|
|
64
54
|
return {
|
|
65
55
|
"jobId": job.identifier,
|
|
66
56
|
"taskId": job.name,
|
|
@@ -68,28 +58,50 @@ def job_create(job: Job):
|
|
|
68
58
|
"status": job.state.name.lower(),
|
|
69
59
|
"tags": list(job.tags.items()),
|
|
70
60
|
"progress": progress_state(job),
|
|
61
|
+
"experimentIds": experiment_ids, # Add experiment IDs
|
|
71
62
|
}
|
|
72
63
|
|
|
73
64
|
|
|
74
65
|
class Listener(BaseListener, ServiceListener):
|
|
75
|
-
def __init__(self,
|
|
76
|
-
self.scheduler = scheduler
|
|
66
|
+
def __init__(self, socketio, state_provider):
|
|
77
67
|
self.socketio = socketio
|
|
78
|
-
self.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 = {}
|
|
82
92
|
|
|
83
93
|
def job_submitted(self, job):
|
|
84
94
|
self.socketio.emit("job.add", job_create(job))
|
|
85
95
|
|
|
86
96
|
def job_state(self, job):
|
|
97
|
+
experiment_ids = [xp.workdir.name for xp in job.experiments]
|
|
87
98
|
self.socketio.emit(
|
|
88
99
|
"job.update",
|
|
89
100
|
{
|
|
90
101
|
"jobId": job.identifier,
|
|
91
102
|
"status": job.state.name.lower(),
|
|
92
103
|
"progress": progress_state(job),
|
|
104
|
+
"experimentIds": experiment_ids,
|
|
93
105
|
},
|
|
94
106
|
)
|
|
95
107
|
|
|
@@ -165,7 +177,7 @@ def start_app(server: "Server"):
|
|
|
165
177
|
|
|
166
178
|
logging.debug("Starting Flask server (SocketIO)...")
|
|
167
179
|
socketio = SocketIO(app, path="/api", async_mode="gevent")
|
|
168
|
-
listener = Listener(server.
|
|
180
|
+
listener = Listener(socketio, server.state_provider)
|
|
169
181
|
|
|
170
182
|
logging.debug("Starting Flask server (setting up socketio)...")
|
|
171
183
|
|
|
@@ -175,13 +187,48 @@ def start_app(server: "Server"):
|
|
|
175
187
|
raise ConnectionRefusedError("invalid token")
|
|
176
188
|
|
|
177
189
|
@socketio.on("refresh")
|
|
178
|
-
def handle_refresh():
|
|
179
|
-
for
|
|
180
|
-
|
|
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)
|
|
181
217
|
|
|
182
218
|
@socketio.on("job.details")
|
|
183
|
-
def handle_details(
|
|
184
|
-
|
|
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)
|
|
185
232
|
|
|
186
233
|
@socketio.on("services")
|
|
187
234
|
def handle_services_list():
|
|
@@ -196,14 +243,26 @@ def start_app(server: "Server"):
|
|
|
196
243
|
)
|
|
197
244
|
|
|
198
245
|
@socketio.on("job.kill")
|
|
199
|
-
def handle_job_kill(
|
|
200
|
-
job
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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")
|
|
207
266
|
|
|
208
267
|
logging.debug("Starting Flask server (setting up routes)...")
|
|
209
268
|
|
|
@@ -213,7 +272,15 @@ def start_app(server: "Server"):
|
|
|
213
272
|
if not path:
|
|
214
273
|
return redirect(f"/services/{service}/", http.HTTPStatus.PERMANENT_REDIRECT)
|
|
215
274
|
|
|
216
|
-
|
|
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
|
|
217
284
|
if service is None:
|
|
218
285
|
return Response(f"Service {service} not found", http.HTTPStatus.NOT_FOUND)
|
|
219
286
|
|
|
@@ -226,7 +293,8 @@ def start_app(server: "Server"):
|
|
|
226
293
|
progress = float(request.args.get("progress", 0.0))
|
|
227
294
|
|
|
228
295
|
try:
|
|
229
|
-
|
|
296
|
+
scheduler = Scheduler.instance()
|
|
297
|
+
scheduler.jobs[jobid].set_progress(
|
|
230
298
|
level,
|
|
231
299
|
progress,
|
|
232
300
|
request.args.get("desc", None),
|
|
@@ -273,24 +341,16 @@ def start_app(server: "Server"):
|
|
|
273
341
|
datapath = "data/%s" % path
|
|
274
342
|
logging.debug("Looking for %s", datapath)
|
|
275
343
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if resource_file.is_file():
|
|
281
|
-
mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
|
|
282
|
-
content = resource_file.read_bytes()
|
|
283
|
-
return Response(content, mimetype=mimetype)
|
|
284
|
-
except (FileNotFoundError, KeyError):
|
|
285
|
-
pass
|
|
286
|
-
elif pkg_resources is not None:
|
|
287
|
-
# Fallback to pkg_resources
|
|
288
|
-
if pkg_resources.resource_exists("experimaestro.server", datapath):
|
|
344
|
+
try:
|
|
345
|
+
package_files = files("experimaestro.server")
|
|
346
|
+
resource_file = package_files / datapath
|
|
347
|
+
if resource_file.is_file():
|
|
289
348
|
mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
|
|
290
|
-
content =
|
|
291
|
-
"experimaestro.server", datapath
|
|
292
|
-
)
|
|
349
|
+
content = resource_file.read_bytes()
|
|
293
350
|
return Response(content, mimetype=mimetype)
|
|
351
|
+
except (FileNotFoundError, KeyError):
|
|
352
|
+
pass
|
|
353
|
+
|
|
294
354
|
return Response("Page not found", status=404)
|
|
295
355
|
|
|
296
356
|
# Start the app
|
|
@@ -323,7 +383,36 @@ def start_app(server: "Server"):
|
|
|
323
383
|
|
|
324
384
|
|
|
325
385
|
class Server:
|
|
326
|
-
|
|
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):
|
|
327
416
|
if settings.autohost == "fqdn":
|
|
328
417
|
settings.host = socket.getfqdn()
|
|
329
418
|
logging.info("Auto host name (fqdn): %s", settings.host)
|
|
@@ -338,8 +427,8 @@ class Server:
|
|
|
338
427
|
|
|
339
428
|
self.host = settings.host or "127.0.0.1"
|
|
340
429
|
self.port = settings.port
|
|
341
|
-
self.scheduler = scheduler
|
|
342
430
|
self.token = settings.token or uuid.uuid4().hex
|
|
431
|
+
self.state_provider = state_provider
|
|
343
432
|
self.instance = None
|
|
344
433
|
self.running = False
|
|
345
434
|
self.cv_running = threading.Condition()
|
|
@@ -362,13 +451,14 @@ class Server:
|
|
|
362
451
|
pass
|
|
363
452
|
|
|
364
453
|
def start(self):
|
|
365
|
-
"""Start the websocket server in a
|
|
454
|
+
"""Start the websocket server in a daemon thread"""
|
|
366
455
|
logging.info("Starting the web server")
|
|
367
456
|
|
|
368
457
|
# Avoids clutering
|
|
369
458
|
logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
|
|
370
459
|
|
|
371
|
-
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()
|
|
372
462
|
|
|
373
463
|
# Wait until we really started
|
|
374
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
|
}
|