ml-dash 0.6.6__py3-none-any.whl → 0.6.7__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/buffer.py +735 -0
- ml_dash/cli_commands/download.py +177 -0
- ml_dash/cli_commands/list.py +146 -0
- ml_dash/cli_commands/upload.py +131 -0
- ml_dash/client.py +265 -20
- ml_dash/experiment.py +286 -126
- ml_dash/files.py +228 -70
- ml_dash/storage.py +403 -0
- ml_dash/track.py +263 -0
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.7.dist-info}/METADATA +1 -1
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.7.dist-info}/RECORD +13 -11
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.7.dist-info}/WHEEL +0 -0
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.7.dist-info}/entry_points.txt +0 -0
ml_dash/client.py
CHANGED
|
@@ -6,6 +6,45 @@ from typing import Optional, Dict, Any, List
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def _serialize_value(value: Any) -> Any:
|
|
10
|
+
"""
|
|
11
|
+
Convert value to JSON-serializable format.
|
|
12
|
+
|
|
13
|
+
Handles numpy arrays, nested dicts/lists, etc.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
value: Value to serialize
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
JSON-serializable value
|
|
20
|
+
"""
|
|
21
|
+
# Check for numpy array
|
|
22
|
+
if hasattr(value, '__array__') or (hasattr(value, 'tolist') and hasattr(value, 'dtype')):
|
|
23
|
+
# It's a numpy array
|
|
24
|
+
try:
|
|
25
|
+
return value.tolist()
|
|
26
|
+
except AttributeError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# Check for numpy scalar types
|
|
30
|
+
if hasattr(value, 'item'):
|
|
31
|
+
try:
|
|
32
|
+
return value.item()
|
|
33
|
+
except (AttributeError, ValueError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# Recursively handle dicts
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
return {k: _serialize_value(v) for k, v in value.items()}
|
|
39
|
+
|
|
40
|
+
# Recursively handle lists
|
|
41
|
+
if isinstance(value, (list, tuple)):
|
|
42
|
+
return [_serialize_value(v) for v in value]
|
|
43
|
+
|
|
44
|
+
# Return as-is for other types (int, float, str, bool, None)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
9
48
|
class RemoteClient:
|
|
10
49
|
"""Client for communicating with ML-Dash server."""
|
|
11
50
|
|
|
@@ -1282,14 +1321,18 @@ class RemoteClient:
|
|
|
1282
1321
|
}
|
|
1283
1322
|
files {
|
|
1284
1323
|
id
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
contentType
|
|
1288
|
-
sizeBytes
|
|
1289
|
-
checksum
|
|
1324
|
+
name
|
|
1325
|
+
pPath
|
|
1290
1326
|
description
|
|
1291
1327
|
tags
|
|
1292
1328
|
metadata
|
|
1329
|
+
physicalFile {
|
|
1330
|
+
filename
|
|
1331
|
+
contentType
|
|
1332
|
+
sizeBytes
|
|
1333
|
+
checksum
|
|
1334
|
+
s3Url
|
|
1335
|
+
}
|
|
1293
1336
|
}
|
|
1294
1337
|
parameters {
|
|
1295
1338
|
id
|
|
@@ -1306,16 +1349,15 @@ class RemoteClient:
|
|
|
1306
1349
|
return result.get("experiments", [])
|
|
1307
1350
|
|
|
1308
1351
|
def get_experiment_graphql(
|
|
1309
|
-
self, project_slug: str, experiment_name: str
|
|
1352
|
+
self, project_slug: str, experiment_name: str, namespace_slug: Optional[str] = None
|
|
1310
1353
|
) -> Optional[Dict[str, Any]]:
|
|
1311
1354
|
"""
|
|
1312
1355
|
Get a single experiment via GraphQL.
|
|
1313
1356
|
|
|
1314
|
-
Namespace is automatically inferred from JWT token on the server.
|
|
1315
|
-
|
|
1316
1357
|
Args:
|
|
1317
1358
|
project_slug: Project slug
|
|
1318
1359
|
experiment_name: Experiment name
|
|
1360
|
+
namespace_slug: Namespace slug (optional - defaults to client's namespace)
|
|
1319
1361
|
|
|
1320
1362
|
Returns:
|
|
1321
1363
|
Experiment dict with metadata, or None if not found
|
|
@@ -1324,8 +1366,8 @@ class RemoteClient:
|
|
|
1324
1366
|
httpx.HTTPStatusError: If request fails
|
|
1325
1367
|
"""
|
|
1326
1368
|
query = """
|
|
1327
|
-
query Experiment($projectSlug: String!, $experimentName: String!) {
|
|
1328
|
-
experiment(projectSlug: $projectSlug, experimentName: $experimentName) {
|
|
1369
|
+
query Experiment($namespaceSlug: String, $projectSlug: String!, $experimentName: String!) {
|
|
1370
|
+
experiment(namespaceSlug: $namespaceSlug, projectSlug: $projectSlug, experimentName: $experimentName) {
|
|
1329
1371
|
id
|
|
1330
1372
|
name
|
|
1331
1373
|
description
|
|
@@ -1349,14 +1391,18 @@ class RemoteClient:
|
|
|
1349
1391
|
}
|
|
1350
1392
|
files {
|
|
1351
1393
|
id
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
contentType
|
|
1355
|
-
sizeBytes
|
|
1356
|
-
checksum
|
|
1394
|
+
name
|
|
1395
|
+
pPath
|
|
1357
1396
|
description
|
|
1358
1397
|
tags
|
|
1359
1398
|
metadata
|
|
1399
|
+
physicalFile {
|
|
1400
|
+
filename
|
|
1401
|
+
contentType
|
|
1402
|
+
sizeBytes
|
|
1403
|
+
checksum
|
|
1404
|
+
s3Url
|
|
1405
|
+
}
|
|
1360
1406
|
}
|
|
1361
1407
|
parameters {
|
|
1362
1408
|
id
|
|
@@ -1365,7 +1411,11 @@ class RemoteClient:
|
|
|
1365
1411
|
}
|
|
1366
1412
|
}
|
|
1367
1413
|
"""
|
|
1414
|
+
# Use provided namespace or fall back to client's namespace
|
|
1415
|
+
ns = namespace_slug or self.namespace
|
|
1416
|
+
|
|
1368
1417
|
variables = {
|
|
1418
|
+
"namespaceSlug": ns,
|
|
1369
1419
|
"projectSlug": project_slug,
|
|
1370
1420
|
"experimentName": experiment_name
|
|
1371
1421
|
}
|
|
@@ -1424,14 +1474,18 @@ class RemoteClient:
|
|
|
1424
1474
|
}
|
|
1425
1475
|
files {
|
|
1426
1476
|
id
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
contentType
|
|
1430
|
-
sizeBytes
|
|
1431
|
-
checksum
|
|
1477
|
+
name
|
|
1478
|
+
pPath
|
|
1432
1479
|
description
|
|
1433
1480
|
tags
|
|
1434
1481
|
metadata
|
|
1482
|
+
physicalFile {
|
|
1483
|
+
filename
|
|
1484
|
+
contentType
|
|
1485
|
+
sizeBytes
|
|
1486
|
+
checksum
|
|
1487
|
+
s3Url
|
|
1488
|
+
}
|
|
1435
1489
|
}
|
|
1436
1490
|
}
|
|
1437
1491
|
}
|
|
@@ -1601,6 +1655,197 @@ class RemoteClient:
|
|
|
1601
1655
|
response.raise_for_status()
|
|
1602
1656
|
return response.json()
|
|
1603
1657
|
|
|
1658
|
+
# =============================================================================
|
|
1659
|
+
# Track Methods
|
|
1660
|
+
# =============================================================================
|
|
1661
|
+
|
|
1662
|
+
def create_track(
|
|
1663
|
+
self,
|
|
1664
|
+
experiment_id: str,
|
|
1665
|
+
topic: str,
|
|
1666
|
+
description: Optional[str] = None,
|
|
1667
|
+
tags: Optional[List[str]] = None,
|
|
1668
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1669
|
+
) -> Dict[str, Any]:
|
|
1670
|
+
"""
|
|
1671
|
+
Create a new track for timestamped multi-modal data.
|
|
1672
|
+
|
|
1673
|
+
Args:
|
|
1674
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
1675
|
+
topic: Track topic (e.g., "robot/position", "camera/rgb")
|
|
1676
|
+
description: Optional track description
|
|
1677
|
+
tags: Optional tags
|
|
1678
|
+
metadata: Optional metadata (e.g., fps, units)
|
|
1679
|
+
|
|
1680
|
+
Returns:
|
|
1681
|
+
Dict with track ID and metadata
|
|
1682
|
+
|
|
1683
|
+
Raises:
|
|
1684
|
+
httpx.HTTPStatusError: If request fails
|
|
1685
|
+
"""
|
|
1686
|
+
payload = {
|
|
1687
|
+
"topic": topic,
|
|
1688
|
+
}
|
|
1689
|
+
if description:
|
|
1690
|
+
payload["description"] = description
|
|
1691
|
+
if tags:
|
|
1692
|
+
payload["tags"] = tags
|
|
1693
|
+
if metadata:
|
|
1694
|
+
payload["metadata"] = metadata
|
|
1695
|
+
|
|
1696
|
+
response = self._client.post(
|
|
1697
|
+
f"/experiments/{experiment_id}/tracks",
|
|
1698
|
+
json=payload,
|
|
1699
|
+
)
|
|
1700
|
+
response.raise_for_status()
|
|
1701
|
+
return response.json()
|
|
1702
|
+
|
|
1703
|
+
def append_to_track(
|
|
1704
|
+
self,
|
|
1705
|
+
experiment_id: str,
|
|
1706
|
+
topic: str,
|
|
1707
|
+
timestamp: float,
|
|
1708
|
+
data: Dict[str, Any],
|
|
1709
|
+
) -> Dict[str, Any]:
|
|
1710
|
+
"""
|
|
1711
|
+
Append a single entry to a track.
|
|
1712
|
+
|
|
1713
|
+
Args:
|
|
1714
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
1715
|
+
topic: Track topic (e.g., "robot/position")
|
|
1716
|
+
timestamp: Numeric timestamp
|
|
1717
|
+
data: Data fields as dict (will be flattened with dot-notation)
|
|
1718
|
+
|
|
1719
|
+
Returns:
|
|
1720
|
+
Dict with timestamp and flattened data
|
|
1721
|
+
|
|
1722
|
+
Raises:
|
|
1723
|
+
httpx.HTTPStatusError: If request fails
|
|
1724
|
+
"""
|
|
1725
|
+
import urllib.parse
|
|
1726
|
+
|
|
1727
|
+
topic_encoded = urllib.parse.quote(topic, safe='')
|
|
1728
|
+
|
|
1729
|
+
response = self._client.post(
|
|
1730
|
+
f"/experiments/{experiment_id}/tracks/{topic_encoded}/append",
|
|
1731
|
+
json={"timestamp": timestamp, "data": data},
|
|
1732
|
+
)
|
|
1733
|
+
response.raise_for_status()
|
|
1734
|
+
return response.json()
|
|
1735
|
+
|
|
1736
|
+
def append_batch_to_track(
|
|
1737
|
+
self,
|
|
1738
|
+
experiment_id: str,
|
|
1739
|
+
topic: str,
|
|
1740
|
+
entries: List[Dict[str, Any]],
|
|
1741
|
+
) -> Dict[str, Any]:
|
|
1742
|
+
"""
|
|
1743
|
+
Append multiple entries to a track in batch.
|
|
1744
|
+
|
|
1745
|
+
Args:
|
|
1746
|
+
experiment_id: Experiment ID (Snowflake ID)
|
|
1747
|
+
topic: Track topic (e.g., "robot/position")
|
|
1748
|
+
entries: List of entries, each with 'timestamp' and other data fields
|
|
1749
|
+
|
|
1750
|
+
Returns:
|
|
1751
|
+
Dict with count of entries added
|
|
1752
|
+
|
|
1753
|
+
Raises:
|
|
1754
|
+
httpx.HTTPStatusError: If request fails
|
|
1755
|
+
"""
|
|
1756
|
+
import urllib.parse
|
|
1757
|
+
|
|
1758
|
+
topic_encoded = urllib.parse.quote(topic, safe='')
|
|
1759
|
+
|
|
1760
|
+
# Serialize entries to handle numpy arrays
|
|
1761
|
+
serialized_entries = [_serialize_value(entry) for entry in entries]
|
|
1762
|
+
|
|
1763
|
+
response = self._client.post(
|
|
1764
|
+
f"/experiments/{experiment_id}/tracks/{topic_encoded}/append_batch",
|
|
1765
|
+
json={"entries": serialized_entries},
|
|
1766
|
+
)
|
|
1767
|
+
response.raise_for_status()
|
|
1768
|
+
return response.json()
|
|
1769
|
+
|
|
1770
|
+
def get_track_data(
|
|
1771
|
+
self,
|
|
1772
|
+
experiment_id: str,
|
|
1773
|
+
topic: str,
|
|
1774
|
+
start_timestamp: Optional[float] = None,
|
|
1775
|
+
end_timestamp: Optional[float] = None,
|
|
1776
|
+
columns: Optional[List[str]] = None,
|
|
1777
|
+
format: str = "json",
|
|
1778
|
+
) -> Any:
|
|
1779
|
+
"""
|
|
1780
|
+
Get track data with optional filtering.
|
|
1781
|
+
|
|
1782
|
+
Args:
|
|
1783
|
+
experiment_id: Experiment ID
|
|
1784
|
+
topic: Track topic
|
|
1785
|
+
start_timestamp: Optional start timestamp filter
|
|
1786
|
+
end_timestamp: Optional end timestamp filter
|
|
1787
|
+
columns: Optional list of columns to retrieve
|
|
1788
|
+
format: Export format ('json', 'jsonl', 'parquet', 'mcap')
|
|
1789
|
+
|
|
1790
|
+
Returns:
|
|
1791
|
+
Track data in requested format (dict for json, bytes for jsonl/parquet/mcap)
|
|
1792
|
+
|
|
1793
|
+
Raises:
|
|
1794
|
+
httpx.HTTPStatusError: If request fails
|
|
1795
|
+
"""
|
|
1796
|
+
import urllib.parse
|
|
1797
|
+
|
|
1798
|
+
topic_encoded = urllib.parse.quote(topic, safe='')
|
|
1799
|
+
params: Dict[str, str] = {"format": format}
|
|
1800
|
+
|
|
1801
|
+
if start_timestamp is not None:
|
|
1802
|
+
params["start_timestamp"] = str(start_timestamp)
|
|
1803
|
+
if end_timestamp is not None:
|
|
1804
|
+
params["end_timestamp"] = str(end_timestamp)
|
|
1805
|
+
if columns:
|
|
1806
|
+
params["columns"] = ",".join(columns)
|
|
1807
|
+
|
|
1808
|
+
response = self._client.get(
|
|
1809
|
+
f"/experiments/{experiment_id}/tracks/{topic_encoded}/data",
|
|
1810
|
+
params=params,
|
|
1811
|
+
)
|
|
1812
|
+
response.raise_for_status()
|
|
1813
|
+
|
|
1814
|
+
# Return bytes for binary formats, dict for JSON
|
|
1815
|
+
if format in ("jsonl", "parquet", "mcap"):
|
|
1816
|
+
return response.content
|
|
1817
|
+
return response.json()
|
|
1818
|
+
|
|
1819
|
+
def list_tracks(
|
|
1820
|
+
self,
|
|
1821
|
+
experiment_id: str,
|
|
1822
|
+
topic_filter: Optional[str] = None,
|
|
1823
|
+
) -> List[Dict[str, Any]]:
|
|
1824
|
+
"""
|
|
1825
|
+
List all tracks in an experiment.
|
|
1826
|
+
|
|
1827
|
+
Args:
|
|
1828
|
+
experiment_id: Experiment ID
|
|
1829
|
+
topic_filter: Optional topic filter (e.g., "robot/*" for prefix match)
|
|
1830
|
+
|
|
1831
|
+
Returns:
|
|
1832
|
+
List of track metadata dicts
|
|
1833
|
+
|
|
1834
|
+
Raises:
|
|
1835
|
+
httpx.HTTPStatusError: If request fails
|
|
1836
|
+
"""
|
|
1837
|
+
params: Dict[str, str] = {}
|
|
1838
|
+
if topic_filter:
|
|
1839
|
+
params["topic"] = topic_filter
|
|
1840
|
+
|
|
1841
|
+
response = self._client.get(
|
|
1842
|
+
f"/experiments/{experiment_id}/tracks",
|
|
1843
|
+
params=params,
|
|
1844
|
+
)
|
|
1845
|
+
response.raise_for_status()
|
|
1846
|
+
result = response.json()
|
|
1847
|
+
return result.get("tracks", [])
|
|
1848
|
+
|
|
1604
1849
|
def close(self):
|
|
1605
1850
|
"""Close the HTTP clients."""
|
|
1606
1851
|
self._client.close()
|