experimaestro 2.0.0b8__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 +239 -126
- 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 +217 -50
- 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 +629 -53
- experimaestro/scheduler/dependencies.py +20 -16
- experimaestro/scheduler/experiment.py +732 -167
- experimaestro/scheduler/interfaces.py +316 -101
- experimaestro/scheduler/jobs.py +58 -20
- experimaestro/scheduler/remote/adaptive_sync.py +265 -0
- experimaestro/scheduler/remote/client.py +171 -117
- experimaestro/scheduler/remote/protocol.py +8 -193
- experimaestro/scheduler/remote/server.py +95 -71
- experimaestro/scheduler/services.py +53 -28
- experimaestro/scheduler/state_provider.py +663 -2430
- 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 +560 -99
- 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 +438 -1966
- 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.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +5 -6
- 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 -437
- experimaestro/scheduler/state_sync.py +0 -891
- 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.0b8.dist-info/RECORD +0 -187
- /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.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""WebSocket handler for real-time communication
|
|
2
|
+
|
|
3
|
+
Manages WebSocket connections and message routing.
|
|
4
|
+
Uses native WebSocket with JSON protocol.
|
|
5
|
+
|
|
6
|
+
Serialization is consistent with SSHStateProviderServer, using db_state_dict()
|
|
7
|
+
for Job/Experiment serialization, then transforming to frontend format.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Dict, List, Optional, Set
|
|
13
|
+
|
|
14
|
+
from fastapi import WebSocket
|
|
15
|
+
|
|
16
|
+
from experimaestro.scheduler.state_provider import StateProvider
|
|
17
|
+
from experimaestro.scheduler.base import Scheduler, Job
|
|
18
|
+
from experimaestro.scheduler.jobs import JobDependency
|
|
19
|
+
from experimaestro.scheduler.interfaces import BaseJob, BaseExperiment, BaseService
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("xpm.webui.websocket")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Serialization helpers - Transform db_state_dict to frontend format
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def job_db_to_frontend(db_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
30
|
+
"""Transform job db_state_dict to frontend format
|
|
31
|
+
|
|
32
|
+
db_state_dict format (snake_case):
|
|
33
|
+
identifier, task_id, path, state, submittime, starttime, endtime,
|
|
34
|
+
progress, exit_code, retry_count, failure_reason
|
|
35
|
+
|
|
36
|
+
Frontend format (camelCase):
|
|
37
|
+
jobId, taskId, locator, status, submitted, start, end,
|
|
38
|
+
tags, progress, experimentIds, dependsOn
|
|
39
|
+
"""
|
|
40
|
+
return {
|
|
41
|
+
"jobId": db_dict.get("identifier"),
|
|
42
|
+
"taskId": db_dict.get("task_id"),
|
|
43
|
+
"locator": db_dict.get("path") or "",
|
|
44
|
+
"status": (db_dict.get("state") or "unknown").lower(),
|
|
45
|
+
"submitted": db_dict.get("submittime") or "",
|
|
46
|
+
"start": db_dict.get("starttime") or "",
|
|
47
|
+
"end": db_dict.get("endtime") or "",
|
|
48
|
+
"tags": db_dict.get("tags", []),
|
|
49
|
+
"progress": db_dict.get("progress", []),
|
|
50
|
+
"experimentIds": db_dict.get("experiment_ids", []),
|
|
51
|
+
"dependsOn": db_dict.get("depends_on", []),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def experiment_db_to_frontend(db_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
56
|
+
"""Transform experiment db_state_dict to frontend format"""
|
|
57
|
+
return {
|
|
58
|
+
"experiment_id": db_dict.get("experiment_id"),
|
|
59
|
+
"workdir": db_dict.get("workdir"),
|
|
60
|
+
"current_run_id": db_dict.get("current_run_id"),
|
|
61
|
+
"total_jobs": db_dict.get("total_jobs", 0),
|
|
62
|
+
"finished_jobs": db_dict.get("finished_jobs", 0),
|
|
63
|
+
"failed_jobs": db_dict.get("failed_jobs", 0),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def service_db_to_frontend(db_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
68
|
+
"""Transform service db_state_dict to frontend format"""
|
|
69
|
+
return {
|
|
70
|
+
"id": db_dict.get("service_id"),
|
|
71
|
+
"description": db_dict.get("description"),
|
|
72
|
+
"state": db_dict.get("state"),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def serialize_progress(progress: List) -> List[Dict[str, Any]]:
|
|
77
|
+
"""Convert progress list to JSON-serializable format
|
|
78
|
+
|
|
79
|
+
Handles both LevelInformation objects and plain dicts.
|
|
80
|
+
"""
|
|
81
|
+
result = []
|
|
82
|
+
for item in progress:
|
|
83
|
+
if hasattr(item, "level"):
|
|
84
|
+
# LevelInformation object
|
|
85
|
+
result.append(
|
|
86
|
+
{
|
|
87
|
+
"level": item.level,
|
|
88
|
+
"progress": item.progress,
|
|
89
|
+
"desc": item.desc,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
elif isinstance(item, dict):
|
|
93
|
+
# Already a dict
|
|
94
|
+
result.append(item)
|
|
95
|
+
else:
|
|
96
|
+
# Unknown format, skip
|
|
97
|
+
pass
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def serialize_job(
|
|
102
|
+
job: BaseJob,
|
|
103
|
+
tags: List = None,
|
|
104
|
+
depends_on: List = None,
|
|
105
|
+
experiment_ids: List = None,
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
"""Serialize job using db_state_dict and transform to frontend format
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
job: Job or MockJob instance
|
|
111
|
+
tags: Optional tags list (for live jobs from scheduler)
|
|
112
|
+
depends_on: Optional dependencies list (for live jobs from scheduler)
|
|
113
|
+
experiment_ids: Optional experiment IDs (for live jobs from scheduler)
|
|
114
|
+
"""
|
|
115
|
+
db_dict = job.db_state_dict()
|
|
116
|
+
|
|
117
|
+
# Convert progress to JSON-serializable format
|
|
118
|
+
if "progress" in db_dict:
|
|
119
|
+
db_dict["progress"] = serialize_progress(db_dict["progress"])
|
|
120
|
+
|
|
121
|
+
# Add additional fields not in db_state_dict
|
|
122
|
+
if tags is not None:
|
|
123
|
+
db_dict["tags"] = tags
|
|
124
|
+
if depends_on is not None:
|
|
125
|
+
db_dict["depends_on"] = depends_on
|
|
126
|
+
if experiment_ids is not None:
|
|
127
|
+
db_dict["experiment_ids"] = experiment_ids
|
|
128
|
+
|
|
129
|
+
return job_db_to_frontend(db_dict)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def serialize_live_job(job: Job) -> Dict[str, Any]:
|
|
133
|
+
"""Serialize a live Job from scheduler with full metadata"""
|
|
134
|
+
# Get experiment IDs
|
|
135
|
+
experiment_ids = [xp.workdir.name for xp in job.experiments]
|
|
136
|
+
|
|
137
|
+
# Get dependencies
|
|
138
|
+
depends_on = [
|
|
139
|
+
dep.origin.identifier
|
|
140
|
+
for dep in job.dependencies
|
|
141
|
+
if isinstance(dep, JobDependency)
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
# Get tags
|
|
145
|
+
tags = list(job.tags.items())
|
|
146
|
+
|
|
147
|
+
return serialize_job(
|
|
148
|
+
job,
|
|
149
|
+
tags=tags,
|
|
150
|
+
depends_on=depends_on,
|
|
151
|
+
experiment_ids=experiment_ids,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def serialize_live_job_update(job: Job) -> Dict[str, Any]:
|
|
156
|
+
"""Serialize a live Job update (partial data)"""
|
|
157
|
+
experiment_ids = [xp.workdir.name for xp in job.experiments]
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"jobId": job.identifier,
|
|
161
|
+
"status": job.state.name.lower(),
|
|
162
|
+
"progress": serialize_progress(job.progress),
|
|
163
|
+
"experimentIds": experiment_ids,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def serialize_experiment(exp: BaseExperiment) -> Dict[str, Any]:
|
|
168
|
+
"""Serialize experiment using db_state_dict and transform to frontend format"""
|
|
169
|
+
db_dict = exp.db_state_dict()
|
|
170
|
+
return experiment_db_to_frontend(db_dict)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def serialize_service(service: BaseService) -> Dict[str, Any]:
|
|
174
|
+
"""Serialize service using db_state_dict and transform to frontend format"""
|
|
175
|
+
db_dict = service.db_state_dict()
|
|
176
|
+
return service_db_to_frontend(db_dict)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# =============================================================================
|
|
180
|
+
# WebSocket Handler
|
|
181
|
+
# =============================================================================
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class WebSocketHandler:
|
|
185
|
+
"""Manages WebSocket connections and message routing
|
|
186
|
+
|
|
187
|
+
Message Protocol (JSON):
|
|
188
|
+
Client -> Server:
|
|
189
|
+
{"type": "refresh", "payload": {"experimentId": "..."}}
|
|
190
|
+
{"type": "experiments"}
|
|
191
|
+
{"type": "services"}
|
|
192
|
+
{"type": "job.details", "payload": {"jobId": "...", "experimentId": "..."}}
|
|
193
|
+
{"type": "job.kill", "payload": {"jobId": "...", "experimentId": "..."}}
|
|
194
|
+
{"type": "quit"}
|
|
195
|
+
|
|
196
|
+
Server -> Client:
|
|
197
|
+
{"type": "job.add", "payload": {...}}
|
|
198
|
+
{"type": "job.update", "payload": {...}}
|
|
199
|
+
{"type": "experiment.add", "payload": {...}}
|
|
200
|
+
{"type": "service.add", "payload": {...}}
|
|
201
|
+
{"type": "service.update", "payload": {...}}
|
|
202
|
+
{"type": "error", "payload": {"message": "..."}}
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(self, state_provider: StateProvider, token: str):
|
|
206
|
+
"""Initialize WebSocket handler
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
state_provider: StateProvider for data access
|
|
210
|
+
token: Authentication token
|
|
211
|
+
"""
|
|
212
|
+
self.state_provider = state_provider
|
|
213
|
+
self.token = token
|
|
214
|
+
self.connections: Set[WebSocket] = set()
|
|
215
|
+
self._lock = asyncio.Lock()
|
|
216
|
+
|
|
217
|
+
# Check if we have a scheduler (active experiment mode)
|
|
218
|
+
self._scheduler: Optional[Scheduler] = None
|
|
219
|
+
if isinstance(state_provider, Scheduler):
|
|
220
|
+
self._scheduler = state_provider
|
|
221
|
+
|
|
222
|
+
async def connect(self, websocket: WebSocket):
|
|
223
|
+
"""Handle new WebSocket connection
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
websocket: FastAPI WebSocket connection
|
|
227
|
+
"""
|
|
228
|
+
# Accept connection first
|
|
229
|
+
await websocket.accept()
|
|
230
|
+
|
|
231
|
+
# Validate token from query params or cookies
|
|
232
|
+
token = websocket.query_params.get("token")
|
|
233
|
+
if not token:
|
|
234
|
+
# Try to get from cookies
|
|
235
|
+
cookies = websocket.cookies
|
|
236
|
+
token = cookies.get("experimaestro_token")
|
|
237
|
+
|
|
238
|
+
if token != self.token:
|
|
239
|
+
await websocket.send_json(
|
|
240
|
+
{"type": "error", "payload": {"message": "Invalid token"}}
|
|
241
|
+
)
|
|
242
|
+
await websocket.close(code=1008, reason="Invalid token")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
async with self._lock:
|
|
246
|
+
self.connections.add(websocket)
|
|
247
|
+
|
|
248
|
+
logger.info("WebSocket client connected (total: %d)", len(self.connections))
|
|
249
|
+
|
|
250
|
+
async def disconnect(self, websocket: WebSocket):
|
|
251
|
+
"""Handle WebSocket disconnection"""
|
|
252
|
+
async with self._lock:
|
|
253
|
+
self.connections.discard(websocket)
|
|
254
|
+
|
|
255
|
+
logger.info("WebSocket client disconnected (total: %d)", len(self.connections))
|
|
256
|
+
|
|
257
|
+
async def handle_message(self, websocket: WebSocket, message: Dict[str, Any]):
|
|
258
|
+
"""Route incoming message to appropriate handler
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
websocket: Source WebSocket connection
|
|
262
|
+
message: Parsed JSON message with 'type' and optional 'payload'
|
|
263
|
+
"""
|
|
264
|
+
msg_type = message.get("type")
|
|
265
|
+
payload = message.get("payload", {})
|
|
266
|
+
|
|
267
|
+
handlers = {
|
|
268
|
+
"refresh": self._handle_refresh,
|
|
269
|
+
"experiments": self._handle_experiments,
|
|
270
|
+
"services": self._handle_services,
|
|
271
|
+
"job.details": self._handle_job_details,
|
|
272
|
+
"job.kill": self._handle_job_kill,
|
|
273
|
+
"quit": self._handle_quit,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
handler = handlers.get(msg_type)
|
|
277
|
+
if handler:
|
|
278
|
+
try:
|
|
279
|
+
await handler(websocket, payload)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.error("Error handling message %s: %s", msg_type, e)
|
|
282
|
+
await websocket.send_json(
|
|
283
|
+
{"type": "error", "payload": {"message": str(e)}}
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
logger.warning("Unknown message type: %s", msg_type)
|
|
287
|
+
|
|
288
|
+
async def broadcast(self, msg_type: str, payload: Dict[str, Any]):
|
|
289
|
+
"""Broadcast message to all connected clients
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
msg_type: Message type (e.g., "job.add", "job.update")
|
|
293
|
+
payload: Message payload
|
|
294
|
+
"""
|
|
295
|
+
message = {"type": msg_type, "payload": payload}
|
|
296
|
+
disconnected = []
|
|
297
|
+
|
|
298
|
+
async with self._lock:
|
|
299
|
+
for websocket in self.connections:
|
|
300
|
+
try:
|
|
301
|
+
await websocket.send_json(message)
|
|
302
|
+
except Exception:
|
|
303
|
+
disconnected.append(websocket)
|
|
304
|
+
|
|
305
|
+
# Clean up disconnected clients
|
|
306
|
+
for ws in disconnected:
|
|
307
|
+
self.connections.discard(ws)
|
|
308
|
+
|
|
309
|
+
async def _handle_refresh(self, websocket: WebSocket, payload: Dict[str, Any]):
|
|
310
|
+
"""Handle refresh request - send all jobs for experiment(s)"""
|
|
311
|
+
experiment_id = payload.get("experimentId")
|
|
312
|
+
|
|
313
|
+
if experiment_id:
|
|
314
|
+
# Refresh specific experiment
|
|
315
|
+
jobs = self.state_provider.get_jobs(experiment_id)
|
|
316
|
+
for job in jobs:
|
|
317
|
+
await websocket.send_json(
|
|
318
|
+
{
|
|
319
|
+
"type": "job.add",
|
|
320
|
+
"payload": job_db_to_frontend(job.db_state_dict()),
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
# Refresh all experiments
|
|
325
|
+
if self._scheduler:
|
|
326
|
+
# Active mode: get jobs from scheduler (live Job objects)
|
|
327
|
+
for job in self._scheduler.jobs.values():
|
|
328
|
+
await websocket.send_json(
|
|
329
|
+
{"type": "job.add", "payload": serialize_live_job(job)}
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
# Monitoring mode: get from state provider (MockJob objects)
|
|
333
|
+
for exp in self.state_provider.get_experiments():
|
|
334
|
+
exp_id = exp.experiment_id
|
|
335
|
+
jobs = self.state_provider.get_jobs(exp_id)
|
|
336
|
+
for job in jobs:
|
|
337
|
+
await websocket.send_json(
|
|
338
|
+
{
|
|
339
|
+
"type": "job.add",
|
|
340
|
+
"payload": job_db_to_frontend(job.db_state_dict()),
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
async def _handle_experiments(self, websocket: WebSocket, payload: Dict[str, Any]):
|
|
345
|
+
"""Handle experiments request - send all experiments"""
|
|
346
|
+
experiments = self.state_provider.get_experiments()
|
|
347
|
+
for exp in experiments:
|
|
348
|
+
await websocket.send_json(
|
|
349
|
+
{"type": "experiment.add", "payload": serialize_experiment(exp)}
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
async def _handle_services(self, websocket: WebSocket, payload: Dict[str, Any]):
|
|
353
|
+
"""Handle services request - send all services"""
|
|
354
|
+
if self._scheduler:
|
|
355
|
+
# Get services from scheduler's experiments
|
|
356
|
+
for xp in self._scheduler.experiments.values():
|
|
357
|
+
for service in xp.services.values():
|
|
358
|
+
await websocket.send_json(
|
|
359
|
+
{"type": "service.add", "payload": serialize_service(service)}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
async def _handle_job_details(self, websocket: WebSocket, payload: Dict[str, Any]):
|
|
363
|
+
"""Handle job details request"""
|
|
364
|
+
job_id = payload.get("jobId")
|
|
365
|
+
experiment_id = payload.get("experimentId")
|
|
366
|
+
|
|
367
|
+
if self._scheduler and job_id in self._scheduler.jobs:
|
|
368
|
+
# Get from scheduler (live Job)
|
|
369
|
+
job = self._scheduler.jobs[job_id]
|
|
370
|
+
await websocket.send_json(
|
|
371
|
+
{"type": "job.update", "payload": serialize_live_job(job)}
|
|
372
|
+
)
|
|
373
|
+
elif experiment_id:
|
|
374
|
+
# Get from state provider (MockJob)
|
|
375
|
+
job = self.state_provider.get_job(job_id, experiment_id)
|
|
376
|
+
if job:
|
|
377
|
+
await websocket.send_json(
|
|
378
|
+
{
|
|
379
|
+
"type": "job.update",
|
|
380
|
+
"payload": job_db_to_frontend(job.db_state_dict()),
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
async def _handle_job_kill(self, websocket: WebSocket, payload: Dict[str, Any]):
|
|
385
|
+
"""Handle job kill request"""
|
|
386
|
+
job_id = payload.get("jobId")
|
|
387
|
+
experiment_id = payload.get("experimentId")
|
|
388
|
+
|
|
389
|
+
if self._scheduler and job_id in self._scheduler.jobs:
|
|
390
|
+
# Kill via scheduler
|
|
391
|
+
job = self._scheduler.jobs[job_id]
|
|
392
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
393
|
+
job.aio_process(), self._scheduler.loop
|
|
394
|
+
)
|
|
395
|
+
process = future.result()
|
|
396
|
+
if process is not None:
|
|
397
|
+
process.kill()
|
|
398
|
+
logger.info("Killed job %s", job_id)
|
|
399
|
+
else:
|
|
400
|
+
# Try state provider (may not be supported)
|
|
401
|
+
try:
|
|
402
|
+
self.state_provider.kill_job(experiment_id, job_id)
|
|
403
|
+
except NotImplementedError:
|
|
404
|
+
logger.warning("kill_job not supported for this state provider")
|
|
405
|
+
|
|
406
|
+
async def _handle_quit(self, websocket: WebSocket, payload: Dict[str, Any]):
|
|
407
|
+
"""Handle quit request from web interface"""
|
|
408
|
+
# Get server reference to trigger quit
|
|
409
|
+
# This is called from app context where server is available
|
|
410
|
+
from experimaestro.webui.server import WebUIServer
|
|
411
|
+
|
|
412
|
+
if WebUIServer._instance:
|
|
413
|
+
WebUIServer._instance.request_quit()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: experimaestro
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b17
|
|
4
4
|
Summary: Experimaestro is a computer science experiment manager
|
|
5
5
|
Project-URL: Homepage, https://github.com/experimaestro/experimaestro-python
|
|
6
6
|
Project-URL: Documentation, https://experimaestro-python.readthedocs.io/
|
|
@@ -26,16 +26,13 @@ Requires-Dist: attrs<24,>=23.1.0
|
|
|
26
26
|
Requires-Dist: click>=8
|
|
27
27
|
Requires-Dist: decorator<6,>=5
|
|
28
28
|
Requires-Dist: docstring-parser<1,>=0.15
|
|
29
|
+
Requires-Dist: fastapi<1,>=0.109
|
|
29
30
|
Requires-Dist: fasteners<1,>=0.19
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist: flask<3,>=2.3
|
|
32
|
-
Requires-Dist: gevent-websocket>=0.10
|
|
33
|
-
Requires-Dist: gevent>=25
|
|
31
|
+
Requires-Dist: httpx<1,>=0.26
|
|
34
32
|
Requires-Dist: huggingface-hub>0.17
|
|
35
33
|
Requires-Dist: humanfriendly>=10
|
|
36
34
|
Requires-Dist: marshmallow<4,>=3.20
|
|
37
35
|
Requires-Dist: omegaconf<3,>=2.3
|
|
38
|
-
Requires-Dist: peewee<4,>=3.17
|
|
39
36
|
Requires-Dist: psutil<8,>=7
|
|
40
37
|
Requires-Dist: pyparsing<4,>=3.1
|
|
41
38
|
Requires-Dist: pyperclip<2,>=1.8
|
|
@@ -48,6 +45,7 @@ Requires-Dist: termcolor<3,>=2.3
|
|
|
48
45
|
Requires-Dist: textual>=6
|
|
49
46
|
Requires-Dist: tqdm<5,>=4.66.1
|
|
50
47
|
Requires-Dist: typing-extensions>=4.2; python_version < '3.12'
|
|
48
|
+
Requires-Dist: uvicorn[standard]<1,>=0.27
|
|
51
49
|
Requires-Dist: watchdog>=2
|
|
52
50
|
Provides-Extra: dev
|
|
53
51
|
Requires-Dist: docutils>=0.18; extra == 'dev'
|
|
@@ -59,6 +57,7 @@ Requires-Dist: textual-dev>=1.8.0; extra == 'dev'
|
|
|
59
57
|
Provides-Extra: docs
|
|
60
58
|
Requires-Dist: myst-parser>=2.0; extra == 'docs'
|
|
61
59
|
Requires-Dist: sphinx-codeautolink>=0.15; extra == 'docs'
|
|
60
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
|
|
62
61
|
Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
|
|
63
62
|
Requires-Dist: sphinx>=6; extra == 'docs'
|
|
64
63
|
Provides-Extra: ssh
|