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/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
- filename
1286
- path
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
- filename
1353
- path
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
- filename
1428
- path
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()