ml-dash 0.0.17__py3-none-any.whl → 0.2.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 +58 -1
- ml_dash/client.py +562 -0
- ml_dash/experiment.py +916 -0
- ml_dash/files.py +313 -0
- ml_dash/log.py +181 -0
- ml_dash/metric.py +186 -0
- ml_dash/params.py +188 -0
- ml_dash/storage.py +922 -0
- ml_dash-0.2.1.dist-info/METADATA +237 -0
- ml_dash-0.2.1.dist-info/RECORD +12 -0
- ml_dash-0.2.1.dist-info/WHEEL +4 -0
- app-build/asset-manifest.json +0 -15
- app-build/favicon.ico +0 -0
- app-build/github-markdown.css +0 -957
- app-build/index.html +0 -1
- app-build/manifest.json +0 -15
- app-build/monaco-editor-worker-loader-proxy.js +0 -6
- app-build/precache-manifest.ffc09f8a591c529a1bd5c6f21f49815f.js +0 -26
- app-build/service-worker.js +0 -34
- ml_dash/app.py +0 -60
- ml_dash/config.py +0 -16
- ml_dash/file_events.py +0 -71
- ml_dash/file_handlers.py +0 -141
- ml_dash/file_utils.py +0 -5
- ml_dash/file_watcher.py +0 -30
- ml_dash/main.py +0 -60
- ml_dash/mime_types.py +0 -20
- ml_dash/schema/__init__.py +0 -110
- ml_dash/schema/archive.py +0 -165
- ml_dash/schema/directories.py +0 -59
- ml_dash/schema/experiments.py +0 -65
- ml_dash/schema/files/__init__.py +0 -204
- ml_dash/schema/files/file_helpers.py +0 -79
- ml_dash/schema/files/images.py +0 -27
- ml_dash/schema/files/metrics.py +0 -64
- ml_dash/schema/files/parameters.py +0 -50
- ml_dash/schema/files/series.py +0 -235
- ml_dash/schema/files/videos.py +0 -27
- ml_dash/schema/helpers.py +0 -66
- ml_dash/schema/projects.py +0 -65
- ml_dash/schema/schema_helpers.py +0 -19
- ml_dash/schema/users.py +0 -33
- ml_dash/sse.py +0 -18
- ml_dash-0.0.17.dist-info/METADATA +0 -67
- ml_dash-0.0.17.dist-info/RECORD +0 -38
- ml_dash-0.0.17.dist-info/WHEEL +0 -5
- ml_dash-0.0.17.dist-info/top_level.txt +0 -2
- /ml_dash/{example.py → py.typed} +0 -0
ml_dash/files.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Files module for ML-Dash SDK.
|
|
3
|
+
|
|
4
|
+
Provides fluent API for file upload, download, list, and delete operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import mimetypes
|
|
9
|
+
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .experiment import Experiment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileBuilder:
|
|
17
|
+
"""
|
|
18
|
+
Fluent interface for file operations.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
# Upload file
|
|
22
|
+
experiment.file(file_path="./model.pt", prefix="/models").save()
|
|
23
|
+
|
|
24
|
+
# List files
|
|
25
|
+
files = experiment.file().list()
|
|
26
|
+
files = experiment.file(prefix="/models").list()
|
|
27
|
+
|
|
28
|
+
# Download file
|
|
29
|
+
experiment.file(file_id="123").download()
|
|
30
|
+
experiment.file(file_id="123", dest_path="./model.pt").download()
|
|
31
|
+
|
|
32
|
+
# Delete file
|
|
33
|
+
experiment.file(file_id="123").delete()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, experiment: 'Experiment', **kwargs):
|
|
37
|
+
"""
|
|
38
|
+
Initialize file builder.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
experiment: Parent experiment instance
|
|
42
|
+
**kwargs: File operation parameters
|
|
43
|
+
- file_path: Path to file to upload
|
|
44
|
+
- prefix: Logical path prefix (default: "/")
|
|
45
|
+
- description: Optional description
|
|
46
|
+
- tags: Optional list of tags
|
|
47
|
+
- metadata: Optional metadata dict
|
|
48
|
+
- file_id: File ID for download/delete/update operations
|
|
49
|
+
- dest_path: Destination path for download
|
|
50
|
+
"""
|
|
51
|
+
self._experiment = experiment
|
|
52
|
+
self._file_path = kwargs.get('file_path')
|
|
53
|
+
self._prefix = kwargs.get('prefix', '/')
|
|
54
|
+
self._description = kwargs.get('description')
|
|
55
|
+
self._tags = kwargs.get('tags', [])
|
|
56
|
+
self._metadata = kwargs.get('metadata')
|
|
57
|
+
self._file_id = kwargs.get('file_id')
|
|
58
|
+
self._dest_path = kwargs.get('dest_path')
|
|
59
|
+
|
|
60
|
+
def save(self) -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Upload and save the file.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RuntimeError: If experiment is not open or write-protected
|
|
69
|
+
ValueError: If file_path not provided or file doesn't exist
|
|
70
|
+
ValueError: If file size exceeds 5GB limit
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
result = experiment.file(file_path="./model.pt", prefix="/models").save()
|
|
74
|
+
# Returns: {"id": "123", "path": "/models", "filename": "model.pt", ...}
|
|
75
|
+
"""
|
|
76
|
+
if not self._experiment._is_open:
|
|
77
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
78
|
+
|
|
79
|
+
if self._experiment.write_protected:
|
|
80
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
81
|
+
|
|
82
|
+
if not self._file_path:
|
|
83
|
+
raise ValueError("file_path is required for save() operation")
|
|
84
|
+
|
|
85
|
+
file_path = Path(self._file_path)
|
|
86
|
+
if not file_path.exists():
|
|
87
|
+
raise ValueError(f"File not found: {self._file_path}")
|
|
88
|
+
|
|
89
|
+
if not file_path.is_file():
|
|
90
|
+
raise ValueError(f"Path is not a file: {self._file_path}")
|
|
91
|
+
|
|
92
|
+
# Check file size (max 5GB)
|
|
93
|
+
file_size = file_path.stat().st_size
|
|
94
|
+
MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024 # 5GB in bytes
|
|
95
|
+
if file_size > MAX_FILE_SIZE:
|
|
96
|
+
raise ValueError(f"File size ({file_size} bytes) exceeds 5GB limit")
|
|
97
|
+
|
|
98
|
+
# Compute checksum
|
|
99
|
+
checksum = compute_sha256(str(file_path))
|
|
100
|
+
|
|
101
|
+
# Detect MIME type
|
|
102
|
+
content_type = get_mime_type(str(file_path))
|
|
103
|
+
|
|
104
|
+
# Get filename
|
|
105
|
+
filename = file_path.name
|
|
106
|
+
|
|
107
|
+
# Upload through experiment
|
|
108
|
+
return self._experiment._upload_file(
|
|
109
|
+
file_path=str(file_path),
|
|
110
|
+
prefix=self._prefix,
|
|
111
|
+
filename=filename,
|
|
112
|
+
description=self._description,
|
|
113
|
+
tags=self._tags,
|
|
114
|
+
metadata=self._metadata,
|
|
115
|
+
checksum=checksum,
|
|
116
|
+
content_type=content_type,
|
|
117
|
+
size_bytes=file_size
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def list(self) -> List[Dict[str, Any]]:
|
|
121
|
+
"""
|
|
122
|
+
List files with optional filters.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of file metadata dicts
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
RuntimeError: If experiment is not open
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
files = experiment.file().list() # All files
|
|
132
|
+
files = experiment.file(prefix="/models").list() # Filter by prefix
|
|
133
|
+
files = experiment.file(tags=["checkpoint"]).list() # Filter by tags
|
|
134
|
+
"""
|
|
135
|
+
if not self._experiment._is_open:
|
|
136
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
137
|
+
|
|
138
|
+
return self._experiment._list_files(
|
|
139
|
+
prefix=self._prefix if self._prefix != '/' else None,
|
|
140
|
+
tags=self._tags if self._tags else None
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def download(self) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Download file with automatic checksum verification.
|
|
146
|
+
|
|
147
|
+
If dest_path not provided, downloads to current directory with original filename.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Path to downloaded file
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
RuntimeError: If experiment is not open
|
|
154
|
+
ValueError: If file_id not provided
|
|
155
|
+
ValueError: If checksum verification fails
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
# Download to current directory with original filename
|
|
159
|
+
path = experiment.file(file_id="123").download()
|
|
160
|
+
|
|
161
|
+
# Download to custom path
|
|
162
|
+
path = experiment.file(file_id="123", dest_path="./model.pt").download()
|
|
163
|
+
"""
|
|
164
|
+
if not self._experiment._is_open:
|
|
165
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
166
|
+
|
|
167
|
+
if not self._file_id:
|
|
168
|
+
raise ValueError("file_id is required for download() operation")
|
|
169
|
+
|
|
170
|
+
return self._experiment._download_file(
|
|
171
|
+
file_id=self._file_id,
|
|
172
|
+
dest_path=self._dest_path
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def delete(self) -> Dict[str, Any]:
|
|
176
|
+
"""
|
|
177
|
+
Delete file (soft delete).
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dict with id and deletedAt timestamp
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
RuntimeError: If experiment is not open or write-protected
|
|
184
|
+
ValueError: If file_id not provided
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
result = experiment.file(file_id="123").delete()
|
|
188
|
+
"""
|
|
189
|
+
if not self._experiment._is_open:
|
|
190
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
191
|
+
|
|
192
|
+
if self._experiment.write_protected:
|
|
193
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
194
|
+
|
|
195
|
+
if not self._file_id:
|
|
196
|
+
raise ValueError("file_id is required for delete() operation")
|
|
197
|
+
|
|
198
|
+
return self._experiment._delete_file(file_id=self._file_id)
|
|
199
|
+
|
|
200
|
+
def update(self) -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Update file metadata (description, tags, metadata).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Updated file metadata dict
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
RuntimeError: If experiment is not open or write-protected
|
|
209
|
+
ValueError: If file_id not provided
|
|
210
|
+
|
|
211
|
+
Examples:
|
|
212
|
+
result = experiment.file(
|
|
213
|
+
file_id="123",
|
|
214
|
+
description="Updated description",
|
|
215
|
+
tags=["new", "tags"],
|
|
216
|
+
metadata={"updated": True}
|
|
217
|
+
).update()
|
|
218
|
+
"""
|
|
219
|
+
if not self._experiment._is_open:
|
|
220
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
221
|
+
|
|
222
|
+
if self._experiment.write_protected:
|
|
223
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
224
|
+
|
|
225
|
+
if not self._file_id:
|
|
226
|
+
raise ValueError("file_id is required for update() operation")
|
|
227
|
+
|
|
228
|
+
return self._experiment._update_file(
|
|
229
|
+
file_id=self._file_id,
|
|
230
|
+
description=self._description,
|
|
231
|
+
tags=self._tags,
|
|
232
|
+
metadata=self._metadata
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def compute_sha256(file_path: str) -> str:
|
|
237
|
+
"""
|
|
238
|
+
Compute SHA256 checksum of a file.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
file_path: Path to file
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Hex-encoded SHA256 checksum
|
|
245
|
+
|
|
246
|
+
Examples:
|
|
247
|
+
checksum = compute_sha256("./model.pt")
|
|
248
|
+
# Returns: "abc123def456..."
|
|
249
|
+
"""
|
|
250
|
+
sha256_hash = hashlib.sha256()
|
|
251
|
+
|
|
252
|
+
with open(file_path, "rb") as f:
|
|
253
|
+
# Read file in chunks to handle large files
|
|
254
|
+
for byte_block in iter(lambda: f.read(8192), b""):
|
|
255
|
+
sha256_hash.update(byte_block)
|
|
256
|
+
|
|
257
|
+
return sha256_hash.hexdigest()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_mime_type(file_path: str) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Detect MIME type of a file.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
file_path: Path to file
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
MIME type string (default: "application/octet-stream")
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
mime_type = get_mime_type("./model.pt")
|
|
272
|
+
# Returns: "application/octet-stream"
|
|
273
|
+
|
|
274
|
+
mime_type = get_mime_type("./image.png")
|
|
275
|
+
# Returns: "image/png"
|
|
276
|
+
"""
|
|
277
|
+
mime_type, _ = mimetypes.guess_type(file_path)
|
|
278
|
+
return mime_type or "application/octet-stream"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def verify_checksum(file_path: str, expected_checksum: str) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
Verify SHA256 checksum of a file.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
file_path: Path to file
|
|
287
|
+
expected_checksum: Expected SHA256 checksum (hex-encoded)
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
True if checksum matches, False otherwise
|
|
291
|
+
|
|
292
|
+
Examples:
|
|
293
|
+
is_valid = verify_checksum("./model.pt", "abc123...")
|
|
294
|
+
"""
|
|
295
|
+
actual_checksum = compute_sha256(file_path)
|
|
296
|
+
return actual_checksum == expected_checksum
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def generate_snowflake_id() -> str:
|
|
300
|
+
"""
|
|
301
|
+
Generate a simple Snowflake-like ID for local mode.
|
|
302
|
+
|
|
303
|
+
Not a true Snowflake ID, but provides unique IDs for local storage.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
String representation of generated ID
|
|
307
|
+
"""
|
|
308
|
+
import time
|
|
309
|
+
import random
|
|
310
|
+
|
|
311
|
+
timestamp = int(time.time() * 1000)
|
|
312
|
+
random_bits = random.randint(0, 4095)
|
|
313
|
+
return str((timestamp << 12) | random_bits)
|
ml_dash/log.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log API for ML-Dash SDK.
|
|
3
|
+
|
|
4
|
+
Provides fluent interface for structured logging with validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .experiment import Experiment
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LogLevel(Enum):
|
|
16
|
+
"""
|
|
17
|
+
Standard log levels for ML-Dash.
|
|
18
|
+
|
|
19
|
+
Supported levels:
|
|
20
|
+
- INFO: Informational messages
|
|
21
|
+
- WARN: Warning messages
|
|
22
|
+
- ERROR: Error messages
|
|
23
|
+
- DEBUG: Debug messages
|
|
24
|
+
- FATAL: Fatal error messages
|
|
25
|
+
"""
|
|
26
|
+
INFO = "info"
|
|
27
|
+
WARN = "warn"
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
DEBUG = "debug"
|
|
30
|
+
FATAL = "fatal"
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def validate(cls, level: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Validate and normalize log level.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
level: Log level string (case-insensitive)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Normalized log level string (lowercase)
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If level is not one of the 5 standard levels
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> LogLevel.validate("INFO")
|
|
48
|
+
"info"
|
|
49
|
+
>>> LogLevel.validate("invalid")
|
|
50
|
+
ValueError: Invalid log level: 'invalid'. Must be one of: info, warn, error, debug, fatal
|
|
51
|
+
"""
|
|
52
|
+
level_lower = level.lower()
|
|
53
|
+
try:
|
|
54
|
+
return cls[level_lower.upper()].value
|
|
55
|
+
except KeyError:
|
|
56
|
+
valid_levels = ", ".join([l.value for l in cls])
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Invalid log level: '{level}'. "
|
|
59
|
+
f"Must be one of: {valid_levels}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class LogBuilder:
|
|
64
|
+
"""
|
|
65
|
+
Fluent builder for creating log entries.
|
|
66
|
+
|
|
67
|
+
This class is returned by Experiment.log() when no message is provided.
|
|
68
|
+
It allows for a fluent API style where metadata is set first, then
|
|
69
|
+
the log level method is called to write the log.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
experiment.log(metadata={"epoch": 1}).info("Training started")
|
|
73
|
+
experiment.log().error("Failed", error_code=500)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, experiment: 'Experiment', metadata: Optional[Dict[str, Any]] = None):
|
|
77
|
+
"""
|
|
78
|
+
Initialize LogBuilder.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
experiment: Parent Experiment instance
|
|
82
|
+
metadata: Optional metadata dict from log() call
|
|
83
|
+
"""
|
|
84
|
+
self._experiment = experiment
|
|
85
|
+
self._metadata = metadata
|
|
86
|
+
|
|
87
|
+
def info(self, message: str, **extra_metadata) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Write info level log.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
message: Log message
|
|
93
|
+
**extra_metadata: Additional metadata as keyword arguments
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
experiment.log().info("Training started")
|
|
97
|
+
experiment.log().info("Epoch complete", epoch=1, loss=0.5)
|
|
98
|
+
"""
|
|
99
|
+
self._write(LogLevel.INFO.value, message, extra_metadata)
|
|
100
|
+
|
|
101
|
+
def warn(self, message: str, **extra_metadata) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Write warning level log.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
message: Log message
|
|
107
|
+
**extra_metadata: Additional metadata as keyword arguments
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
experiment.log().warn("High loss detected", loss=1.5)
|
|
111
|
+
"""
|
|
112
|
+
self._write(LogLevel.WARN.value, message, extra_metadata)
|
|
113
|
+
|
|
114
|
+
def error(self, message: str, **extra_metadata) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Write error level log.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
message: Log message
|
|
120
|
+
**extra_metadata: Additional metadata as keyword arguments
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
experiment.log().error("Failed to save", path="/models/checkpoint.pth")
|
|
124
|
+
"""
|
|
125
|
+
self._write(LogLevel.ERROR.value, message, extra_metadata)
|
|
126
|
+
|
|
127
|
+
def debug(self, message: str, **extra_metadata) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Write debug level log.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
message: Log message
|
|
133
|
+
**extra_metadata: Additional metadata as keyword arguments
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
experiment.log().debug("Memory usage", memory_mb=2500)
|
|
137
|
+
"""
|
|
138
|
+
self._write(LogLevel.DEBUG.value, message, extra_metadata)
|
|
139
|
+
|
|
140
|
+
def fatal(self, message: str, **extra_metadata) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Write fatal level log.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
message: Log message
|
|
146
|
+
**extra_metadata: Additional metadata as keyword arguments
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
experiment.log().fatal("Unrecoverable error", exit_code=1)
|
|
150
|
+
"""
|
|
151
|
+
self._write(LogLevel.FATAL.value, message, extra_metadata)
|
|
152
|
+
|
|
153
|
+
def _write(self, level: str, message: str, extra_metadata: Dict[str, Any]) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Internal: Execute the actual log write.
|
|
156
|
+
|
|
157
|
+
Merges metadata from log() call with metadata from level method,
|
|
158
|
+
then writes immediately (no buffering).
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
level: Log level (already validated)
|
|
162
|
+
message: Log message
|
|
163
|
+
extra_metadata: Additional metadata from level method kwargs
|
|
164
|
+
"""
|
|
165
|
+
# Merge metadata from log() call with metadata from level method
|
|
166
|
+
if self._metadata and extra_metadata:
|
|
167
|
+
final_metadata = {**self._metadata, **extra_metadata}
|
|
168
|
+
elif self._metadata:
|
|
169
|
+
final_metadata = self._metadata
|
|
170
|
+
elif extra_metadata:
|
|
171
|
+
final_metadata = extra_metadata
|
|
172
|
+
else:
|
|
173
|
+
final_metadata = None
|
|
174
|
+
|
|
175
|
+
# Write immediately (no buffering)
|
|
176
|
+
self._experiment._write_log(
|
|
177
|
+
message=message,
|
|
178
|
+
level=level,
|
|
179
|
+
metadata=final_metadata,
|
|
180
|
+
timestamp=None
|
|
181
|
+
)
|
ml_dash/metric.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metric API - Time-series data metricing for ML experiments.
|
|
3
|
+
|
|
4
|
+
Metrics are used for storing continuous data series like training metrics,
|
|
5
|
+
validation losses, system measurements, etc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .experiment import Experiment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MetricBuilder:
|
|
15
|
+
"""
|
|
16
|
+
Builder for metric operations.
|
|
17
|
+
|
|
18
|
+
Provides fluent API for appending, reading, and querying metric data.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
# Append single data point
|
|
22
|
+
experiment.metric(name="train_loss").append(value=0.5, step=100)
|
|
23
|
+
|
|
24
|
+
# Append batch
|
|
25
|
+
experiment.metric(name="train_loss").append_batch([
|
|
26
|
+
{"value": 0.5, "step": 100},
|
|
27
|
+
{"value": 0.45, "step": 101}
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
# Read data
|
|
31
|
+
data = experiment.metric(name="train_loss").read(start_index=0, limit=100)
|
|
32
|
+
|
|
33
|
+
# Get statistics
|
|
34
|
+
stats = experiment.metric(name="train_loss").stats()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, experiment: 'Experiment', name: str, description: Optional[str] = None,
|
|
38
|
+
tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize MetricBuilder.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
experiment: Parent Experiment instance
|
|
44
|
+
name: Metric name (unique within experiment)
|
|
45
|
+
description: Optional metric description
|
|
46
|
+
tags: Optional tags for categorization
|
|
47
|
+
metadata: Optional structured metadata (units, type, etc.)
|
|
48
|
+
"""
|
|
49
|
+
self._experiment = experiment
|
|
50
|
+
self._name = name
|
|
51
|
+
self._description = description
|
|
52
|
+
self._tags = tags
|
|
53
|
+
self._metadata = metadata
|
|
54
|
+
|
|
55
|
+
def append(self, **kwargs) -> 'MetricBuilder':
|
|
56
|
+
"""
|
|
57
|
+
Append a single data point to the metric.
|
|
58
|
+
|
|
59
|
+
The data point can have any structure - common patterns:
|
|
60
|
+
- {value: 0.5, step: 100}
|
|
61
|
+
- {loss: 0.3, accuracy: 0.92, epoch: 5}
|
|
62
|
+
- {timestamp: "...", temperature: 25.5, humidity: 60}
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
**kwargs: Data point fields (flexible schema)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict with metricId, index, bufferedDataPoints, chunkSize
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
result = experiment.metric(name="train_loss").append(value=0.5, step=100, epoch=1)
|
|
72
|
+
print(f"Appended at index {result['index']}")
|
|
73
|
+
"""
|
|
74
|
+
result = self._experiment._append_to_metric(
|
|
75
|
+
name=self._name,
|
|
76
|
+
data=kwargs,
|
|
77
|
+
description=self._description,
|
|
78
|
+
tags=self._tags,
|
|
79
|
+
metadata=self._metadata
|
|
80
|
+
)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def append_batch(self, data_points: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Append multiple data points in batch (more efficient than multiple append calls).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
data_points: List of data point dicts
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict with metricId, startIndex, endIndex, count, bufferedDataPoints, chunkSize
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
result = experiment.metric(name="metrics").append_batch([
|
|
95
|
+
{"loss": 0.5, "acc": 0.8, "step": 1},
|
|
96
|
+
{"loss": 0.4, "acc": 0.85, "step": 2},
|
|
97
|
+
{"loss": 0.3, "acc": 0.9, "step": 3}
|
|
98
|
+
])
|
|
99
|
+
print(f"Appended {result['count']} points")
|
|
100
|
+
"""
|
|
101
|
+
if not data_points:
|
|
102
|
+
raise ValueError("data_points cannot be empty")
|
|
103
|
+
|
|
104
|
+
result = self._experiment._append_batch_to_metric(
|
|
105
|
+
name=self._name,
|
|
106
|
+
data_points=data_points,
|
|
107
|
+
description=self._description,
|
|
108
|
+
tags=self._tags,
|
|
109
|
+
metadata=self._metadata
|
|
110
|
+
)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
def read(self, start_index: int = 0, limit: int = 1000) -> Dict[str, Any]:
|
|
114
|
+
"""
|
|
115
|
+
Read data points from the metric by index range.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
start_index: Starting index (inclusive, default 0)
|
|
119
|
+
limit: Maximum number of points to read (default 1000, max 10000)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dict with keys:
|
|
123
|
+
- data: List of {index: str, data: dict, createdAt: str}
|
|
124
|
+
- startIndex: Starting index
|
|
125
|
+
- endIndex: Ending index
|
|
126
|
+
- total: Number of points returned
|
|
127
|
+
- hasMore: Whether more data exists beyond this range
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
result = experiment.metric(name="train_loss").read(start_index=0, limit=100)
|
|
131
|
+
for point in result['data']:
|
|
132
|
+
print(f"Index {point['index']}: {point['data']}")
|
|
133
|
+
"""
|
|
134
|
+
return self._experiment._read_metric_data(
|
|
135
|
+
name=self._name,
|
|
136
|
+
start_index=start_index,
|
|
137
|
+
limit=limit
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def stats(self) -> Dict[str, Any]:
|
|
141
|
+
"""
|
|
142
|
+
Get metric statistics and metadata.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dict with metric info:
|
|
146
|
+
- metricId: Unique metric ID
|
|
147
|
+
- name: Metric name
|
|
148
|
+
- description: Metric description (if set)
|
|
149
|
+
- tags: Tags list
|
|
150
|
+
- metadata: User metadata
|
|
151
|
+
- totalDataPoints: Total points (buffered + chunked)
|
|
152
|
+
- bufferedDataPoints: Points in MongoDB (hot storage)
|
|
153
|
+
- chunkedDataPoints: Points in S3 (cold storage)
|
|
154
|
+
- totalChunks: Number of chunks in S3
|
|
155
|
+
- chunkSize: Chunking threshold
|
|
156
|
+
- firstDataAt: Timestamp of first point (if data has timestamp)
|
|
157
|
+
- lastDataAt: Timestamp of last point (if data has timestamp)
|
|
158
|
+
- createdAt: Metric creation time
|
|
159
|
+
- updatedAt: Last update time
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
stats = experiment.metric(name="train_loss").stats()
|
|
163
|
+
print(f"Total points: {stats['totalDataPoints']}")
|
|
164
|
+
print(f"Buffered: {stats['bufferedDataPoints']}, Chunked: {stats['chunkedDataPoints']}")
|
|
165
|
+
"""
|
|
166
|
+
return self._experiment._get_metric_stats(name=self._name)
|
|
167
|
+
|
|
168
|
+
def list_all(self) -> List[Dict[str, Any]]:
|
|
169
|
+
"""
|
|
170
|
+
List all metrics in the experiment.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of metric summaries with keys:
|
|
174
|
+
- metricId: Unique metric ID
|
|
175
|
+
- name: Metric name
|
|
176
|
+
- description: Metric description
|
|
177
|
+
- tags: Tags list
|
|
178
|
+
- totalDataPoints: Total data points
|
|
179
|
+
- createdAt: Creation timestamp
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
metrics = experiment.metric().list_all()
|
|
183
|
+
for metric in metrics:
|
|
184
|
+
print(f"{metric['name']}: {metric['totalDataPoints']} points")
|
|
185
|
+
"""
|
|
186
|
+
return self._experiment._list_metrics()
|