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.

Files changed (154) hide show
  1. experimaestro/__init__.py +12 -5
  2. experimaestro/cli/__init__.py +393 -134
  3. experimaestro/cli/filter.py +48 -23
  4. experimaestro/cli/jobs.py +253 -71
  5. experimaestro/cli/refactor.py +1 -2
  6. experimaestro/commandline.py +7 -4
  7. experimaestro/connectors/__init__.py +9 -1
  8. experimaestro/connectors/local.py +43 -3
  9. experimaestro/core/arguments.py +18 -18
  10. experimaestro/core/identifier.py +11 -11
  11. experimaestro/core/objects/config.py +96 -39
  12. experimaestro/core/objects/config_walk.py +3 -3
  13. experimaestro/core/{subparameters.py → partial.py} +16 -16
  14. experimaestro/core/partial_lock.py +394 -0
  15. experimaestro/core/types.py +12 -15
  16. experimaestro/dynamic.py +290 -0
  17. experimaestro/experiments/__init__.py +6 -2
  18. experimaestro/experiments/cli.py +223 -52
  19. experimaestro/experiments/configuration.py +24 -0
  20. experimaestro/generators.py +5 -5
  21. experimaestro/ipc.py +118 -1
  22. experimaestro/launcherfinder/__init__.py +2 -2
  23. experimaestro/launcherfinder/registry.py +6 -7
  24. experimaestro/launcherfinder/specs.py +2 -9
  25. experimaestro/launchers/slurm/__init__.py +2 -2
  26. experimaestro/launchers/slurm/base.py +62 -0
  27. experimaestro/locking.py +957 -1
  28. experimaestro/notifications.py +89 -201
  29. experimaestro/progress.py +63 -366
  30. experimaestro/rpyc.py +0 -2
  31. experimaestro/run.py +29 -2
  32. experimaestro/scheduler/__init__.py +8 -1
  33. experimaestro/scheduler/base.py +650 -53
  34. experimaestro/scheduler/dependencies.py +20 -16
  35. experimaestro/scheduler/experiment.py +764 -169
  36. experimaestro/scheduler/interfaces.py +338 -96
  37. experimaestro/scheduler/jobs.py +58 -20
  38. experimaestro/scheduler/remote/__init__.py +31 -0
  39. experimaestro/scheduler/remote/adaptive_sync.py +265 -0
  40. experimaestro/scheduler/remote/client.py +928 -0
  41. experimaestro/scheduler/remote/protocol.py +282 -0
  42. experimaestro/scheduler/remote/server.py +447 -0
  43. experimaestro/scheduler/remote/sync.py +144 -0
  44. experimaestro/scheduler/services.py +186 -35
  45. experimaestro/scheduler/state_provider.py +811 -2157
  46. experimaestro/scheduler/state_status.py +1247 -0
  47. experimaestro/scheduler/transient.py +31 -0
  48. experimaestro/scheduler/workspace.py +1 -1
  49. experimaestro/scheduler/workspace_state_provider.py +1273 -0
  50. experimaestro/scriptbuilder.py +4 -4
  51. experimaestro/settings.py +36 -0
  52. experimaestro/tests/conftest.py +33 -5
  53. experimaestro/tests/connectors/bin/executable.py +1 -1
  54. experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
  55. experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
  56. experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
  57. experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
  58. experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
  59. experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
  60. experimaestro/tests/launchers/bin/test.py +1 -0
  61. experimaestro/tests/launchers/test_slurm.py +9 -9
  62. experimaestro/tests/partial_reschedule.py +46 -0
  63. experimaestro/tests/restart.py +3 -3
  64. experimaestro/tests/restart_main.py +1 -0
  65. experimaestro/tests/scripts/notifyandwait.py +1 -0
  66. experimaestro/tests/task_partial.py +38 -0
  67. experimaestro/tests/task_tokens.py +2 -2
  68. experimaestro/tests/tasks/test_dynamic.py +6 -6
  69. experimaestro/tests/test_dependencies.py +3 -3
  70. experimaestro/tests/test_deprecated.py +15 -15
  71. experimaestro/tests/test_dynamic_locking.py +317 -0
  72. experimaestro/tests/test_environment.py +24 -14
  73. experimaestro/tests/test_experiment.py +171 -36
  74. experimaestro/tests/test_identifier.py +25 -25
  75. experimaestro/tests/test_identifier_stability.py +3 -5
  76. experimaestro/tests/test_multitoken.py +2 -4
  77. experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
  78. experimaestro/tests/test_partial_paths.py +81 -138
  79. experimaestro/tests/test_pre_experiment.py +219 -0
  80. experimaestro/tests/test_progress.py +2 -8
  81. experimaestro/tests/test_remote_state.py +1132 -0
  82. experimaestro/tests/test_stray_jobs.py +261 -0
  83. experimaestro/tests/test_tasks.py +1 -2
  84. experimaestro/tests/test_token_locking.py +52 -67
  85. experimaestro/tests/test_tokens.py +5 -6
  86. experimaestro/tests/test_transient.py +225 -0
  87. experimaestro/tests/test_workspace_state_provider.py +768 -0
  88. experimaestro/tests/token_reschedule.py +1 -3
  89. experimaestro/tests/utils.py +2 -7
  90. experimaestro/tokens.py +227 -372
  91. experimaestro/tools/diff.py +1 -0
  92. experimaestro/tools/documentation.py +4 -5
  93. experimaestro/tools/jobs.py +1 -2
  94. experimaestro/tui/app.py +459 -1895
  95. experimaestro/tui/app.tcss +162 -0
  96. experimaestro/tui/dialogs.py +172 -0
  97. experimaestro/tui/log_viewer.py +253 -3
  98. experimaestro/tui/messages.py +137 -0
  99. experimaestro/tui/utils.py +54 -0
  100. experimaestro/tui/widgets/__init__.py +23 -0
  101. experimaestro/tui/widgets/experiments.py +468 -0
  102. experimaestro/tui/widgets/global_services.py +238 -0
  103. experimaestro/tui/widgets/jobs.py +972 -0
  104. experimaestro/tui/widgets/log.py +156 -0
  105. experimaestro/tui/widgets/orphans.py +363 -0
  106. experimaestro/tui/widgets/runs.py +185 -0
  107. experimaestro/tui/widgets/services.py +314 -0
  108. experimaestro/tui/widgets/stray_jobs.py +528 -0
  109. experimaestro/utils/__init__.py +1 -1
  110. experimaestro/utils/environment.py +105 -22
  111. experimaestro/utils/fswatcher.py +124 -0
  112. experimaestro/utils/jobs.py +1 -2
  113. experimaestro/utils/jupyter.py +1 -2
  114. experimaestro/utils/logging.py +72 -0
  115. experimaestro/version.py +2 -2
  116. experimaestro/webui/__init__.py +9 -0
  117. experimaestro/webui/app.py +117 -0
  118. experimaestro/{server → webui}/data/index.css +66 -11
  119. experimaestro/webui/data/index.css.map +1 -0
  120. experimaestro/{server → webui}/data/index.js +82763 -87217
  121. experimaestro/webui/data/index.js.map +1 -0
  122. experimaestro/webui/routes/__init__.py +5 -0
  123. experimaestro/webui/routes/auth.py +53 -0
  124. experimaestro/webui/routes/proxy.py +117 -0
  125. experimaestro/webui/server.py +200 -0
  126. experimaestro/webui/state_bridge.py +152 -0
  127. experimaestro/webui/websocket.py +413 -0
  128. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +8 -9
  129. experimaestro-2.0.0b17.dist-info/RECORD +219 -0
  130. experimaestro/cli/progress.py +0 -269
  131. experimaestro/scheduler/state.py +0 -75
  132. experimaestro/scheduler/state_db.py +0 -388
  133. experimaestro/scheduler/state_sync.py +0 -834
  134. experimaestro/server/__init__.py +0 -467
  135. experimaestro/server/data/index.css.map +0 -1
  136. experimaestro/server/data/index.js.map +0 -1
  137. experimaestro/tests/test_cli_jobs.py +0 -615
  138. experimaestro/tests/test_file_progress.py +0 -425
  139. experimaestro/tests/test_file_progress_integration.py +0 -477
  140. experimaestro/tests/test_state_db.py +0 -434
  141. experimaestro-2.0.0b4.dist-info/RECORD +0 -181
  142. /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
  143. /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
  144. /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
  145. /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
  146. /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
  147. /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
  148. /experimaestro/{server → webui}/data/favicon.ico +0 -0
  149. /experimaestro/{server → webui}/data/index.html +0 -0
  150. /experimaestro/{server → webui}/data/login.html +0 -0
  151. /experimaestro/{server → webui}/data/manifest.json +0 -0
  152. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
  153. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
  154. {experimaestro-2.0.0b4.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.0b4
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: flask-socketio<6,>=5.3
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
@@ -82,11 +81,11 @@ The full documentation can be read by going to the following URL: [https://exper
82
81
 
83
82
  ## Textual interface (new in v2)
84
83
 
85
- ![Experiment screen](docs/img/tui-experiments.png)
84
+ ![Experiment screen](docs/source/img/tui-experiments.png)
86
85
 
87
- ![Jobs screen](docs/img/tui-jobs.png)
86
+ ![Jobs screen](docs/source/img/tui-jobs.png)
88
87
 
89
- ![Log screen](docs/img/tui-logs.png)
88
+ ![Log screen](docs/source/img/tui-logs.png)
90
89
 
91
90
  # Install
92
91