ml-dash 0.6.4__py3-none-any.whl → 0.6.6__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 CHANGED
@@ -37,7 +37,7 @@ Usage:
37
37
  """
38
38
 
39
39
  from .client import RemoteClient
40
- from .experiment import Experiment, OperationMode, RunManager, ml_dash_experiment
40
+ from .experiment import Experiment, OperationMode, ml_dash_experiment
41
41
  from .log import LogBuilder, LogLevel
42
42
  from .params import ParametersBuilder
43
43
  from .run import RUN
@@ -49,7 +49,6 @@ __all__ = [
49
49
  "Experiment",
50
50
  "ml_dash_experiment",
51
51
  "OperationMode",
52
- "RunManager",
53
52
  "RemoteClient",
54
53
  "LocalStorage",
55
54
  "LogLevel",
ml_dash/auto_start.py CHANGED
@@ -43,10 +43,7 @@ _user = get_jwt_user()
43
43
  _username = _user["username"] if _user else getpass.getuser()
44
44
  _now = datetime.now()
45
45
 
46
- dxp = Experiment(
47
- prefix=f"{_username}/scratch/{_now:%Y-%m-%d/%H%M%S}",
48
- dash_url="https://api.dash.ml",
49
- )
46
+ dxp = Experiment()
50
47
 
51
48
 
52
49
  # Register cleanup handler to complete experiment on Python exit (if still open)
ml_dash/cli.py CHANGED
@@ -25,7 +25,7 @@ def create_parser() -> argparse.ArgumentParser:
25
25
  )
26
26
 
27
27
  # Import and add command parsers
28
- from .cli_commands import upload, download, list as list_cmd, login, logout, profile, api
28
+ from .cli_commands import upload, download, list as list_cmd, login, logout, profile, api, create
29
29
 
30
30
  # Authentication commands
31
31
  login.add_parser(subparsers)
@@ -35,6 +35,9 @@ def create_parser() -> argparse.ArgumentParser:
35
35
  # API commands
36
36
  api.add_parser(subparsers)
37
37
 
38
+ # Project commands
39
+ create.add_parser(subparsers)
40
+
38
41
  # Data commands
39
42
  upload.add_parser(subparsers)
40
43
  download.add_parser(subparsers)
@@ -71,6 +74,9 @@ def main(argv: Optional[List[str]] = None) -> int:
71
74
  elif args.command == "profile":
72
75
  from .cli_commands import profile
73
76
  return profile.cmd_profile(args)
77
+ elif args.command == "create":
78
+ from .cli_commands import create
79
+ return create.cmd_create(args)
74
80
  elif args.command == "upload":
75
81
  from .cli_commands import upload
76
82
  return upload.cmd_upload(args)
@@ -0,0 +1,145 @@
1
+ """Create command for ml-dash CLI - create projects."""
2
+
3
+ import argparse
4
+ from typing import Optional
5
+
6
+ from rich.console import Console
7
+
8
+ from ml_dash.client import RemoteClient
9
+ from ml_dash.config import config
10
+
11
+
12
+ def add_parser(subparsers):
13
+ """Add create command parser."""
14
+ parser = subparsers.add_parser(
15
+ "create",
16
+ help="Create a new project",
17
+ description="""Create a new project in ml-dash.
18
+
19
+ Examples:
20
+ # Create a project in current user's namespace
21
+ ml-dash create -p new-project
22
+
23
+ # Create a project in a specific namespace
24
+ ml-dash create -p geyang/new-project
25
+
26
+ # Create with description
27
+ ml-dash create -p geyang/tutorials -d "ML tutorials and examples"
28
+ """,
29
+ formatter_class=argparse.RawDescriptionHelpFormatter,
30
+ )
31
+ parser.add_argument(
32
+ "-p", "--prefix",
33
+ type=str,
34
+ required=True,
35
+ help="Project name or namespace/project",
36
+ )
37
+ parser.add_argument(
38
+ "-d", "--description",
39
+ type=str,
40
+ help="Project description (optional)",
41
+ )
42
+ parser.add_argument(
43
+ "--dash-url",
44
+ type=str,
45
+ help="ML-Dash server URL (default: https://api.dash.ml)",
46
+ )
47
+
48
+
49
+ def cmd_create(args) -> int:
50
+ """Execute create command."""
51
+ console = Console()
52
+
53
+ # Get remote URL
54
+ remote_url = args.dash_url or config.remote_url or "https://api.dash.ml"
55
+
56
+ # Parse the prefix
57
+ prefix = args.prefix.strip("/")
58
+ parts = prefix.split("/")
59
+
60
+ if len(parts) > 2:
61
+ console.print(
62
+ f"[red]Error:[/red] Prefix can have at most 2 parts (namespace/project).\n"
63
+ f"Got: {args.prefix}\n\n"
64
+ f"Examples:\n"
65
+ f" ml-dash create -p new-project\n"
66
+ f" ml-dash create -p geyang/new-project"
67
+ )
68
+ return 1
69
+
70
+ if len(parts) == 1:
71
+ # Format: project (use current user's namespace)
72
+ namespace = None
73
+ project_name = parts[0]
74
+ else:
75
+ # Format: namespace/project
76
+ namespace = parts[0]
77
+ project_name = parts[1]
78
+
79
+ return _create_project(
80
+ namespace=namespace,
81
+ project_name=project_name,
82
+ description=args.description,
83
+ dash_url=remote_url,
84
+ console=console,
85
+ )
86
+
87
+
88
+ def _create_project(
89
+ namespace: Optional[str],
90
+ project_name: str,
91
+ description: Optional[str],
92
+ dash_url: str,
93
+ console: Console,
94
+ ) -> int:
95
+ """Create a new project."""
96
+ try:
97
+ # Initialize client (namespace will be auto-fetched from server if not provided)
98
+ client = RemoteClient(base_url=dash_url, namespace=namespace)
99
+
100
+ # Get namespace (triggers server query if not set)
101
+ namespace = client.namespace
102
+
103
+ if not namespace:
104
+ console.print("[red]Error:[/red] Could not determine namespace. Please login first.")
105
+ return 1
106
+
107
+ console.print(f"[dim]Creating project '{project_name}' in namespace '{namespace}'[/dim]")
108
+
109
+ # Create project using unified node API
110
+ response = client._client.post(
111
+ f"/namespaces/{namespace}/nodes",
112
+ json={
113
+ "type": "PROJECT",
114
+ "name": project_name,
115
+ "slug": project_name,
116
+ "description": description or "",
117
+ }
118
+ )
119
+ response.raise_for_status()
120
+ result = response.json()
121
+
122
+ # Extract project info
123
+ project = result.get("project", {})
124
+ project_id = project.get("id")
125
+ project_slug = project.get("slug")
126
+
127
+ # Success message
128
+ console.print(f"[green]✓[/green] Project created successfully!")
129
+ console.print(f" Name: [bold]{project_slug}[/bold]")
130
+ console.print(f" Namespace: [bold]{namespace}[/bold]")
131
+ console.print(f" ID: {project_id}")
132
+ if description:
133
+ console.print(f" Description: {description}")
134
+ console.print(f"\n View at: https://dash.ml/@{namespace}/{project_slug}")
135
+
136
+ return 0
137
+
138
+ except Exception as e:
139
+ # Check if it's a 409 conflict (project already exists)
140
+ if hasattr(e, 'response') and hasattr(e.response, 'status_code') and e.response.status_code == 409:
141
+ console.print(f"[yellow]⚠[/yellow] Project '[bold]{project_name}[/bold]' already exists in namespace '[bold]{namespace}[/bold]'")
142
+ return 0
143
+
144
+ console.print(f"[red]Error creating project:[/red] {e}")
145
+ return 1
@@ -265,7 +265,11 @@ def discover_experiments(
265
265
 
266
266
  Args:
267
267
  local_path: Root path of local storage
268
- project_filter: Glob pattern to filter experiments by prefix (e.g., "tom/*/exp*")
268
+ project_filter: Either a simple project name (e.g., "proj1") or a glob
269
+ pattern for the full path (e.g., "tom/*/exp*"). If the
270
+ filter contains '/', '*', or '?', it's treated as a glob
271
+ pattern matched against the full relative path. Otherwise,
272
+ it's matched exactly against the project name.
269
273
  experiment_filter: Only discover this experiment (requires project_filter)
270
274
 
271
275
  Returns:
@@ -319,9 +323,18 @@ def discover_experiments(
319
323
 
320
324
  # Apply filters with glob pattern support
321
325
  if project_filter:
322
- # Support glob pattern matching on the full relative path
323
- if not fnmatch.fnmatch(full_relative_path, project_filter):
324
- continue
326
+ # Check if project_filter is a glob pattern or simple project name
327
+ is_glob_pattern = any(c in project_filter for c in ['*', '?', '/'])
328
+
329
+ if is_glob_pattern:
330
+ # Treat as glob pattern - match against full relative path
331
+ if not fnmatch.fnmatch(full_relative_path, project_filter):
332
+ continue
333
+ else:
334
+ # Treat as simple project name - match against parsed project
335
+ if project_name != project_filter:
336
+ continue
337
+
325
338
  if experiment_filter and exp_name != experiment_filter:
326
339
  continue
327
340