experimaestro 2.0.0a8__py3-none-any.whl → 2.0.0b8__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 +278 -7
- 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 +111 -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 +510 -125
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +256 -31
- experimaestro/scheduler/interfaces.py +501 -0
- experimaestro/scheduler/jobs.py +216 -206
- experimaestro/scheduler/remote/__init__.py +31 -0
- experimaestro/scheduler/remote/client.py +874 -0
- experimaestro/scheduler/remote/protocol.py +467 -0
- experimaestro/scheduler/remote/server.py +423 -0
- experimaestro/scheduler/remote/sync.py +144 -0
- experimaestro/scheduler/services.py +323 -23
- experimaestro/scheduler/state_db.py +437 -0
- experimaestro/scheduler/state_provider.py +2766 -0
- experimaestro/scheduler/state_sync.py +891 -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_remote_state.py +671 -0
- 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 +2395 -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.0b8.dist-info}/METADATA +68 -38
- experimaestro-2.0.0b8.dist-info/RECORD +187 -0
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b8.dist-info}/WHEEL +1 -1
- experimaestro-2.0.0b8.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.0b8.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 protocol utilities for SSH-based remote monitoring
|
|
2
|
+
|
|
3
|
+
This module provides JSON-RPC message types, serialization utilities,
|
|
4
|
+
and protocol constants for communication between SSHStateProviderServer
|
|
5
|
+
and SSHStateProviderClient.
|
|
6
|
+
|
|
7
|
+
Message format: Newline-delimited JSON (one JSON object per line)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from experimaestro.scheduler.interfaces import JobState
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("xpm.remote.protocol")
|
|
22
|
+
|
|
23
|
+
# JSON-RPC 2.0 version
|
|
24
|
+
JSONRPC_VERSION = "2.0"
|
|
25
|
+
|
|
26
|
+
# Standard JSON-RPC error codes
|
|
27
|
+
PARSE_ERROR = -32700
|
|
28
|
+
INVALID_REQUEST = -32600
|
|
29
|
+
METHOD_NOT_FOUND = -32601
|
|
30
|
+
INVALID_PARAMS = -32602
|
|
31
|
+
INTERNAL_ERROR = -32603
|
|
32
|
+
|
|
33
|
+
# Custom error codes
|
|
34
|
+
CONNECTION_ERROR = -32001
|
|
35
|
+
WORKSPACE_NOT_FOUND = -32002
|
|
36
|
+
PERMISSION_DENIED = -32003
|
|
37
|
+
TIMEOUT_ERROR = -32004
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NotificationMethod(str, Enum):
|
|
41
|
+
"""Server-to-client notification methods"""
|
|
42
|
+
|
|
43
|
+
EXPERIMENT_UPDATED = "notification.experiment_updated"
|
|
44
|
+
RUN_UPDATED = "notification.run_updated"
|
|
45
|
+
JOB_UPDATED = "notification.job_updated"
|
|
46
|
+
SERVICE_UPDATED = "notification.service_updated"
|
|
47
|
+
FILE_CHANGED = "notification.file_changed"
|
|
48
|
+
SHUTDOWN = "notification.shutdown"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RPCMethod(str, Enum):
|
|
52
|
+
"""Client-to-server RPC methods"""
|
|
53
|
+
|
|
54
|
+
GET_EXPERIMENTS = "get_experiments"
|
|
55
|
+
GET_EXPERIMENT = "get_experiment"
|
|
56
|
+
GET_EXPERIMENT_RUNS = "get_experiment_runs"
|
|
57
|
+
GET_JOBS = "get_jobs"
|
|
58
|
+
GET_JOB = "get_job"
|
|
59
|
+
GET_ALL_JOBS = "get_all_jobs"
|
|
60
|
+
GET_SERVICES = "get_services"
|
|
61
|
+
KILL_JOB = "kill_job"
|
|
62
|
+
CLEAN_JOB = "clean_job"
|
|
63
|
+
GET_SYNC_INFO = "get_sync_info"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class RPCError:
|
|
68
|
+
"""JSON-RPC error object"""
|
|
69
|
+
|
|
70
|
+
code: int
|
|
71
|
+
message: str
|
|
72
|
+
data: Optional[Any] = None
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> Dict:
|
|
75
|
+
result = {"code": self.code, "message": self.message}
|
|
76
|
+
if self.data is not None:
|
|
77
|
+
result["data"] = self.data
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, d: Dict) -> "RPCError":
|
|
82
|
+
return cls(code=d["code"], message=d["message"], data=d.get("data"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class RPCRequest:
|
|
87
|
+
"""JSON-RPC request message"""
|
|
88
|
+
|
|
89
|
+
method: str
|
|
90
|
+
params: Dict = field(default_factory=dict)
|
|
91
|
+
id: Optional[int] = None # None for notifications
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> Dict:
|
|
94
|
+
result = {"jsonrpc": JSONRPC_VERSION, "method": self.method}
|
|
95
|
+
if self.params:
|
|
96
|
+
result["params"] = self.params
|
|
97
|
+
if self.id is not None:
|
|
98
|
+
result["id"] = self.id
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def to_json(self) -> str:
|
|
102
|
+
return json.dumps(self.to_dict())
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(cls, d: Dict) -> "RPCRequest":
|
|
106
|
+
return cls(method=d["method"], params=d.get("params", {}), id=d.get("id"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class RPCResponse:
|
|
111
|
+
"""JSON-RPC response message"""
|
|
112
|
+
|
|
113
|
+
id: int
|
|
114
|
+
result: Optional[Any] = None
|
|
115
|
+
error: Optional[RPCError] = None
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict:
|
|
118
|
+
d = {"jsonrpc": JSONRPC_VERSION, "id": self.id}
|
|
119
|
+
if self.error is not None:
|
|
120
|
+
d["error"] = self.error.to_dict()
|
|
121
|
+
else:
|
|
122
|
+
d["result"] = self.result
|
|
123
|
+
return d
|
|
124
|
+
|
|
125
|
+
def to_json(self) -> str:
|
|
126
|
+
return json.dumps(self.to_dict())
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_dict(cls, d: Dict) -> "RPCResponse":
|
|
130
|
+
error = None
|
|
131
|
+
if "error" in d:
|
|
132
|
+
error = RPCError.from_dict(d["error"])
|
|
133
|
+
return cls(id=d["id"], result=d.get("result"), error=error)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class RPCNotification:
|
|
138
|
+
"""JSON-RPC notification (no id, no response expected)"""
|
|
139
|
+
|
|
140
|
+
method: str
|
|
141
|
+
params: Dict = field(default_factory=dict)
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> Dict:
|
|
144
|
+
result = {"jsonrpc": JSONRPC_VERSION, "method": self.method}
|
|
145
|
+
if self.params:
|
|
146
|
+
result["params"] = self.params
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
def to_json(self) -> str:
|
|
150
|
+
return json.dumps(self.to_dict())
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_dict(cls, d: Dict) -> "RPCNotification":
|
|
154
|
+
return cls(method=d["method"], params=d.get("params", {}))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_message(line: str) -> Union[RPCRequest, RPCResponse, RPCNotification]:
|
|
158
|
+
"""Parse a JSON-RPC message from a line of text
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
line: A single line of JSON text
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
RPCRequest, RPCResponse, or RPCNotification
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If the message is malformed
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
d = json.loads(line)
|
|
171
|
+
except json.JSONDecodeError as e:
|
|
172
|
+
raise ValueError(f"Invalid JSON: {e}")
|
|
173
|
+
|
|
174
|
+
if "jsonrpc" not in d or d["jsonrpc"] != JSONRPC_VERSION:
|
|
175
|
+
raise ValueError("Invalid or missing jsonrpc version")
|
|
176
|
+
|
|
177
|
+
# Response: has "id" and either "result" or "error"
|
|
178
|
+
if "result" in d or "error" in d:
|
|
179
|
+
return RPCResponse.from_dict(d)
|
|
180
|
+
|
|
181
|
+
# Request or notification: has "method"
|
|
182
|
+
if "method" in d:
|
|
183
|
+
if "id" in d:
|
|
184
|
+
return RPCRequest.from_dict(d)
|
|
185
|
+
else:
|
|
186
|
+
return RPCNotification.from_dict(d)
|
|
187
|
+
|
|
188
|
+
raise ValueError("Cannot determine message type")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def create_error_response(id: int, code: int, message: str, data: Any = None) -> str:
|
|
192
|
+
"""Create a JSON-RPC error response
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
id: Request ID
|
|
196
|
+
code: Error code
|
|
197
|
+
message: Error message
|
|
198
|
+
data: Optional additional data
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
JSON string
|
|
202
|
+
"""
|
|
203
|
+
response = RPCResponse(id=id, error=RPCError(code=code, message=message, data=data))
|
|
204
|
+
return response.to_json()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def create_success_response(id: int, result: Any) -> str:
|
|
208
|
+
"""Create a JSON-RPC success response
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
id: Request ID
|
|
212
|
+
result: Result data
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
JSON string
|
|
216
|
+
"""
|
|
217
|
+
response = RPCResponse(id=id, result=result)
|
|
218
|
+
return response.to_json()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_notification(method: Union[str, NotificationMethod], params: Dict) -> str:
|
|
222
|
+
"""Create a JSON-RPC notification
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
method: Notification method name
|
|
226
|
+
params: Notification parameters
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
JSON string
|
|
230
|
+
"""
|
|
231
|
+
if isinstance(method, NotificationMethod):
|
|
232
|
+
method = method.value
|
|
233
|
+
notification = RPCNotification(method=method, params=params)
|
|
234
|
+
return notification.to_json()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def create_request(method: Union[str, RPCMethod], params: Dict, id: int) -> str:
|
|
238
|
+
"""Create a JSON-RPC request
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
method: Method name
|
|
242
|
+
params: Request parameters
|
|
243
|
+
id: Request ID
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
JSON string
|
|
247
|
+
"""
|
|
248
|
+
if isinstance(method, RPCMethod):
|
|
249
|
+
method = method.value
|
|
250
|
+
request = RPCRequest(method=method, params=params, id=id)
|
|
251
|
+
return request.to_json()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# Serialization helpers for data types
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def serialize_datetime(dt) -> Optional[str]:
|
|
258
|
+
"""Serialize datetime or timestamp to ISO format string
|
|
259
|
+
|
|
260
|
+
Handles:
|
|
261
|
+
- None: returns None
|
|
262
|
+
- datetime: returns ISO format string
|
|
263
|
+
- float/int: treats as Unix timestamp, converts to ISO format
|
|
264
|
+
- str: returns as-is (already serialized)
|
|
265
|
+
"""
|
|
266
|
+
if dt is None:
|
|
267
|
+
return None
|
|
268
|
+
if isinstance(dt, str):
|
|
269
|
+
return dt # Already serialized
|
|
270
|
+
if isinstance(dt, (int, float)):
|
|
271
|
+
# Unix timestamp
|
|
272
|
+
return datetime.fromtimestamp(dt).isoformat()
|
|
273
|
+
if isinstance(dt, datetime):
|
|
274
|
+
return dt.isoformat()
|
|
275
|
+
# Try to convert to string as fallback
|
|
276
|
+
return str(dt)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def deserialize_datetime(s: Optional[str]) -> Optional[datetime]:
|
|
280
|
+
"""Deserialize ISO format string to datetime"""
|
|
281
|
+
if s is None:
|
|
282
|
+
return None
|
|
283
|
+
return datetime.fromisoformat(s)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def serialize_job(job) -> Dict:
|
|
287
|
+
"""Serialize a job (MockJob or Job) to a dictionary for JSON-RPC"""
|
|
288
|
+
from experimaestro.scheduler.interfaces import JobState
|
|
289
|
+
|
|
290
|
+
result = {
|
|
291
|
+
"identifier": job.identifier,
|
|
292
|
+
"task_id": job.task_id,
|
|
293
|
+
"locator": job.locator,
|
|
294
|
+
"path": str(job.path) if job.path else None,
|
|
295
|
+
"state": job.state.name if isinstance(job.state, JobState) else str(job.state),
|
|
296
|
+
"submittime": serialize_datetime(job.submittime),
|
|
297
|
+
"starttime": serialize_datetime(job.starttime),
|
|
298
|
+
"endtime": serialize_datetime(job.endtime),
|
|
299
|
+
"progress": job.progress,
|
|
300
|
+
"tags": job.tags,
|
|
301
|
+
"experiment_id": getattr(job, "experiment_id", None),
|
|
302
|
+
"run_id": getattr(job, "run_id", None),
|
|
303
|
+
}
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def deserialize_job(d: Dict) -> "MockJobData":
|
|
308
|
+
"""Deserialize a dictionary to MockJobData"""
|
|
309
|
+
from experimaestro.scheduler.interfaces import JobState, STATE_NAME_TO_JOBSTATE
|
|
310
|
+
from pathlib import Path
|
|
311
|
+
|
|
312
|
+
state = STATE_NAME_TO_JOBSTATE.get(d["state"], JobState.WAITING)
|
|
313
|
+
return MockJobData(
|
|
314
|
+
identifier=d["identifier"],
|
|
315
|
+
task_id=d["task_id"],
|
|
316
|
+
locator=d["locator"],
|
|
317
|
+
path=Path(d["path"]) if d["path"] else None,
|
|
318
|
+
state=state,
|
|
319
|
+
submittime=deserialize_datetime(d.get("submittime")),
|
|
320
|
+
starttime=deserialize_datetime(d.get("starttime")),
|
|
321
|
+
endtime=deserialize_datetime(d.get("endtime")),
|
|
322
|
+
progress=d.get("progress"),
|
|
323
|
+
tags=d.get("tags", {}),
|
|
324
|
+
experiment_id=d.get("experiment_id"),
|
|
325
|
+
run_id=d.get("run_id"),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def serialize_experiment(experiment) -> Dict:
|
|
330
|
+
"""Serialize a MockExperiment to a dictionary for JSON-RPC"""
|
|
331
|
+
result = {
|
|
332
|
+
"experiment_id": experiment.experiment_id,
|
|
333
|
+
"workdir": str(experiment.workdir) if experiment.workdir else None,
|
|
334
|
+
"current_run_id": experiment.current_run_id,
|
|
335
|
+
"total_jobs": experiment.total_jobs,
|
|
336
|
+
"finished_jobs": experiment.finished_jobs,
|
|
337
|
+
"failed_jobs": experiment.failed_jobs,
|
|
338
|
+
"updated_at": serialize_datetime(experiment.updated_at),
|
|
339
|
+
"started_at": serialize_datetime(experiment.started_at),
|
|
340
|
+
"ended_at": serialize_datetime(experiment.ended_at),
|
|
341
|
+
"hostname": experiment.hostname,
|
|
342
|
+
}
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def deserialize_experiment(d: Dict) -> "MockExperimentData":
|
|
347
|
+
"""Deserialize a dictionary to MockExperimentData"""
|
|
348
|
+
from pathlib import Path
|
|
349
|
+
|
|
350
|
+
return MockExperimentData(
|
|
351
|
+
experiment_id=d["experiment_id"],
|
|
352
|
+
workdir=Path(d["workdir"]) if d["workdir"] else None,
|
|
353
|
+
current_run_id=d.get("current_run_id"),
|
|
354
|
+
total_jobs=d.get("total_jobs", 0),
|
|
355
|
+
finished_jobs=d.get("finished_jobs", 0),
|
|
356
|
+
failed_jobs=d.get("failed_jobs", 0),
|
|
357
|
+
updated_at=deserialize_datetime(d.get("updated_at")),
|
|
358
|
+
started_at=deserialize_datetime(d.get("started_at")),
|
|
359
|
+
ended_at=deserialize_datetime(d.get("ended_at")),
|
|
360
|
+
hostname=d.get("hostname"),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def serialize_service(service) -> Dict:
|
|
365
|
+
"""Serialize a service to a dictionary for JSON-RPC"""
|
|
366
|
+
from experimaestro.scheduler.services import Service
|
|
367
|
+
|
|
368
|
+
# Service has id attribute, description() method, state property, state_dict() method
|
|
369
|
+
state = service.state
|
|
370
|
+
if hasattr(state, "name"):
|
|
371
|
+
state = state.name # Convert ServiceState enum to string
|
|
372
|
+
elif hasattr(state, "value"):
|
|
373
|
+
state = state.value
|
|
374
|
+
|
|
375
|
+
# Get URL if service has it (e.g., TensorboardService)
|
|
376
|
+
url = None
|
|
377
|
+
if hasattr(service, "url"):
|
|
378
|
+
url = service.url
|
|
379
|
+
elif hasattr(service, "get_url"):
|
|
380
|
+
try:
|
|
381
|
+
url = service.get_url()
|
|
382
|
+
except Exception:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
# Get state_dict with __class__ and serialize paths
|
|
386
|
+
if hasattr(service, "_full_state_dict"):
|
|
387
|
+
state_dict = Service.serialize_state_dict(service._full_state_dict())
|
|
388
|
+
elif callable(getattr(service, "state_dict", None)):
|
|
389
|
+
# Fallback: serialize paths in the raw state_dict
|
|
390
|
+
state_dict = Service.serialize_state_dict(service.state_dict())
|
|
391
|
+
else:
|
|
392
|
+
state_dict = getattr(service, "state_dict", {})
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
"service_id": getattr(service, "id", None),
|
|
396
|
+
"description": (
|
|
397
|
+
service.description()
|
|
398
|
+
if callable(service.description)
|
|
399
|
+
else service.description
|
|
400
|
+
),
|
|
401
|
+
"state": state,
|
|
402
|
+
"state_dict": state_dict,
|
|
403
|
+
"experiment_id": getattr(service, "experiment_id", None),
|
|
404
|
+
"run_id": getattr(service, "run_id", None),
|
|
405
|
+
"url": url,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def serialize_run(run) -> Dict:
|
|
410
|
+
"""Serialize an experiment run to a dictionary for JSON-RPC
|
|
411
|
+
|
|
412
|
+
Handles both dictionary and object inputs (get_experiment_runs returns dicts).
|
|
413
|
+
"""
|
|
414
|
+
if isinstance(run, dict):
|
|
415
|
+
# Already a dictionary - just ensure datetime serialization
|
|
416
|
+
return {
|
|
417
|
+
"run_id": run.get("run_id"),
|
|
418
|
+
"experiment_id": run.get("experiment_id"),
|
|
419
|
+
"hostname": run.get("hostname"),
|
|
420
|
+
"started_at": run.get("started_at"), # Already serialized
|
|
421
|
+
"ended_at": run.get("ended_at"), # Already serialized
|
|
422
|
+
"status": run.get("status"),
|
|
423
|
+
}
|
|
424
|
+
else:
|
|
425
|
+
# Object with attributes
|
|
426
|
+
return {
|
|
427
|
+
"run_id": run.run_id,
|
|
428
|
+
"experiment_id": run.experiment_id,
|
|
429
|
+
"hostname": getattr(run, "hostname", None),
|
|
430
|
+
"started_at": serialize_datetime(run.started_at),
|
|
431
|
+
"ended_at": serialize_datetime(run.ended_at),
|
|
432
|
+
"status": run.status,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@dataclass
|
|
437
|
+
class MockJobData:
|
|
438
|
+
"""Deserialized job data from remote"""
|
|
439
|
+
|
|
440
|
+
identifier: str
|
|
441
|
+
task_id: str
|
|
442
|
+
locator: str
|
|
443
|
+
path: Optional["Path"]
|
|
444
|
+
state: "JobState"
|
|
445
|
+
submittime: Optional[datetime]
|
|
446
|
+
starttime: Optional[datetime]
|
|
447
|
+
endtime: Optional[datetime]
|
|
448
|
+
progress: Optional[float]
|
|
449
|
+
tags: Dict
|
|
450
|
+
experiment_id: Optional[str]
|
|
451
|
+
run_id: Optional[str]
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@dataclass
|
|
455
|
+
class MockExperimentData:
|
|
456
|
+
"""Deserialized experiment data from remote"""
|
|
457
|
+
|
|
458
|
+
experiment_id: str
|
|
459
|
+
workdir: Optional["Path"]
|
|
460
|
+
current_run_id: Optional[str]
|
|
461
|
+
total_jobs: int
|
|
462
|
+
finished_jobs: int
|
|
463
|
+
failed_jobs: int
|
|
464
|
+
updated_at: Optional[datetime]
|
|
465
|
+
started_at: Optional[datetime]
|
|
466
|
+
ended_at: Optional[datetime]
|
|
467
|
+
hostname: Optional[str]
|