ml-dash 0.4.0__py3-none-any.whl → 0.5.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 +51 -7
- ml_dash/client.py +595 -0
- ml_dash/experiment.py +939 -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/py.typed +0 -0
- ml_dash/storage.py +941 -0
- ml_dash-0.5.1.dist-info/METADATA +240 -0
- ml_dash-0.5.1.dist-info/RECORD +12 -0
- {ml_dash-0.4.0.dist-info → ml_dash-0.5.1.dist-info}/WHEEL +1 -1
- ml_dash/ARCHITECTURE.md +0 -382
- ml_dash/autolog.py +0 -32
- ml_dash/backends/__init__.py +0 -11
- ml_dash/backends/base.py +0 -124
- ml_dash/backends/dash_backend.py +0 -571
- ml_dash/backends/local_backend.py +0 -90
- ml_dash/components/__init__.py +0 -13
- ml_dash/components/files.py +0 -246
- ml_dash/components/logs.py +0 -104
- ml_dash/components/metrics.py +0 -169
- ml_dash/components/parameters.py +0 -144
- ml_dash/job_logger.py +0 -42
- ml_dash/ml_logger.py +0 -234
- ml_dash/run.py +0 -331
- ml_dash-0.4.0.dist-info/METADATA +0 -1424
- ml_dash-0.4.0.dist-info/RECORD +0 -19
- ml_dash-0.4.0.dist-info/entry_points.txt +0 -3
ml_dash/__init__.py
CHANGED
|
@@ -1,14 +1,58 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
ML-Dash Python SDK
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
from .ml_logger import ML_Logger, LogLevel
|
|
5
|
-
from .job_logger import JobLogger
|
|
4
|
+
A simple and flexible SDK for ML experiment metricing and data storage.
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
Usage:
|
|
7
|
+
|
|
8
|
+
# Remote mode (API server)
|
|
9
|
+
from ml_dash import Experiment
|
|
10
|
+
|
|
11
|
+
with Experiment(
|
|
12
|
+
name="my-experiment",
|
|
13
|
+
project="my-project",
|
|
14
|
+
remote="http://localhost:3000",
|
|
15
|
+
api_key="your-jwt-token"
|
|
16
|
+
) as experiment:
|
|
17
|
+
experiment.log("Training started")
|
|
18
|
+
experiment.metric("loss", {"step": 0, "value": 0.5})
|
|
19
|
+
|
|
20
|
+
# Local mode (filesystem)
|
|
21
|
+
with Experiment(
|
|
22
|
+
name="my-experiment",
|
|
23
|
+
project="my-project",
|
|
24
|
+
local_path=".ml-dash"
|
|
25
|
+
) as experiment:
|
|
26
|
+
experiment.log("Training started")
|
|
27
|
+
|
|
28
|
+
# Decorator style
|
|
29
|
+
from ml_dash import ml_dash_experiment
|
|
30
|
+
|
|
31
|
+
@ml_dash_experiment(
|
|
32
|
+
name="my-experiment",
|
|
33
|
+
project="my-project",
|
|
34
|
+
remote="http://localhost:3000",
|
|
35
|
+
api_key="your-jwt-token"
|
|
36
|
+
)
|
|
37
|
+
def train_model(experiment):
|
|
38
|
+
experiment.log("Training started")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from .experiment import Experiment, ml_dash_experiment, OperationMode
|
|
42
|
+
from .client import RemoteClient
|
|
43
|
+
from .storage import LocalStorage
|
|
44
|
+
from .log import LogLevel, LogBuilder
|
|
45
|
+
from .params import ParametersBuilder
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
|
8
48
|
|
|
9
49
|
__all__ = [
|
|
10
50
|
"Experiment",
|
|
11
|
-
"
|
|
51
|
+
"ml_dash_experiment",
|
|
52
|
+
"OperationMode",
|
|
53
|
+
"RemoteClient",
|
|
54
|
+
"LocalStorage",
|
|
12
55
|
"LogLevel",
|
|
13
|
-
"
|
|
56
|
+
"LogBuilder",
|
|
57
|
+
"ParametersBuilder",
|
|
14
58
|
]
|
ml_dash/client.py
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Remote API client for ML-Dash server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Dict, Any, List
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RemoteClient:
|
|
10
|
+
"""Client for communicating with ML-Dash server."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, base_url: str, api_key: str):
|
|
13
|
+
"""
|
|
14
|
+
Initialize remote client.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
base_url: Base URL of ML-Dash server (e.g., "http://localhost:3000")
|
|
18
|
+
api_key: JWT token for authentication
|
|
19
|
+
"""
|
|
20
|
+
self.base_url = base_url.rstrip("/")
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self._client = httpx.Client(
|
|
23
|
+
base_url=self.base_url,
|
|
24
|
+
headers={
|
|
25
|
+
"Authorization": f"Bearer {api_key}",
|
|
26
|
+
# Note: Don't set Content-Type here as default
|
|
27
|
+
# It will be set per-request (json or multipart)
|
|
28
|
+
},
|
|
29
|
+
timeout=30.0,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def create_or_update_experiment(
|
|
33
|
+
self,
|
|
34
|
+
project: str,
|
|
35
|
+
name: str,
|
|
36
|
+
description: Optional[str] = None,
|
|
37
|
+
tags: Optional[List[str]] = None,
|
|
38
|
+
bindrs: Optional[List[str]] = None,
|
|
39
|
+
folder: Optional[str] = None,
|
|
40
|
+
write_protected: bool = False,
|
|
41
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Create or update an experiment.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
project: Project name
|
|
48
|
+
name: Experiment name
|
|
49
|
+
description: Optional description
|
|
50
|
+
tags: Optional list of tags
|
|
51
|
+
bindrs: Optional list of bindrs
|
|
52
|
+
folder: Optional folder path
|
|
53
|
+
write_protected: If True, experiment becomes immutable
|
|
54
|
+
metadata: Optional metadata dict
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Response dict with experiment, project, folder, and namespace data
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
httpx.HTTPStatusError: If request fails
|
|
61
|
+
"""
|
|
62
|
+
payload = {
|
|
63
|
+
"name": name,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if description is not None:
|
|
67
|
+
payload["description"] = description
|
|
68
|
+
if tags is not None:
|
|
69
|
+
payload["tags"] = tags
|
|
70
|
+
if bindrs is not None:
|
|
71
|
+
payload["bindrs"] = bindrs
|
|
72
|
+
if folder is not None:
|
|
73
|
+
payload["folder"] = folder
|
|
74
|
+
if write_protected:
|
|
75
|
+
payload["writeProtected"] = write_protected
|
|
76
|
+
if metadata is not None:
|
|
77
|
+
payload["metadata"] = metadata
|
|
78
|
+
|
|
79
|
+
response = self._client.post(
|
|
80
|
+
f"/projects/{project}/experiments",
|
|
81
|
+
json=payload,
|
|
82
|
+
)
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
return response.json()
|
|
85
|
+
|
|
86
|
+
def update_experiment_status(
|
|
87
|
+
self,
|
|
88
|
+
experiment_id: str,
|
|
89
|
+
status: str,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Update experiment status.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
experiment_id: Experiment ID
|
|
96
|
+
status: Status value - "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED"
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Response dict with updated experiment data
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
httpx.HTTPStatusError: If request fails
|
|
103
|
+
"""
|
|
104
|
+
payload = {
|
|
105
|
+
"status": status,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
response = self._client.patch(
|
|
109
|
+
f"/experiments/{experiment_id}/status",
|
|
110
|
+
json=payload,
|
|
111
|
+
)
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
return response.json()
|
|
114
|
+
|
|
115
|
+
def create_log_entries(
|
|
116
|
+
self,
|
|
117
|
+
experiment_id: str,
|
|
118
|
+
logs: List[Dict[str, Any]]
|
|
119
|
+
) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Create log entries in batch.
|
|
122
|
+
|
|
123
|
+
Supports both single log and multiple logs via array.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
127
|
+
logs: List of log entries, each with fields:
|
|
128
|
+
- timestamp: ISO 8601 string
|
|
129
|
+
- level: "info"|"warn"|"error"|"debug"|"fatal"
|
|
130
|
+
- message: Log message string
|
|
131
|
+
- metadata: Optional dict
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Response dict:
|
|
135
|
+
{
|
|
136
|
+
"created": 1,
|
|
137
|
+
"startSequence": 42,
|
|
138
|
+
"endSequence": 42,
|
|
139
|
+
"experimentId": "123456789"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
httpx.HTTPStatusError: If request fails
|
|
144
|
+
"""
|
|
145
|
+
response = self._client.post(
|
|
146
|
+
f"/experiments/{experiment_id}/logs",
|
|
147
|
+
json={"logs": logs}
|
|
148
|
+
)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
return response.json()
|
|
151
|
+
|
|
152
|
+
def set_parameters(
|
|
153
|
+
self,
|
|
154
|
+
experiment_id: str,
|
|
155
|
+
data: Dict[str, Any]
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Set/merge parameters for an experiment.
|
|
159
|
+
|
|
160
|
+
Always merges with existing parameters (upsert behavior).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
164
|
+
data: Flattened parameter dict with dot notation
|
|
165
|
+
Example: {"model.lr": 0.001, "model.batch_size": 32}
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Response dict:
|
|
169
|
+
{
|
|
170
|
+
"id": "snowflake_id",
|
|
171
|
+
"experimentId": "experiment_id",
|
|
172
|
+
"data": {...},
|
|
173
|
+
"version": 2,
|
|
174
|
+
"createdAt": "...",
|
|
175
|
+
"updatedAt": "..."
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
httpx.HTTPStatusError: If request fails
|
|
180
|
+
"""
|
|
181
|
+
response = self._client.post(
|
|
182
|
+
f"/experiments/{experiment_id}/parameters",
|
|
183
|
+
json={"data": data}
|
|
184
|
+
)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
return response.json()
|
|
187
|
+
|
|
188
|
+
def get_parameters(self, experiment_id: str) -> Dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Get parameters for an experiment.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Flattened parameter dict with dot notation
|
|
197
|
+
Example: {"model.lr": 0.001, "model.batch_size": 32}
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
httpx.HTTPStatusError: If request fails or parameters don't exist
|
|
201
|
+
"""
|
|
202
|
+
response = self._client.get(f"/experiments/{experiment_id}/parameters")
|
|
203
|
+
response.raise_for_status()
|
|
204
|
+
result = response.json()
|
|
205
|
+
return result.get("data", {})
|
|
206
|
+
|
|
207
|
+
def upload_file(
|
|
208
|
+
self,
|
|
209
|
+
experiment_id: str,
|
|
210
|
+
file_path: str,
|
|
211
|
+
prefix: str,
|
|
212
|
+
filename: str,
|
|
213
|
+
description: Optional[str],
|
|
214
|
+
tags: Optional[List[str]],
|
|
215
|
+
metadata: Optional[Dict[str, Any]],
|
|
216
|
+
checksum: str,
|
|
217
|
+
content_type: str,
|
|
218
|
+
size_bytes: int
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Upload a file to an experiment.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
225
|
+
file_path: Local file path
|
|
226
|
+
prefix: Logical path prefix
|
|
227
|
+
filename: Original filename
|
|
228
|
+
description: Optional description
|
|
229
|
+
tags: Optional tags
|
|
230
|
+
metadata: Optional metadata
|
|
231
|
+
checksum: SHA256 checksum
|
|
232
|
+
content_type: MIME type
|
|
233
|
+
size_bytes: File size in bytes
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
File metadata dict
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
httpx.HTTPStatusError: If request fails
|
|
240
|
+
"""
|
|
241
|
+
# Prepare multipart form data
|
|
242
|
+
# Read file content first (httpx needs content, not file handle)
|
|
243
|
+
with open(file_path, "rb") as f:
|
|
244
|
+
file_content = f.read()
|
|
245
|
+
|
|
246
|
+
files = {"file": (filename, file_content, content_type)}
|
|
247
|
+
data = {
|
|
248
|
+
"prefix": prefix,
|
|
249
|
+
"checksum": checksum,
|
|
250
|
+
"sizeBytes": str(size_bytes),
|
|
251
|
+
}
|
|
252
|
+
if description:
|
|
253
|
+
data["description"] = description
|
|
254
|
+
if tags:
|
|
255
|
+
data["tags"] = ",".join(tags)
|
|
256
|
+
if metadata:
|
|
257
|
+
import json
|
|
258
|
+
data["metadata"] = json.dumps(metadata)
|
|
259
|
+
|
|
260
|
+
# httpx will automatically set multipart/form-data content-type
|
|
261
|
+
response = self._client.post(
|
|
262
|
+
f"/experiments/{experiment_id}/files",
|
|
263
|
+
files=files,
|
|
264
|
+
data=data
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
response.raise_for_status()
|
|
268
|
+
return response.json()
|
|
269
|
+
|
|
270
|
+
def list_files(
|
|
271
|
+
self,
|
|
272
|
+
experiment_id: str,
|
|
273
|
+
prefix: Optional[str] = None,
|
|
274
|
+
tags: Optional[List[str]] = None
|
|
275
|
+
) -> List[Dict[str, Any]]:
|
|
276
|
+
"""
|
|
277
|
+
List files in an experiment.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
281
|
+
prefix: Optional prefix filter
|
|
282
|
+
tags: Optional tags filter
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of file metadata dicts
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
httpx.HTTPStatusError: If request fails
|
|
289
|
+
"""
|
|
290
|
+
params = {}
|
|
291
|
+
if prefix:
|
|
292
|
+
params["prefix"] = prefix
|
|
293
|
+
if tags:
|
|
294
|
+
params["tags"] = ",".join(tags)
|
|
295
|
+
|
|
296
|
+
response = self._client.get(
|
|
297
|
+
f"/experiments/{experiment_id}/files",
|
|
298
|
+
params=params
|
|
299
|
+
)
|
|
300
|
+
response.raise_for_status()
|
|
301
|
+
result = response.json()
|
|
302
|
+
return result.get("files", [])
|
|
303
|
+
|
|
304
|
+
def get_file(self, experiment_id: str, file_id: str) -> Dict[str, Any]:
|
|
305
|
+
"""
|
|
306
|
+
Get file metadata.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
310
|
+
file_id: File ID (Snowflake ID)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
File metadata dict
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
httpx.HTTPStatusError: If request fails
|
|
317
|
+
"""
|
|
318
|
+
response = self._client.get(f"/experiments/{experiment_id}/files/{file_id}")
|
|
319
|
+
response.raise_for_status()
|
|
320
|
+
return response.json()
|
|
321
|
+
|
|
322
|
+
def download_file(
|
|
323
|
+
self,
|
|
324
|
+
experiment_id: str,
|
|
325
|
+
file_id: str,
|
|
326
|
+
dest_path: Optional[str] = None
|
|
327
|
+
) -> str:
|
|
328
|
+
"""
|
|
329
|
+
Download a file from a experiment.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
333
|
+
file_id: File ID (Snowflake ID)
|
|
334
|
+
dest_path: Optional destination path (defaults to original filename)
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Path to downloaded file
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
httpx.HTTPStatusError: If request fails
|
|
341
|
+
ValueError: If checksum verification fails
|
|
342
|
+
"""
|
|
343
|
+
# Get file metadata first to get filename and checksum
|
|
344
|
+
file_metadata = self.get_file(experiment_id, file_id)
|
|
345
|
+
filename = file_metadata["filename"]
|
|
346
|
+
expected_checksum = file_metadata["checksum"]
|
|
347
|
+
|
|
348
|
+
# Determine destination path
|
|
349
|
+
if dest_path is None:
|
|
350
|
+
dest_path = filename
|
|
351
|
+
|
|
352
|
+
# Download file
|
|
353
|
+
response = self._client.get(
|
|
354
|
+
f"/experiments/{experiment_id}/files/{file_id}/download"
|
|
355
|
+
)
|
|
356
|
+
response.raise_for_status()
|
|
357
|
+
|
|
358
|
+
# Write to file
|
|
359
|
+
with open(dest_path, "wb") as f:
|
|
360
|
+
f.write(response.content)
|
|
361
|
+
|
|
362
|
+
# Verify checksum
|
|
363
|
+
from .files import verify_checksum
|
|
364
|
+
if not verify_checksum(dest_path, expected_checksum):
|
|
365
|
+
# Delete corrupted file
|
|
366
|
+
import os
|
|
367
|
+
os.remove(dest_path)
|
|
368
|
+
raise ValueError(f"Checksum verification failed for file {file_id}")
|
|
369
|
+
|
|
370
|
+
return dest_path
|
|
371
|
+
|
|
372
|
+
def delete_file(self, experiment_id: str, file_id: str) -> Dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Delete a file (soft delete).
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
378
|
+
file_id: File ID (Snowflake ID)
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Dict with id and deletedAt
|
|
382
|
+
|
|
383
|
+
Raises:
|
|
384
|
+
httpx.HTTPStatusError: If request fails
|
|
385
|
+
"""
|
|
386
|
+
response = self._client.delete(f"/experiments/{experiment_id}/files/{file_id}")
|
|
387
|
+
response.raise_for_status()
|
|
388
|
+
return response.json()
|
|
389
|
+
|
|
390
|
+
def update_file(
|
|
391
|
+
self,
|
|
392
|
+
experiment_id: str,
|
|
393
|
+
file_id: str,
|
|
394
|
+
description: Optional[str] = None,
|
|
395
|
+
tags: Optional[List[str]] = None,
|
|
396
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
397
|
+
) -> Dict[str, Any]:
|
|
398
|
+
"""
|
|
399
|
+
Update file metadata.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
403
|
+
file_id: File ID (Snowflake ID)
|
|
404
|
+
description: Optional description
|
|
405
|
+
tags: Optional tags
|
|
406
|
+
metadata: Optional metadata
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Updated file metadata dict
|
|
410
|
+
|
|
411
|
+
Raises:
|
|
412
|
+
httpx.HTTPStatusError: If request fails
|
|
413
|
+
"""
|
|
414
|
+
payload = {}
|
|
415
|
+
if description is not None:
|
|
416
|
+
payload["description"] = description
|
|
417
|
+
if tags is not None:
|
|
418
|
+
payload["tags"] = tags
|
|
419
|
+
if metadata is not None:
|
|
420
|
+
payload["metadata"] = metadata
|
|
421
|
+
|
|
422
|
+
response = self._client.patch(
|
|
423
|
+
f"/experiments/{experiment_id}/files/{file_id}",
|
|
424
|
+
json=payload
|
|
425
|
+
)
|
|
426
|
+
response.raise_for_status()
|
|
427
|
+
return response.json()
|
|
428
|
+
|
|
429
|
+
def append_to_metric(
|
|
430
|
+
self,
|
|
431
|
+
experiment_id: str,
|
|
432
|
+
metric_name: str,
|
|
433
|
+
data: Dict[str, Any],
|
|
434
|
+
description: Optional[str] = None,
|
|
435
|
+
tags: Optional[List[str]] = None,
|
|
436
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
437
|
+
) -> Dict[str, Any]:
|
|
438
|
+
"""
|
|
439
|
+
Append a single data point to a metric.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
443
|
+
metric_name: Metric name (unique within experiment)
|
|
444
|
+
data: Data point (flexible schema)
|
|
445
|
+
description: Optional metric description
|
|
446
|
+
tags: Optional tags
|
|
447
|
+
metadata: Optional metadata
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Dict with metricId, index, bufferedDataPoints, chunkSize
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
httpx.HTTPStatusError: If request fails
|
|
454
|
+
"""
|
|
455
|
+
payload = {"data": data}
|
|
456
|
+
if description:
|
|
457
|
+
payload["description"] = description
|
|
458
|
+
if tags:
|
|
459
|
+
payload["tags"] = tags
|
|
460
|
+
if metadata:
|
|
461
|
+
payload["metadata"] = metadata
|
|
462
|
+
|
|
463
|
+
response = self._client.post(
|
|
464
|
+
f"/experiments/{experiment_id}/metrics/{metric_name}/append",
|
|
465
|
+
json=payload
|
|
466
|
+
)
|
|
467
|
+
response.raise_for_status()
|
|
468
|
+
return response.json()
|
|
469
|
+
|
|
470
|
+
def append_batch_to_metric(
|
|
471
|
+
self,
|
|
472
|
+
experiment_id: str,
|
|
473
|
+
metric_name: str,
|
|
474
|
+
data_points: List[Dict[str, Any]],
|
|
475
|
+
description: Optional[str] = None,
|
|
476
|
+
tags: Optional[List[str]] = None,
|
|
477
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
478
|
+
) -> Dict[str, Any]:
|
|
479
|
+
"""
|
|
480
|
+
Append multiple data points to a metric in batch.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
484
|
+
metric_name: Metric name (unique within experiment)
|
|
485
|
+
data_points: List of data points
|
|
486
|
+
description: Optional metric description
|
|
487
|
+
tags: Optional tags
|
|
488
|
+
metadata: Optional metadata
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Dict with metricId, startIndex, endIndex, count, bufferedDataPoints, chunkSize
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
httpx.HTTPStatusError: If request fails
|
|
495
|
+
"""
|
|
496
|
+
payload = {"dataPoints": data_points}
|
|
497
|
+
if description:
|
|
498
|
+
payload["description"] = description
|
|
499
|
+
if tags:
|
|
500
|
+
payload["tags"] = tags
|
|
501
|
+
if metadata:
|
|
502
|
+
payload["metadata"] = metadata
|
|
503
|
+
|
|
504
|
+
response = self._client.post(
|
|
505
|
+
f"/experiments/{experiment_id}/metrics/{metric_name}/append-batch",
|
|
506
|
+
json=payload
|
|
507
|
+
)
|
|
508
|
+
response.raise_for_status()
|
|
509
|
+
return response.json()
|
|
510
|
+
|
|
511
|
+
def read_metric_data(
|
|
512
|
+
self,
|
|
513
|
+
experiment_id: str,
|
|
514
|
+
metric_name: str,
|
|
515
|
+
start_index: int = 0,
|
|
516
|
+
limit: int = 1000
|
|
517
|
+
) -> Dict[str, Any]:
|
|
518
|
+
"""
|
|
519
|
+
Read data points from a metric.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
523
|
+
metric_name: Metric name
|
|
524
|
+
start_index: Starting index (default 0)
|
|
525
|
+
limit: Max points to read (default 1000, max 10000)
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Dict with data, startIndex, endIndex, total, hasMore
|
|
529
|
+
|
|
530
|
+
Raises:
|
|
531
|
+
httpx.HTTPStatusError: If request fails
|
|
532
|
+
"""
|
|
533
|
+
response = self._client.get(
|
|
534
|
+
f"/experiments/{experiment_id}/metrics/{metric_name}/data",
|
|
535
|
+
params={"startIndex": start_index, "limit": limit}
|
|
536
|
+
)
|
|
537
|
+
response.raise_for_status()
|
|
538
|
+
return response.json()
|
|
539
|
+
|
|
540
|
+
def get_metric_stats(
|
|
541
|
+
self,
|
|
542
|
+
experiment_id: str,
|
|
543
|
+
metric_name: str
|
|
544
|
+
) -> Dict[str, Any]:
|
|
545
|
+
"""
|
|
546
|
+
Get metric statistics and metadata.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
550
|
+
metric_name: Metric name
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Dict with metric stats (totalDataPoints, bufferedDataPoints, etc.)
|
|
554
|
+
|
|
555
|
+
Raises:
|
|
556
|
+
httpx.HTTPStatusError: If request fails
|
|
557
|
+
"""
|
|
558
|
+
response = self._client.get(
|
|
559
|
+
f"/experiments/{experiment_id}/metrics/{metric_name}/stats"
|
|
560
|
+
)
|
|
561
|
+
response.raise_for_status()
|
|
562
|
+
return response.json()
|
|
563
|
+
|
|
564
|
+
def list_metrics(
|
|
565
|
+
self,
|
|
566
|
+
experiment_id: str
|
|
567
|
+
) -> List[Dict[str, Any]]:
|
|
568
|
+
"""
|
|
569
|
+
List all metrics in an experiment.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
List of metric summaries
|
|
576
|
+
|
|
577
|
+
Raises:
|
|
578
|
+
httpx.HTTPStatusError: If request fails
|
|
579
|
+
"""
|
|
580
|
+
response = self._client.get(f"/experiments/{experiment_id}/metrics")
|
|
581
|
+
response.raise_for_status()
|
|
582
|
+
return response.json()["metrics"]
|
|
583
|
+
|
|
584
|
+
def close(self):
|
|
585
|
+
"""Close the HTTP client."""
|
|
586
|
+
self._client.close()
|
|
587
|
+
|
|
588
|
+
def __enter__(self):
|
|
589
|
+
"""Context manager entry."""
|
|
590
|
+
return self
|
|
591
|
+
|
|
592
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
593
|
+
"""Context manager exit."""
|
|
594
|
+
self.close()
|
|
595
|
+
return False
|