ml-dash 0.6.1__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.
- ml_dash/__init__.py +85 -0
- ml_dash/auth/__init__.py +51 -0
- ml_dash/auth/constants.py +10 -0
- ml_dash/auth/device_flow.py +237 -0
- ml_dash/auth/device_secret.py +49 -0
- ml_dash/auth/exceptions.py +31 -0
- ml_dash/auth/token_storage.py +262 -0
- ml_dash/auto_start.py +52 -0
- ml_dash/cli.py +79 -0
- ml_dash/cli_commands/__init__.py +1 -0
- ml_dash/cli_commands/download.py +769 -0
- ml_dash/cli_commands/list.py +319 -0
- ml_dash/cli_commands/login.py +225 -0
- ml_dash/cli_commands/logout.py +54 -0
- ml_dash/cli_commands/upload.py +1248 -0
- ml_dash/client.py +1003 -0
- ml_dash/config.py +133 -0
- ml_dash/experiment.py +1116 -0
- ml_dash/files.py +785 -0
- ml_dash/log.py +181 -0
- ml_dash/metric.py +481 -0
- ml_dash/params.py +277 -0
- ml_dash/py.typed +0 -0
- ml_dash/remote_auto_start.py +55 -0
- ml_dash/storage.py +1127 -0
- ml_dash-0.6.1.dist-info/METADATA +248 -0
- ml_dash-0.6.1.dist-info/RECORD +29 -0
- ml_dash-0.6.1.dist-info/WHEEL +4 -0
- ml_dash-0.6.1.dist-info/entry_points.txt +3 -0
ml_dash/experiment.py
ADDED
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Experiment class for ML-Dash SDK.
|
|
3
|
+
|
|
4
|
+
Supports three usage styles:
|
|
5
|
+
1. Decorator: @ml_dash_experiment(...)
|
|
6
|
+
2. Context manager: with Experiment(...) as exp:
|
|
7
|
+
3. Direct instantiation: exp = Experiment(...)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Optional, Dict, Any, List, Callable
|
|
11
|
+
from enum import Enum
|
|
12
|
+
import functools
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
from .client import RemoteClient
|
|
17
|
+
from .storage import LocalStorage
|
|
18
|
+
from .log import LogLevel, LogBuilder
|
|
19
|
+
from .params import ParametersBuilder
|
|
20
|
+
from .files import FileBuilder
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OperationMode(Enum):
|
|
24
|
+
"""Operation mode for the experiment."""
|
|
25
|
+
LOCAL = "local"
|
|
26
|
+
REMOTE = "remote"
|
|
27
|
+
HYBRID = "hybrid" # Future: sync local to remote
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RunManager:
|
|
31
|
+
"""
|
|
32
|
+
Lifecycle manager for experiments.
|
|
33
|
+
|
|
34
|
+
Supports three usage patterns:
|
|
35
|
+
1. Method calls: experiment.run.start(), experiment.run.complete()
|
|
36
|
+
2. Context manager: with Experiment(...).run as exp:
|
|
37
|
+
3. Decorator: @exp.run or @Experiment(...).run
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, experiment: "Experiment"):
|
|
41
|
+
"""
|
|
42
|
+
Initialize RunManager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
experiment: Parent Experiment instance
|
|
46
|
+
"""
|
|
47
|
+
self._experiment = experiment
|
|
48
|
+
|
|
49
|
+
def start(self) -> "Experiment":
|
|
50
|
+
"""
|
|
51
|
+
Start the experiment (sets status to RUNNING).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The experiment instance for chaining
|
|
55
|
+
"""
|
|
56
|
+
return self._experiment._open()
|
|
57
|
+
|
|
58
|
+
def complete(self) -> None:
|
|
59
|
+
"""Mark experiment as completed (status: COMPLETED)."""
|
|
60
|
+
self._experiment._close(status="COMPLETED")
|
|
61
|
+
|
|
62
|
+
def fail(self) -> None:
|
|
63
|
+
"""Mark experiment as failed (status: FAILED)."""
|
|
64
|
+
self._experiment._close(status="FAILED")
|
|
65
|
+
|
|
66
|
+
def cancel(self) -> None:
|
|
67
|
+
"""Mark experiment as cancelled (status: CANCELLED)."""
|
|
68
|
+
self._experiment._close(status="CANCELLED")
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def folder(self) -> Optional[str]:
|
|
72
|
+
"""
|
|
73
|
+
Get the current folder for this experiment.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Current folder path or None
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
current_folder = exp.run.folder
|
|
80
|
+
"""
|
|
81
|
+
return self._experiment.folder
|
|
82
|
+
|
|
83
|
+
@folder.setter
|
|
84
|
+
def folder(self, value: Optional[str]) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Set the folder for this experiment before initialization.
|
|
87
|
+
|
|
88
|
+
This can ONLY be set before the experiment is started (initialized).
|
|
89
|
+
Once the experiment is opened, the folder cannot be changed.
|
|
90
|
+
|
|
91
|
+
Supports template variables:
|
|
92
|
+
- {RUN.name} - Experiment name
|
|
93
|
+
- {RUN.project} - Project name
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
value: Folder path with optional template variables
|
|
97
|
+
(e.g., "experiments/{RUN.name}" or None)
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If experiment is already initialized/open
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
from ml_dash import dxp
|
|
104
|
+
|
|
105
|
+
# Static folder
|
|
106
|
+
dxp.run.folder = "experiments/vision/resnet"
|
|
107
|
+
|
|
108
|
+
# Template with experiment name
|
|
109
|
+
dxp.run.folder = "/iclr_2024/{RUN.name}"
|
|
110
|
+
|
|
111
|
+
# Template with multiple variables
|
|
112
|
+
dxp.run.folder = "{RUN.project}/experiments/{RUN.name}"
|
|
113
|
+
|
|
114
|
+
# Now start the experiment
|
|
115
|
+
with dxp.run:
|
|
116
|
+
dxp.params.set(lr=0.001)
|
|
117
|
+
"""
|
|
118
|
+
if self._experiment._is_open:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
"Cannot change folder after experiment is initialized. "
|
|
121
|
+
"Set folder before calling start() or entering 'with' block."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Process template variables if present
|
|
125
|
+
if value and '{RUN.' in value:
|
|
126
|
+
# Generate unique run ID (timestamp-based)
|
|
127
|
+
from datetime import datetime
|
|
128
|
+
run_timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
|
129
|
+
|
|
130
|
+
# Simple string replacement for template variables
|
|
131
|
+
# Supports: {RUN.name}, {RUN.project}, {RUN.id}, {RUN.timestamp}
|
|
132
|
+
replacements = {
|
|
133
|
+
'{RUN.name}': f"{self._experiment.name}_{run_timestamp}", # Unique name with timestamp
|
|
134
|
+
'{RUN.project}': self._experiment.project,
|
|
135
|
+
'{RUN.id}': run_timestamp, # Just the timestamp
|
|
136
|
+
'{RUN.timestamp}': run_timestamp, # Alias for id
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Replace all template variables
|
|
140
|
+
for template, replacement in replacements.items():
|
|
141
|
+
if template in value:
|
|
142
|
+
value = value.replace(template, replacement)
|
|
143
|
+
|
|
144
|
+
# Update the folder on the experiment
|
|
145
|
+
self._experiment.folder = value
|
|
146
|
+
|
|
147
|
+
def __enter__(self) -> "Experiment":
|
|
148
|
+
"""Context manager entry - starts the experiment."""
|
|
149
|
+
return self.start()
|
|
150
|
+
|
|
151
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
152
|
+
"""Context manager exit - completes or fails the experiment."""
|
|
153
|
+
if exc_type is not None:
|
|
154
|
+
self.fail()
|
|
155
|
+
else:
|
|
156
|
+
self.complete()
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def __call__(self, func: Callable) -> Callable:
|
|
160
|
+
"""
|
|
161
|
+
Decorator support for wrapping functions with experiment lifecycle.
|
|
162
|
+
|
|
163
|
+
Usage:
|
|
164
|
+
@exp.run
|
|
165
|
+
def train(exp):
|
|
166
|
+
exp.log("Training...")
|
|
167
|
+
"""
|
|
168
|
+
@functools.wraps(func)
|
|
169
|
+
def wrapper(*args, **kwargs):
|
|
170
|
+
with self as exp:
|
|
171
|
+
return func(exp, *args, **kwargs)
|
|
172
|
+
return wrapper
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Experiment:
|
|
176
|
+
"""
|
|
177
|
+
ML-Dash experiment for metricing experiments.
|
|
178
|
+
|
|
179
|
+
Usage examples:
|
|
180
|
+
|
|
181
|
+
# Remote mode
|
|
182
|
+
experiment = Experiment(
|
|
183
|
+
name="my-experiment",
|
|
184
|
+
project="my-project",
|
|
185
|
+
remote="https://api.dash.ml",
|
|
186
|
+
api_key="your-jwt-token"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Local mode
|
|
190
|
+
experiment = Experiment(
|
|
191
|
+
name="my-experiment",
|
|
192
|
+
project="my-project",
|
|
193
|
+
local_path=".ml-dash"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Context manager
|
|
197
|
+
with Experiment(...) as exp:
|
|
198
|
+
exp.log(...)
|
|
199
|
+
|
|
200
|
+
# Decorator
|
|
201
|
+
@ml_dash_experiment(name="exp", project="ws", remote="...")
|
|
202
|
+
def train():
|
|
203
|
+
...
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
name: str,
|
|
209
|
+
project: str,
|
|
210
|
+
*,
|
|
211
|
+
description: Optional[str] = None,
|
|
212
|
+
tags: Optional[List[str]] = None,
|
|
213
|
+
bindrs: Optional[List[str]] = None,
|
|
214
|
+
folder: Optional[str] = None,
|
|
215
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
216
|
+
# Mode configuration
|
|
217
|
+
remote: Optional[str] = None,
|
|
218
|
+
api_key: Optional[str] = None,
|
|
219
|
+
local_path: Optional[str] = None,
|
|
220
|
+
# Internal parameters
|
|
221
|
+
_write_protected: bool = False,
|
|
222
|
+
):
|
|
223
|
+
"""
|
|
224
|
+
Initialize an ML-Dash experiment.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
name: Experiment name (unique within project)
|
|
228
|
+
project: Project name
|
|
229
|
+
description: Optional experiment description
|
|
230
|
+
tags: Optional list of tags
|
|
231
|
+
bindrs: Optional list of bindrs
|
|
232
|
+
folder: Optional folder path (e.g., "/experiments/baseline")
|
|
233
|
+
metadata: Optional metadata dict
|
|
234
|
+
remote: Remote API URL (e.g., "https://api.dash.ml")
|
|
235
|
+
api_key: JWT token for authentication (auto-loaded from storage if not provided)
|
|
236
|
+
local_path: Local storage root path (for local mode)
|
|
237
|
+
_write_protected: Internal parameter - if True, experiment becomes immutable after creation
|
|
238
|
+
"""
|
|
239
|
+
self.name = name
|
|
240
|
+
self.project = project
|
|
241
|
+
self.description = description
|
|
242
|
+
self.tags = tags
|
|
243
|
+
self.bindrs = bindrs
|
|
244
|
+
self.folder = folder
|
|
245
|
+
self._write_protected = _write_protected
|
|
246
|
+
self.metadata = metadata
|
|
247
|
+
|
|
248
|
+
# Determine operation mode
|
|
249
|
+
if remote and local_path:
|
|
250
|
+
self.mode = OperationMode.HYBRID
|
|
251
|
+
elif remote:
|
|
252
|
+
self.mode = OperationMode.REMOTE
|
|
253
|
+
elif local_path:
|
|
254
|
+
self.mode = OperationMode.LOCAL
|
|
255
|
+
else:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"Must specify either 'remote' (with api_key) or 'local_path'"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Initialize backend
|
|
261
|
+
self._client: Optional[RemoteClient] = None
|
|
262
|
+
self._storage: Optional[LocalStorage] = None
|
|
263
|
+
self._experiment_id: Optional[str] = None
|
|
264
|
+
self._experiment_data: Optional[Dict[str, Any]] = None
|
|
265
|
+
self._is_open = False
|
|
266
|
+
self._metrics_manager: Optional['MetricsManager'] = None # Cached metrics manager
|
|
267
|
+
|
|
268
|
+
if self.mode in (OperationMode.REMOTE, OperationMode.HYBRID):
|
|
269
|
+
# api_key can be None - RemoteClient will auto-load from storage
|
|
270
|
+
self._client = RemoteClient(base_url=remote, api_key=api_key)
|
|
271
|
+
|
|
272
|
+
if self.mode in (OperationMode.LOCAL, OperationMode.HYBRID):
|
|
273
|
+
if not local_path:
|
|
274
|
+
raise ValueError("local_path is required for local mode")
|
|
275
|
+
self._storage = LocalStorage(root_path=Path(local_path))
|
|
276
|
+
|
|
277
|
+
def _open(self) -> "Experiment":
|
|
278
|
+
"""
|
|
279
|
+
Internal method to open the experiment (create or update on server/filesystem).
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
self for chaining
|
|
283
|
+
"""
|
|
284
|
+
if self._is_open:
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
if self._client:
|
|
288
|
+
# Remote mode: create/update experiment via API
|
|
289
|
+
response = self._client.create_or_update_experiment(
|
|
290
|
+
project=self.project,
|
|
291
|
+
name=self.name,
|
|
292
|
+
description=self.description,
|
|
293
|
+
tags=self.tags,
|
|
294
|
+
bindrs=self.bindrs,
|
|
295
|
+
folder=self.folder,
|
|
296
|
+
write_protected=self._write_protected,
|
|
297
|
+
metadata=self.metadata,
|
|
298
|
+
)
|
|
299
|
+
self._experiment_data = response
|
|
300
|
+
self._experiment_id = response["experiment"]["id"]
|
|
301
|
+
|
|
302
|
+
if self._storage:
|
|
303
|
+
# Local mode: create experiment directory structure
|
|
304
|
+
self._storage.create_experiment(
|
|
305
|
+
project=self.project,
|
|
306
|
+
name=self.name,
|
|
307
|
+
description=self.description,
|
|
308
|
+
tags=self.tags,
|
|
309
|
+
bindrs=self.bindrs,
|
|
310
|
+
folder=self.folder,
|
|
311
|
+
metadata=self.metadata,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
self._is_open = True
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
def _close(self, status: str = "COMPLETED"):
|
|
318
|
+
"""
|
|
319
|
+
Internal method to close the experiment and update status.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
status: Status to set - "COMPLETED" (default), "FAILED", or "CANCELLED"
|
|
323
|
+
"""
|
|
324
|
+
if not self._is_open:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Flush any pending writes
|
|
328
|
+
if self._storage:
|
|
329
|
+
self._storage.flush()
|
|
330
|
+
|
|
331
|
+
# Update experiment status in remote mode
|
|
332
|
+
if self._client and self._experiment_id:
|
|
333
|
+
try:
|
|
334
|
+
self._client.update_experiment_status(
|
|
335
|
+
experiment_id=self._experiment_id,
|
|
336
|
+
status=status
|
|
337
|
+
)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
# Log error but don't fail the close operation
|
|
340
|
+
print(f"Warning: Failed to update experiment status: {e}")
|
|
341
|
+
|
|
342
|
+
self._is_open = False
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def run(self) -> RunManager:
|
|
346
|
+
"""
|
|
347
|
+
Get the RunManager for lifecycle operations.
|
|
348
|
+
|
|
349
|
+
Usage:
|
|
350
|
+
# Method calls
|
|
351
|
+
experiment.run.start()
|
|
352
|
+
experiment.run.complete()
|
|
353
|
+
|
|
354
|
+
# Context manager
|
|
355
|
+
with Experiment(...).run as exp:
|
|
356
|
+
exp.log("Training...")
|
|
357
|
+
|
|
358
|
+
# Decorator
|
|
359
|
+
@experiment.run
|
|
360
|
+
def train(exp):
|
|
361
|
+
exp.log("Training...")
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
RunManager instance
|
|
365
|
+
"""
|
|
366
|
+
return RunManager(self)
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def params(self) -> ParametersBuilder:
|
|
370
|
+
"""
|
|
371
|
+
Get a ParametersBuilder for parameter operations.
|
|
372
|
+
|
|
373
|
+
Usage:
|
|
374
|
+
# Set parameters
|
|
375
|
+
experiment.params.set(lr=0.001, batch_size=32)
|
|
376
|
+
|
|
377
|
+
# Get parameters
|
|
378
|
+
params = experiment.params.get()
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
ParametersBuilder instance
|
|
382
|
+
|
|
383
|
+
Raises:
|
|
384
|
+
RuntimeError: If experiment is not open
|
|
385
|
+
"""
|
|
386
|
+
if not self._is_open:
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
"Experiment not started. Use 'with experiment.run:' or call experiment.run.start() first.\n"
|
|
389
|
+
"Example:\n"
|
|
390
|
+
" with dxp.run:\n"
|
|
391
|
+
" dxp.params.set(lr=0.001)"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
return ParametersBuilder(self)
|
|
395
|
+
|
|
396
|
+
def log(
|
|
397
|
+
self,
|
|
398
|
+
message: Optional[str] = None,
|
|
399
|
+
level: Optional[str] = None,
|
|
400
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
401
|
+
**extra_metadata
|
|
402
|
+
) -> Optional[LogBuilder]:
|
|
403
|
+
"""
|
|
404
|
+
Create a log entry or return a LogBuilder for fluent API.
|
|
405
|
+
|
|
406
|
+
This method supports two styles:
|
|
407
|
+
|
|
408
|
+
1. Fluent style (no message provided):
|
|
409
|
+
Returns a LogBuilder that allows chaining with level methods.
|
|
410
|
+
|
|
411
|
+
Examples:
|
|
412
|
+
experiment.log(metadata={"epoch": 1}).info("Training started")
|
|
413
|
+
experiment.log().error("Failed", error_code=500)
|
|
414
|
+
|
|
415
|
+
2. Traditional style (message provided):
|
|
416
|
+
Writes the log immediately and returns None.
|
|
417
|
+
|
|
418
|
+
Examples:
|
|
419
|
+
experiment.log("Training started", level="info", epoch=1)
|
|
420
|
+
experiment.log("Training started") # Defaults to "info"
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
message: Optional log message (for traditional style)
|
|
424
|
+
level: Optional log level (for traditional style, defaults to "info")
|
|
425
|
+
metadata: Optional metadata dict
|
|
426
|
+
**extra_metadata: Additional metadata as keyword arguments
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
LogBuilder if no message provided (fluent mode)
|
|
430
|
+
None if log was written directly (traditional mode)
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
RuntimeError: If experiment is not open
|
|
434
|
+
ValueError: If log level is invalid
|
|
435
|
+
"""
|
|
436
|
+
if not self._is_open:
|
|
437
|
+
raise RuntimeError(
|
|
438
|
+
"Experiment not started. Use 'with experiment.run:' or call experiment.run.start() first.\n"
|
|
439
|
+
"Example:\n"
|
|
440
|
+
" with dxp.run:\n"
|
|
441
|
+
" dxp.log().info('Training started')"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Fluent mode: return LogBuilder
|
|
445
|
+
if message is None:
|
|
446
|
+
combined_metadata = {**(metadata or {}), **extra_metadata}
|
|
447
|
+
return LogBuilder(self, combined_metadata if combined_metadata else None)
|
|
448
|
+
|
|
449
|
+
# Traditional mode: write immediately
|
|
450
|
+
level = level or LogLevel.INFO.value # Default to "info"
|
|
451
|
+
level = LogLevel.validate(level) # Validate level
|
|
452
|
+
|
|
453
|
+
combined_metadata = {**(metadata or {}), **extra_metadata}
|
|
454
|
+
self._write_log(
|
|
455
|
+
message=message,
|
|
456
|
+
level=level,
|
|
457
|
+
metadata=combined_metadata if combined_metadata else None,
|
|
458
|
+
timestamp=None
|
|
459
|
+
)
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
def _write_log(
|
|
463
|
+
self,
|
|
464
|
+
message: str,
|
|
465
|
+
level: str,
|
|
466
|
+
metadata: Optional[Dict[str, Any]],
|
|
467
|
+
timestamp: Optional[datetime]
|
|
468
|
+
) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Internal method to write a log entry immediately.
|
|
471
|
+
No buffering - writes directly to storage/remote AND stdout/stderr.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
message: Log message
|
|
475
|
+
level: Log level (already validated)
|
|
476
|
+
metadata: Optional metadata dict
|
|
477
|
+
timestamp: Optional custom timestamp (defaults to now)
|
|
478
|
+
"""
|
|
479
|
+
log_entry = {
|
|
480
|
+
"timestamp": (timestamp or datetime.utcnow()).isoformat() + "Z",
|
|
481
|
+
"level": level,
|
|
482
|
+
"message": message,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if metadata:
|
|
486
|
+
log_entry["metadata"] = metadata
|
|
487
|
+
|
|
488
|
+
# Mirror to stdout/stderr before writing to storage
|
|
489
|
+
self._print_log(message, level, metadata)
|
|
490
|
+
|
|
491
|
+
# Write immediately (no buffering)
|
|
492
|
+
if self._client:
|
|
493
|
+
# Remote mode: send to API (wrapped in array for batch API)
|
|
494
|
+
self._client.create_log_entries(
|
|
495
|
+
experiment_id=self._experiment_id,
|
|
496
|
+
logs=[log_entry] # Single log in array
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if self._storage:
|
|
500
|
+
# Local mode: write to file immediately
|
|
501
|
+
self._storage.write_log(
|
|
502
|
+
project=self.project,
|
|
503
|
+
experiment=self.name,
|
|
504
|
+
folder=self.folder,
|
|
505
|
+
message=log_entry["message"],
|
|
506
|
+
level=log_entry["level"],
|
|
507
|
+
metadata=log_entry.get("metadata"),
|
|
508
|
+
timestamp=log_entry["timestamp"]
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def _print_log(
|
|
512
|
+
self,
|
|
513
|
+
message: str,
|
|
514
|
+
level: str,
|
|
515
|
+
metadata: Optional[Dict[str, Any]]
|
|
516
|
+
) -> None:
|
|
517
|
+
"""
|
|
518
|
+
Print log to stdout or stderr based on level.
|
|
519
|
+
|
|
520
|
+
ERROR and FATAL go to stderr, all others go to stdout.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
message: Log message
|
|
524
|
+
level: Log level
|
|
525
|
+
metadata: Optional metadata dict
|
|
526
|
+
"""
|
|
527
|
+
import sys
|
|
528
|
+
|
|
529
|
+
# Format the log message
|
|
530
|
+
level_upper = level.upper()
|
|
531
|
+
|
|
532
|
+
# Build metadata string if present
|
|
533
|
+
metadata_str = ""
|
|
534
|
+
if metadata:
|
|
535
|
+
# Format metadata as key=value pairs
|
|
536
|
+
pairs = [f"{k}={v}" for k, v in metadata.items()]
|
|
537
|
+
metadata_str = f" [{', '.join(pairs)}]"
|
|
538
|
+
|
|
539
|
+
# Format: [LEVEL] message [key=value, ...]
|
|
540
|
+
formatted_message = f"[{level_upper}] {message}{metadata_str}"
|
|
541
|
+
|
|
542
|
+
# Route to stdout or stderr based on level
|
|
543
|
+
if level in ("error", "fatal"):
|
|
544
|
+
print(formatted_message, file=sys.stderr)
|
|
545
|
+
else:
|
|
546
|
+
print(formatted_message, file=sys.stdout)
|
|
547
|
+
|
|
548
|
+
def files(self, **kwargs) -> FileBuilder:
|
|
549
|
+
"""
|
|
550
|
+
Get a FileBuilder for fluent file operations.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
FileBuilder instance for chaining
|
|
554
|
+
|
|
555
|
+
Raises:
|
|
556
|
+
RuntimeError: If experiment is not open
|
|
557
|
+
|
|
558
|
+
Examples:
|
|
559
|
+
# Upload file
|
|
560
|
+
experiment.files(file_path="./model.pt", prefix="/models").save()
|
|
561
|
+
|
|
562
|
+
# List files
|
|
563
|
+
files = experiment.files().list()
|
|
564
|
+
files = experiment.files(prefix="/models").list()
|
|
565
|
+
|
|
566
|
+
# Download file
|
|
567
|
+
experiment.files(file_id="123").download()
|
|
568
|
+
|
|
569
|
+
# Delete file
|
|
570
|
+
experiment.files(file_id="123").delete()
|
|
571
|
+
"""
|
|
572
|
+
if not self._is_open:
|
|
573
|
+
raise RuntimeError(
|
|
574
|
+
"Experiment not started. Use 'with experiment.run:' or call experiment.run.start() first.\n"
|
|
575
|
+
"Example:\n"
|
|
576
|
+
" with dxp.run:\n"
|
|
577
|
+
" dxp.files().save()"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
return FileBuilder(self, **kwargs)
|
|
581
|
+
|
|
582
|
+
def _upload_file(
|
|
583
|
+
self,
|
|
584
|
+
file_path: str,
|
|
585
|
+
prefix: str,
|
|
586
|
+
filename: str,
|
|
587
|
+
description: Optional[str],
|
|
588
|
+
tags: Optional[List[str]],
|
|
589
|
+
metadata: Optional[Dict[str, Any]],
|
|
590
|
+
checksum: str,
|
|
591
|
+
content_type: str,
|
|
592
|
+
size_bytes: int
|
|
593
|
+
) -> Dict[str, Any]:
|
|
594
|
+
"""
|
|
595
|
+
Internal method to upload a file.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
file_path: Local file path
|
|
599
|
+
prefix: Logical path prefix
|
|
600
|
+
filename: Original filename
|
|
601
|
+
description: Optional description
|
|
602
|
+
tags: Optional tags
|
|
603
|
+
metadata: Optional metadata
|
|
604
|
+
checksum: SHA256 checksum
|
|
605
|
+
content_type: MIME type
|
|
606
|
+
size_bytes: File size in bytes
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
File metadata dict
|
|
610
|
+
"""
|
|
611
|
+
result = None
|
|
612
|
+
|
|
613
|
+
if self._client:
|
|
614
|
+
# Remote mode: upload to API
|
|
615
|
+
result = self._client.upload_file(
|
|
616
|
+
experiment_id=self._experiment_id,
|
|
617
|
+
file_path=file_path,
|
|
618
|
+
prefix=prefix,
|
|
619
|
+
filename=filename,
|
|
620
|
+
description=description,
|
|
621
|
+
tags=tags,
|
|
622
|
+
metadata=metadata,
|
|
623
|
+
checksum=checksum,
|
|
624
|
+
content_type=content_type,
|
|
625
|
+
size_bytes=size_bytes
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if self._storage:
|
|
629
|
+
# Local mode: copy to local storage
|
|
630
|
+
result = self._storage.write_file(
|
|
631
|
+
project=self.project,
|
|
632
|
+
experiment=self.name,
|
|
633
|
+
folder=self.folder,
|
|
634
|
+
file_path=file_path,
|
|
635
|
+
prefix=prefix,
|
|
636
|
+
filename=filename,
|
|
637
|
+
description=description,
|
|
638
|
+
tags=tags,
|
|
639
|
+
metadata=metadata,
|
|
640
|
+
checksum=checksum,
|
|
641
|
+
content_type=content_type,
|
|
642
|
+
size_bytes=size_bytes
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return result
|
|
646
|
+
|
|
647
|
+
def _list_files(
|
|
648
|
+
self,
|
|
649
|
+
prefix: Optional[str] = None,
|
|
650
|
+
tags: Optional[List[str]] = None
|
|
651
|
+
) -> List[Dict[str, Any]]:
|
|
652
|
+
"""
|
|
653
|
+
Internal method to list files.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
prefix: Optional prefix filter
|
|
657
|
+
tags: Optional tags filter
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
List of file metadata dicts
|
|
661
|
+
"""
|
|
662
|
+
files = []
|
|
663
|
+
|
|
664
|
+
if self._client:
|
|
665
|
+
# Remote mode: fetch from API
|
|
666
|
+
files = self._client.list_files(
|
|
667
|
+
experiment_id=self._experiment_id,
|
|
668
|
+
prefix=prefix,
|
|
669
|
+
tags=tags
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
if self._storage:
|
|
673
|
+
# Local mode: read from metadata file
|
|
674
|
+
files = self._storage.list_files(
|
|
675
|
+
project=self.project,
|
|
676
|
+
experiment=self.name,
|
|
677
|
+
prefix=prefix,
|
|
678
|
+
tags=tags
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
return files
|
|
682
|
+
|
|
683
|
+
def _download_file(
|
|
684
|
+
self,
|
|
685
|
+
file_id: str,
|
|
686
|
+
dest_path: Optional[str] = None
|
|
687
|
+
) -> str:
|
|
688
|
+
"""
|
|
689
|
+
Internal method to download a file.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
file_id: File ID
|
|
693
|
+
dest_path: Optional destination path (defaults to original filename)
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
Path to downloaded file
|
|
697
|
+
"""
|
|
698
|
+
if self._client:
|
|
699
|
+
# Remote mode: download from API
|
|
700
|
+
return self._client.download_file(
|
|
701
|
+
experiment_id=self._experiment_id,
|
|
702
|
+
file_id=file_id,
|
|
703
|
+
dest_path=dest_path
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
if self._storage:
|
|
707
|
+
# Local mode: copy from local storage
|
|
708
|
+
return self._storage.read_file(
|
|
709
|
+
project=self.project,
|
|
710
|
+
experiment=self.name,
|
|
711
|
+
file_id=file_id,
|
|
712
|
+
dest_path=dest_path
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
raise RuntimeError("No client or storage configured")
|
|
716
|
+
|
|
717
|
+
def _delete_file(self, file_id: str) -> Dict[str, Any]:
|
|
718
|
+
"""
|
|
719
|
+
Internal method to delete a file.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
file_id: File ID
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Dict with id and deletedAt
|
|
726
|
+
"""
|
|
727
|
+
result = None
|
|
728
|
+
|
|
729
|
+
if self._client:
|
|
730
|
+
# Remote mode: delete via API
|
|
731
|
+
result = self._client.delete_file(
|
|
732
|
+
experiment_id=self._experiment_id,
|
|
733
|
+
file_id=file_id
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
if self._storage:
|
|
737
|
+
# Local mode: soft delete in metadata
|
|
738
|
+
result = self._storage.delete_file(
|
|
739
|
+
project=self.project,
|
|
740
|
+
experiment=self.name,
|
|
741
|
+
file_id=file_id
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
return result
|
|
745
|
+
|
|
746
|
+
def _update_file(
|
|
747
|
+
self,
|
|
748
|
+
file_id: str,
|
|
749
|
+
description: Optional[str],
|
|
750
|
+
tags: Optional[List[str]],
|
|
751
|
+
metadata: Optional[Dict[str, Any]]
|
|
752
|
+
) -> Dict[str, Any]:
|
|
753
|
+
"""
|
|
754
|
+
Internal method to update file metadata.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
file_id: File ID
|
|
758
|
+
description: Optional description
|
|
759
|
+
tags: Optional tags
|
|
760
|
+
metadata: Optional metadata
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Updated file metadata dict
|
|
764
|
+
"""
|
|
765
|
+
result = None
|
|
766
|
+
|
|
767
|
+
if self._client:
|
|
768
|
+
# Remote mode: update via API
|
|
769
|
+
result = self._client.update_file(
|
|
770
|
+
experiment_id=self._experiment_id,
|
|
771
|
+
file_id=file_id,
|
|
772
|
+
description=description,
|
|
773
|
+
tags=tags,
|
|
774
|
+
metadata=metadata
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
if self._storage:
|
|
778
|
+
# Local mode: update in metadata file
|
|
779
|
+
result = self._storage.update_file_metadata(
|
|
780
|
+
project=self.project,
|
|
781
|
+
experiment=self.name,
|
|
782
|
+
file_id=file_id,
|
|
783
|
+
description=description,
|
|
784
|
+
tags=tags,
|
|
785
|
+
metadata=metadata
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
return result
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _write_params(self, flattened_params: Dict[str, Any]) -> None:
|
|
792
|
+
"""
|
|
793
|
+
Internal method to write/merge parameters.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
flattened_params: Already-flattened parameter dict with dot notation
|
|
797
|
+
"""
|
|
798
|
+
if self._client:
|
|
799
|
+
# Remote mode: send to API
|
|
800
|
+
self._client.set_parameters(
|
|
801
|
+
experiment_id=self._experiment_id,
|
|
802
|
+
data=flattened_params
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
if self._storage:
|
|
806
|
+
# Local mode: write to file
|
|
807
|
+
self._storage.write_parameters(
|
|
808
|
+
project=self.project,
|
|
809
|
+
experiment=self.name,
|
|
810
|
+
folder=self.folder,
|
|
811
|
+
data=flattened_params
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
def _read_params(self) -> Optional[Dict[str, Any]]:
|
|
815
|
+
"""
|
|
816
|
+
Internal method to read parameters.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
Flattened parameters dict, or None if no parameters exist
|
|
820
|
+
"""
|
|
821
|
+
params = None
|
|
822
|
+
|
|
823
|
+
if self._client:
|
|
824
|
+
# Remote mode: fetch from API
|
|
825
|
+
try:
|
|
826
|
+
params = self._client.get_parameters(experiment_id=self._experiment_id)
|
|
827
|
+
except Exception:
|
|
828
|
+
# Parameters don't exist yet
|
|
829
|
+
params = None
|
|
830
|
+
|
|
831
|
+
if self._storage:
|
|
832
|
+
# Local mode: read from file
|
|
833
|
+
params = self._storage.read_parameters(
|
|
834
|
+
project=self.project,
|
|
835
|
+
experiment=self.name
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
return params
|
|
839
|
+
|
|
840
|
+
@property
|
|
841
|
+
def metrics(self) -> 'MetricsManager':
|
|
842
|
+
"""
|
|
843
|
+
Get a MetricsManager for metric operations.
|
|
844
|
+
|
|
845
|
+
Supports two usage patterns:
|
|
846
|
+
1. Named: experiment.metrics("loss").append(value=0.5, step=1)
|
|
847
|
+
2. Unnamed: experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
MetricsManager instance
|
|
851
|
+
|
|
852
|
+
Raises:
|
|
853
|
+
RuntimeError: If experiment is not open
|
|
854
|
+
|
|
855
|
+
Examples:
|
|
856
|
+
# Named metric
|
|
857
|
+
experiment.metrics("train_loss").append(value=0.5, step=100)
|
|
858
|
+
|
|
859
|
+
# Unnamed (name in append call)
|
|
860
|
+
experiment.metrics.append(name="train_loss", value=0.5, step=100)
|
|
861
|
+
|
|
862
|
+
# Append batch
|
|
863
|
+
experiment.metrics("metrics").append_batch([
|
|
864
|
+
{"loss": 0.5, "acc": 0.8, "step": 1},
|
|
865
|
+
{"loss": 0.4, "acc": 0.85, "step": 2}
|
|
866
|
+
])
|
|
867
|
+
|
|
868
|
+
# Read data
|
|
869
|
+
data = experiment.metrics("train_loss").read(start_index=0, limit=100)
|
|
870
|
+
|
|
871
|
+
# Get statistics
|
|
872
|
+
stats = experiment.metrics("train_loss").stats()
|
|
873
|
+
"""
|
|
874
|
+
from .metric import MetricsManager
|
|
875
|
+
|
|
876
|
+
if not self._is_open:
|
|
877
|
+
raise RuntimeError(
|
|
878
|
+
"Cannot use metrics on closed experiment. "
|
|
879
|
+
"Use 'with Experiment(...).run as experiment:' or call experiment.run.start() first."
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# Cache the MetricsManager instance to preserve MetricBuilder cache across calls
|
|
883
|
+
if self._metrics_manager is None:
|
|
884
|
+
self._metrics_manager = MetricsManager(self)
|
|
885
|
+
return self._metrics_manager
|
|
886
|
+
|
|
887
|
+
def _append_to_metric(
|
|
888
|
+
self,
|
|
889
|
+
name: Optional[str],
|
|
890
|
+
data: Dict[str, Any],
|
|
891
|
+
description: Optional[str],
|
|
892
|
+
tags: Optional[List[str]],
|
|
893
|
+
metadata: Optional[Dict[str, Any]]
|
|
894
|
+
) -> Dict[str, Any]:
|
|
895
|
+
"""
|
|
896
|
+
Internal method to append a single data point to a metric.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
name: Metric name (can be None for unnamed metrics)
|
|
900
|
+
data: Data point (flexible schema)
|
|
901
|
+
description: Optional metric description
|
|
902
|
+
tags: Optional tags
|
|
903
|
+
metadata: Optional metadata
|
|
904
|
+
|
|
905
|
+
Returns:
|
|
906
|
+
Dict with metricId, index, bufferedDataPoints, chunkSize
|
|
907
|
+
"""
|
|
908
|
+
result = None
|
|
909
|
+
|
|
910
|
+
if self._client:
|
|
911
|
+
# Remote mode: append via API
|
|
912
|
+
result = self._client.append_to_metric(
|
|
913
|
+
experiment_id=self._experiment_id,
|
|
914
|
+
metric_name=name,
|
|
915
|
+
data=data,
|
|
916
|
+
description=description,
|
|
917
|
+
tags=tags,
|
|
918
|
+
metadata=metadata
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
if self._storage:
|
|
922
|
+
# Local mode: append to local storage
|
|
923
|
+
result = self._storage.append_to_metric(
|
|
924
|
+
project=self.project,
|
|
925
|
+
experiment=self.name,
|
|
926
|
+
folder=self.folder,
|
|
927
|
+
metric_name=name,
|
|
928
|
+
data=data,
|
|
929
|
+
description=description,
|
|
930
|
+
tags=tags,
|
|
931
|
+
metadata=metadata
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
return result
|
|
935
|
+
|
|
936
|
+
def _append_batch_to_metric(
|
|
937
|
+
self,
|
|
938
|
+
name: Optional[str],
|
|
939
|
+
data_points: List[Dict[str, Any]],
|
|
940
|
+
description: Optional[str],
|
|
941
|
+
tags: Optional[List[str]],
|
|
942
|
+
metadata: Optional[Dict[str, Any]]
|
|
943
|
+
) -> Dict[str, Any]:
|
|
944
|
+
"""
|
|
945
|
+
Internal method to append multiple data points to a metric.
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
name: Metric name (can be None for unnamed metrics)
|
|
949
|
+
data_points: List of data points
|
|
950
|
+
description: Optional metric description
|
|
951
|
+
tags: Optional tags
|
|
952
|
+
metadata: Optional metadata
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
Dict with metricId, startIndex, endIndex, count
|
|
956
|
+
"""
|
|
957
|
+
result = None
|
|
958
|
+
|
|
959
|
+
if self._client:
|
|
960
|
+
# Remote mode: append batch via API
|
|
961
|
+
result = self._client.append_batch_to_metric(
|
|
962
|
+
experiment_id=self._experiment_id,
|
|
963
|
+
metric_name=name,
|
|
964
|
+
data_points=data_points,
|
|
965
|
+
description=description,
|
|
966
|
+
tags=tags,
|
|
967
|
+
metadata=metadata
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
if self._storage:
|
|
971
|
+
# Local mode: append batch to local storage
|
|
972
|
+
result = self._storage.append_batch_to_metric(
|
|
973
|
+
project=self.project,
|
|
974
|
+
experiment=self.name,
|
|
975
|
+
metric_name=name,
|
|
976
|
+
data_points=data_points,
|
|
977
|
+
description=description,
|
|
978
|
+
tags=tags,
|
|
979
|
+
metadata=metadata
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
return result
|
|
983
|
+
|
|
984
|
+
def _read_metric_data(
|
|
985
|
+
self,
|
|
986
|
+
name: str,
|
|
987
|
+
start_index: int,
|
|
988
|
+
limit: int
|
|
989
|
+
) -> Dict[str, Any]:
|
|
990
|
+
"""
|
|
991
|
+
Internal method to read data points from a metric.
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
name: Metric name
|
|
995
|
+
start_index: Starting index
|
|
996
|
+
limit: Max points to read
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
Dict with data, startIndex, endIndex, total, hasMore
|
|
1000
|
+
"""
|
|
1001
|
+
result = None
|
|
1002
|
+
|
|
1003
|
+
if self._client:
|
|
1004
|
+
# Remote mode: read via API
|
|
1005
|
+
result = self._client.read_metric_data(
|
|
1006
|
+
experiment_id=self._experiment_id,
|
|
1007
|
+
metric_name=name,
|
|
1008
|
+
start_index=start_index,
|
|
1009
|
+
limit=limit
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
if self._storage:
|
|
1013
|
+
# Local mode: read from local storage
|
|
1014
|
+
result = self._storage.read_metric_data(
|
|
1015
|
+
project=self.project,
|
|
1016
|
+
experiment=self.name,
|
|
1017
|
+
metric_name=name,
|
|
1018
|
+
start_index=start_index,
|
|
1019
|
+
limit=limit
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
return result
|
|
1023
|
+
|
|
1024
|
+
def _get_metric_stats(self, name: str) -> Dict[str, Any]:
|
|
1025
|
+
"""
|
|
1026
|
+
Internal method to get metric statistics.
|
|
1027
|
+
|
|
1028
|
+
Args:
|
|
1029
|
+
name: Metric name
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
Dict with metric stats
|
|
1033
|
+
"""
|
|
1034
|
+
result = None
|
|
1035
|
+
|
|
1036
|
+
if self._client:
|
|
1037
|
+
# Remote mode: get stats via API
|
|
1038
|
+
result = self._client.get_metric_stats(
|
|
1039
|
+
experiment_id=self._experiment_id,
|
|
1040
|
+
metric_name=name
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
if self._storage:
|
|
1044
|
+
# Local mode: get stats from local storage
|
|
1045
|
+
result = self._storage.get_metric_stats(
|
|
1046
|
+
project=self.project,
|
|
1047
|
+
experiment=self.name,
|
|
1048
|
+
metric_name=name
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
return result
|
|
1052
|
+
|
|
1053
|
+
def _list_metrics(self) -> List[Dict[str, Any]]:
|
|
1054
|
+
"""
|
|
1055
|
+
Internal method to list all metrics in experiment.
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
List of metric summaries
|
|
1059
|
+
"""
|
|
1060
|
+
result = None
|
|
1061
|
+
|
|
1062
|
+
if self._client:
|
|
1063
|
+
# Remote mode: list via API
|
|
1064
|
+
result = self._client.list_metrics(experiment_id=self._experiment_id)
|
|
1065
|
+
|
|
1066
|
+
if self._storage:
|
|
1067
|
+
# Local mode: list from local storage
|
|
1068
|
+
result = self._storage.list_metrics(
|
|
1069
|
+
project=self.project,
|
|
1070
|
+
experiment=self.name
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
return result or []
|
|
1074
|
+
|
|
1075
|
+
@property
|
|
1076
|
+
def id(self) -> Optional[str]:
|
|
1077
|
+
"""Get the experiment ID (only available after open in remote mode)."""
|
|
1078
|
+
return self._experiment_id
|
|
1079
|
+
|
|
1080
|
+
@property
|
|
1081
|
+
def data(self) -> Optional[Dict[str, Any]]:
|
|
1082
|
+
"""Get the full experiment data (only available after open in remote mode)."""
|
|
1083
|
+
return self._experiment_data
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def ml_dash_experiment(
|
|
1087
|
+
name: str,
|
|
1088
|
+
project: str,
|
|
1089
|
+
**kwargs
|
|
1090
|
+
) -> Callable:
|
|
1091
|
+
"""
|
|
1092
|
+
Decorator for wrapping functions with an ML-Dash experiment.
|
|
1093
|
+
|
|
1094
|
+
Usage:
|
|
1095
|
+
@ml_dash_experiment(
|
|
1096
|
+
name="my-experiment",
|
|
1097
|
+
project="my-project",
|
|
1098
|
+
remote="https://api.dash.ml",
|
|
1099
|
+
api_key="your-token"
|
|
1100
|
+
)
|
|
1101
|
+
def train_model():
|
|
1102
|
+
# Function code here
|
|
1103
|
+
pass
|
|
1104
|
+
|
|
1105
|
+
The decorated function will receive an 'experiment' keyword argument
|
|
1106
|
+
with the active Experiment instance.
|
|
1107
|
+
"""
|
|
1108
|
+
def decorator(func: Callable) -> Callable:
|
|
1109
|
+
@functools.wraps(func)
|
|
1110
|
+
def wrapper(*args, **func_kwargs):
|
|
1111
|
+
with Experiment(name=name, project=project, **kwargs).run as experiment:
|
|
1112
|
+
# Inject experiment into function kwargs
|
|
1113
|
+
func_kwargs['experiment'] = experiment
|
|
1114
|
+
return func(*args, **func_kwargs)
|
|
1115
|
+
return wrapper
|
|
1116
|
+
return decorator
|