ml-dash 0.6.0__py3-none-any.whl → 0.6.2__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
@@ -17,8 +17,9 @@ class RemoteClient:
17
17
  base_url: Base URL of ML-Dash server (e.g., "http://localhost:3000")
18
18
  api_key: JWT token for authentication (optional - auto-loads from storage if not provided)
19
19
 
20
- Raises:
21
- AuthenticationError: If no api_key provided and no token found in storage
20
+ Note:
21
+ If no api_key is provided, token will be loaded from storage on first API call.
22
+ If still not found, AuthenticationError will be raised at that time.
22
23
  """
23
24
  # Store original base URL for GraphQL (no /api prefix)
24
25
  self.graphql_base_url = base_url.rstrip("/")
@@ -29,38 +30,52 @@ class RemoteClient:
29
30
  # If no api_key provided, try to load from storage
30
31
  if not api_key:
31
32
  from .auth.token_storage import get_token_storage
32
- from .auth.exceptions import AuthenticationError
33
33
 
34
34
  storage = get_token_storage()
35
35
  api_key = storage.load("ml-dash-token")
36
36
 
37
- if not api_key:
38
- raise AuthenticationError(
39
- "Not authenticated. Run 'ml-dash login' to authenticate, "
40
- "or provide an explicit api_key parameter."
41
- )
42
-
43
37
  self.api_key = api_key
38
+ self._rest_client = None
39
+ self._gql_client = None
44
40
 
45
- # REST API client (with /api prefix)
46
- self._client = httpx.Client(
47
- base_url=self.base_url,
48
- headers={
49
- "Authorization": f"Bearer {api_key}",
50
- # Note: Don't set Content-Type here as default
51
- # It will be set per-request (json or multipart)
52
- },
53
- timeout=30.0,
54
- )
55
-
56
- # GraphQL client (without /api prefix)
57
- self._graphql_client = httpx.Client(
58
- base_url=self.graphql_base_url,
59
- headers={
60
- "Authorization": f"Bearer {api_key}",
61
- },
62
- timeout=30.0,
63
- )
41
+ def _ensure_authenticated(self):
42
+ """Check if authenticated, raise error if not."""
43
+ if not self.api_key:
44
+ from .auth.exceptions import AuthenticationError
45
+ raise AuthenticationError(
46
+ "Not authenticated. Run 'ml-dash login' to authenticate, "
47
+ "or provide an explicit api_key parameter."
48
+ )
49
+
50
+ @property
51
+ def _client(self):
52
+ """Lazy REST API client (with /api prefix)."""
53
+ if self._rest_client is None:
54
+ self._ensure_authenticated()
55
+ self._rest_client = httpx.Client(
56
+ base_url=self.base_url,
57
+ headers={
58
+ "Authorization": f"Bearer {self.api_key}",
59
+ # Note: Don't set Content-Type here as default
60
+ # It will be set per-request (json or multipart)
61
+ },
62
+ timeout=30.0,
63
+ )
64
+ return self._rest_client
65
+
66
+ @property
67
+ def _graphql_client(self):
68
+ """Lazy GraphQL client (without /api prefix)."""
69
+ if self._gql_client is None:
70
+ self._ensure_authenticated()
71
+ self._gql_client = httpx.Client(
72
+ base_url=self.graphql_base_url,
73
+ headers={
74
+ "Authorization": f"Bearer {self.api_key}",
75
+ },
76
+ timeout=30.0,
77
+ )
78
+ return self._gql_client
64
79
 
65
80
  def create_or_update_experiment(
66
81
  self,
@@ -69,7 +84,7 @@ class RemoteClient:
69
84
  description: Optional[str] = None,
70
85
  tags: Optional[List[str]] = None,
71
86
  bindrs: Optional[List[str]] = None,
72
- folder: Optional[str] = None,
87
+ prefix: Optional[str] = None,
73
88
  write_protected: bool = False,
74
89
  metadata: Optional[Dict[str, Any]] = None,
75
90
  ) -> Dict[str, Any]:
@@ -78,16 +93,16 @@ class RemoteClient:
78
93
 
79
94
  Args:
80
95
  project: Project name
81
- name: Experiment name
96
+ name: Experiment name (last segment of prefix)
82
97
  description: Optional description
83
98
  tags: Optional list of tags
84
99
  bindrs: Optional list of bindrs
85
- folder: Optional folder path
100
+ prefix: Full prefix path sent to backend for folder hierarchy creation
86
101
  write_protected: If True, experiment becomes immutable
87
102
  metadata: Optional metadata dict
88
103
 
89
104
  Returns:
90
- Response dict with experiment, project, folder, and namespace data
105
+ Response dict with experiment, project, and namespace data
91
106
 
92
107
  Raises:
93
108
  httpx.HTTPStatusError: If request fails
@@ -102,12 +117,12 @@ class RemoteClient:
102
117
  payload["tags"] = tags
103
118
  if bindrs is not None:
104
119
  payload["bindrs"] = bindrs
105
- if folder is not None:
106
- payload["folder"] = folder
107
120
  if write_protected:
108
121
  payload["writeProtected"] = write_protected
109
122
  if metadata is not None:
110
123
  payload["metadata"] = metadata
124
+ if prefix is not None:
125
+ payload["prefix"] = prefix
111
126
 
112
127
  response = self._client.post(
113
128
  f"/projects/{project}/experiments",
@@ -713,6 +728,9 @@ class RemoteClient:
713
728
  metadata
714
729
  project {
715
730
  slug
731
+ namespace {
732
+ slug
733
+ }
716
734
  }
717
735
  logMetadata {
718
736
  totalLogs
@@ -777,6 +795,9 @@ class RemoteClient:
777
795
  metadata
778
796
  project {
779
797
  slug
798
+ namespace {
799
+ slug
800
+ }
780
801
  }
781
802
  logMetadata {
782
803
  totalLogs
@@ -813,6 +834,73 @@ class RemoteClient:
813
834
  result = self.graphql_query(query, variables)
814
835
  return result.get("experiment")
815
836
 
837
+ def search_experiments_graphql(self, pattern: str) -> List[Dict[str, Any]]:
838
+ """
839
+ Search experiments using glob pattern via GraphQL.
840
+
841
+ Pattern format: namespace/project/experiment
842
+ Supports wildcards: *, ?, [0-9], [a-z], etc.
843
+
844
+ Args:
845
+ pattern: Glob pattern (e.g., "tom*/tutorials/*", "*/project-?/exp*")
846
+
847
+ Returns:
848
+ List of experiment dicts matching the pattern
849
+
850
+ Raises:
851
+ httpx.HTTPStatusError: If request fails
852
+
853
+ Examples:
854
+ >>> client.search_experiments_graphql("tom*/tutorials/*")
855
+ >>> client.search_experiments_graphql("*/my-project/baseline*")
856
+ """
857
+ query = """
858
+ query SearchExperiments($pattern: String!) {
859
+ searchExperiments(pattern: $pattern) {
860
+ id
861
+ name
862
+ description
863
+ tags
864
+ status
865
+ startedAt
866
+ endedAt
867
+ metadata
868
+ project {
869
+ id
870
+ slug
871
+ name
872
+ namespace {
873
+ id
874
+ slug
875
+ }
876
+ }
877
+ logMetadata {
878
+ totalLogs
879
+ }
880
+ metrics {
881
+ name
882
+ metricMetadata {
883
+ totalDataPoints
884
+ }
885
+ }
886
+ files {
887
+ id
888
+ filename
889
+ path
890
+ contentType
891
+ sizeBytes
892
+ checksum
893
+ description
894
+ tags
895
+ metadata
896
+ }
897
+ }
898
+ }
899
+ """
900
+ variables = {"pattern": pattern}
901
+ result = self.graphql_query(query, variables)
902
+ return result.get("searchExperiments", [])
903
+
816
904
  def download_file_streaming(
817
905
  self, experiment_id: str, file_id: str, dest_path: str
818
906
  ) -> str:
ml_dash/config.py CHANGED
@@ -1,132 +1,132 @@
1
1
  """Configuration file management for ML-Dash CLI."""
2
2
 
3
- from pathlib import Path
4
3
  import json
5
- from typing import Optional, Dict, Any
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
6
 
7
7
 
8
8
  class Config:
9
+ """
10
+ Manages ML-Dash CLI configuration file.
11
+
12
+ Configuration is stored in ~/.dash/config.json with structure:
13
+ {
14
+ "remote_url": "https://api.dash.ml",
15
+ "api_key": "token",
16
+ "default_batch_size": 100
17
+ }
18
+ """
19
+
20
+ DEFAULT_CONFIG_DIR = Path.home() / ".dash"
21
+ CONFIG_FILE = "config.json"
22
+
23
+ def __init__(self, config_dir: Optional[Path] = None):
9
24
  """
10
- Manages ML-Dash CLI configuration file.
11
-
12
- Configuration is stored in ~/.ml-dash/config.json with structure:
13
- {
14
- "remote_url": "https://api.dash.ml",
15
- "api_key": "token",
16
- "default_batch_size": 100
17
- }
18
- """
25
+ Initialize config manager.
19
26
 
20
- DEFAULT_CONFIG_DIR = Path.home() / ".ml-dash"
21
- CONFIG_FILE = "config.json"
22
-
23
- def __init__(self, config_dir: Optional[Path] = None):
24
- """
25
- Initialize config manager.
26
-
27
- Args:
28
- config_dir: Config directory path (defaults to ~/.ml-dash)
29
- """
30
- self.config_dir = config_dir or self.DEFAULT_CONFIG_DIR
31
- self.config_path = self.config_dir / self.CONFIG_FILE
32
- self._data = self._load()
33
-
34
- def _load(self) -> Dict[str, Any]:
35
- """Load config from file."""
36
- if self.config_path.exists():
37
- try:
38
- with open(self.config_path, "r") as f:
39
- return json.load(f)
40
- except (json.JSONDecodeError, IOError):
41
- # If config is corrupted, return empty dict
42
- return {}
27
+ Args:
28
+ config_dir: Config directory path (defaults to ~/.dash)
29
+ """
30
+ self.config_dir = config_dir or self.DEFAULT_CONFIG_DIR
31
+ self.config_path = self.config_dir / self.CONFIG_FILE
32
+ self._data = self._load()
33
+
34
+ def _load(self) -> Dict[str, Any]:
35
+ """Load config from file."""
36
+ if self.config_path.exists():
37
+ try:
38
+ with open(self.config_path, "r") as f:
39
+ return json.load(f)
40
+ except (json.JSONDecodeError, IOError):
41
+ # If config is corrupted, return empty dict
43
42
  return {}
43
+ return {}
44
+
45
+ def save(self):
46
+ """Save config to file."""
47
+ self.config_dir.mkdir(parents=True, exist_ok=True)
48
+ with open(self.config_path, "w") as f:
49
+ json.dump(self._data, f, indent=2)
50
+
51
+ def get(self, key: str, default: Any = None) -> Any:
52
+ """
53
+ Get config value.
44
54
 
45
- def save(self):
46
- """Save config to file."""
47
- self.config_dir.mkdir(parents=True, exist_ok=True)
48
- with open(self.config_path, "w") as f:
49
- json.dump(self._data, f, indent=2)
50
-
51
- def get(self, key: str, default: Any = None) -> Any:
52
- """
53
- Get config value.
54
-
55
- Args:
56
- key: Config key
57
- default: Default value if key not found
58
-
59
- Returns:
60
- Config value or default
61
- """
62
- return self._data.get(key, default)
63
-
64
- def set(self, key: str, value: Any):
65
- """
66
- Set config value and save.
67
-
68
- Args:
69
- key: Config key
70
- value: Config value
71
- """
72
- self._data[key] = value
73
- self.save()
74
-
75
- def delete(self, key: str):
76
- """
77
- Delete config key and save.
78
-
79
- Args:
80
- key: Config key to delete
81
- """
82
- if key in self._data:
83
- del self._data[key]
84
- self.save()
85
-
86
- def clear(self):
87
- """Clear all config and save."""
88
- self._data = {}
89
- self.save()
90
-
91
- @property
92
- def remote_url(self) -> Optional[str]:
93
- """Get default remote URL."""
94
- return self.get("remote_url", "https://api.dash.ml")
95
-
96
- @remote_url.setter
97
- def remote_url(self, url: str):
98
- """Set default remote URL."""
99
- self.set("remote_url", url)
100
-
101
- @property
102
- def api_key(self) -> Optional[str]:
103
- """Get default API key."""
104
- return self.get("api_key")
105
-
106
- @api_key.setter
107
- def api_key(self, key: str):
108
- """Set default API key."""
109
- self.set("api_key", key)
110
-
111
- @property
112
- def batch_size(self) -> int:
113
- """Get default batch size for uploads."""
114
- return self.get("default_batch_size", 100)
115
-
116
- @batch_size.setter
117
- def batch_size(self, size: int):
118
- """Set default batch size."""
119
- self.set("default_batch_size", size)
120
-
121
- @property
122
- def device_secret(self) -> Optional[str]:
123
- """Get device secret for OAuth device flow."""
124
- return self.get("device_secret")
125
-
126
- @device_secret.setter
127
- def device_secret(self, secret: str):
128
- """Set device secret."""
129
- self.set("device_secret", secret)
55
+ Args:
56
+ key: Config key
57
+ default: Default value if key not found
58
+
59
+ Returns:
60
+ Config value or default
61
+ """
62
+ return self._data.get(key, default)
63
+
64
+ def set(self, key: str, value: Any):
65
+ """
66
+ Set config value and save.
67
+
68
+ Args:
69
+ key: Config key
70
+ value: Config value
71
+ """
72
+ self._data[key] = value
73
+ self.save()
74
+
75
+ def delete(self, key: str):
76
+ """
77
+ Delete config key and save.
78
+
79
+ Args:
80
+ key: Config key to delete
81
+ """
82
+ if key in self._data:
83
+ del self._data[key]
84
+ self.save()
85
+
86
+ def clear(self):
87
+ """Clear all config and save."""
88
+ self._data = {}
89
+ self.save()
90
+
91
+ @property
92
+ def remote_url(self) -> Optional[str]:
93
+ """Get default remote URL."""
94
+ return self.get("remote_url", "https://api.dash.ml")
95
+
96
+ @remote_url.setter
97
+ def remote_url(self, url: str):
98
+ """Set default remote URL."""
99
+ self.set("remote_url", url)
100
+
101
+ @property
102
+ def api_key(self) -> Optional[str]:
103
+ """Get default API key."""
104
+ return self.get("api_key")
105
+
106
+ @api_key.setter
107
+ def api_key(self, key: str):
108
+ """Set default API key."""
109
+ self.set("api_key", key)
110
+
111
+ @property
112
+ def batch_size(self) -> int:
113
+ """Get default batch size for uploads."""
114
+ return self.get("default_batch_size", 100)
115
+
116
+ @batch_size.setter
117
+ def batch_size(self, size: int):
118
+ """Set default batch size."""
119
+ self.set("default_batch_size", size)
120
+
121
+ @property
122
+ def device_secret(self) -> Optional[str]:
123
+ """Get device secret for OAuth device flow."""
124
+ return self.get("device_secret")
125
+
126
+ @device_secret.setter
127
+ def device_secret(self, secret: str):
128
+ """Set device secret."""
129
+ self.set("device_secret", secret)
130
130
 
131
131
 
132
132
  # Global config instance