lyceum-cli 1.0.24__tar.gz → 1.0.25__tar.gz

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.
Files changed (50) hide show
  1. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/PKG-INFO +1 -1
  2. lyceum_cli-1.0.25/lyceum/external/compute/execution/docker_config.py +123 -0
  3. lyceum_cli-1.0.25/lyceum/external/compute/execution/notebook.py +242 -0
  4. lyceum_cli-1.0.25/lyceum/external/storage/__init__.py +0 -0
  5. lyceum_cli-1.0.25/lyceum/external/storage/files.py +273 -0
  6. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/main.py +2 -0
  7. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cli.egg-info/PKG-INFO +1 -1
  8. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cli.egg-info/SOURCES.txt +4 -0
  9. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/setup.py +1 -1
  10. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/__init__.py +0 -0
  11. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/__init__.py +0 -0
  12. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/auth/__init__.py +0 -0
  13. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/auth/login.py +0 -0
  14. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/__init__.py +0 -0
  15. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/execution/__init__.py +0 -0
  16. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/execution/config.py +0 -0
  17. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/execution/docker.py +0 -0
  18. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/execution/python.py +0 -0
  19. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/execution/workloads.py +0 -0
  20. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/inference/__init__.py +0 -0
  21. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/inference/batch.py +0 -0
  22. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/inference/chat.py +0 -0
  23. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/compute/inference/models.py +0 -0
  24. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/external/general/__init__.py +0 -0
  25. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/shared/__init__.py +0 -0
  26. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/shared/config.py +0 -0
  27. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/shared/display.py +0 -0
  28. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/shared/imports.py +0 -0
  29. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum/shared/streaming.py +0 -0
  30. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cli.egg-info/dependency_links.txt +0 -0
  31. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cli.egg-info/entry_points.txt +0 -0
  32. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cli.egg-info/requires.txt +0 -0
  33. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cli.egg-info/top_level.txt +0 -0
  34. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cloud_execution_api_client/__init__.py +0 -0
  35. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cloud_execution_api_client/api/__init__.py +0 -0
  36. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/lyceum_cloud_execution_api_client/models/__init__.py +0 -0
  37. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/setup.cfg +0 -0
  38. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/__init__.py +0 -0
  39. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/conftest.py +0 -0
  40. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/__init__.py +0 -0
  41. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/__init__.py +0 -0
  42. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/compute/__init__.py +0 -0
  43. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/compute/execution/__init__.py +0 -0
  44. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/compute/execution/test_data.py +0 -0
  45. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/compute/execution/test_dependency_resolver.py +0 -0
  46. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/compute/execution/test_python_helpers.py +0 -0
  47. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/external/compute/execution/test_python_run.py +0 -0
  48. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/shared/__init__.py +0 -0
  49. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/shared/test_config.py +0 -0
  50. {lyceum_cli-1.0.24 → lyceum_cli-1.0.25}/tests/unit/shared/test_streaming.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lyceum-cli
3
- Version: 1.0.24
3
+ Version: 1.0.25
4
4
  Summary: Command-line interface for Lyceum Cloud Execution API
5
5
  Home-page: https://lyceum.technology
6
6
  Author: Lyceum Team
@@ -0,0 +1,123 @@
1
+ """Docker registry configuration commands"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ console = Console()
11
+
12
+ docker_config_app = typer.Typer(name="config", help="Docker registry configuration")
13
+
14
+ CONFIG_DIR = Path.home() / ".lyceum"
15
+ DOCKER_CONFIG_FILE = CONFIG_DIR / "docker-registries.json"
16
+
17
+
18
+ def load_docker_config() -> dict:
19
+ """Load docker registry configuration."""
20
+ if DOCKER_CONFIG_FILE.exists():
21
+ try:
22
+ with open(DOCKER_CONFIG_FILE) as f:
23
+ return json.load(f)
24
+ except Exception:
25
+ pass
26
+ return {"registries": {}}
27
+
28
+
29
+ def save_docker_config(config: dict) -> None:
30
+ """Save docker registry configuration."""
31
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
32
+ with open(DOCKER_CONFIG_FILE, "w") as f:
33
+ json.dump(config, f, indent=2)
34
+ # Set restrictive permissions since this contains credentials
35
+ DOCKER_CONFIG_FILE.chmod(0o600)
36
+
37
+
38
+ def get_hub_credentials(image: str) -> dict | None:
39
+ """Get Docker Hub credentials if the image is from Docker Hub.
40
+
41
+ Docker Hub images either have no registry prefix or use docker.io.
42
+ Returns None if no credentials found or image is not from Docker Hub.
43
+ """
44
+ config = load_docker_config()
45
+ registries = config.get("registries", {})
46
+
47
+ if "hub" not in registries:
48
+ return None
49
+
50
+ # Check if this looks like a Docker Hub image
51
+ # Docker Hub: no dots in the first part, or explicitly docker.io
52
+ if "/" not in image:
53
+ # Single name like "python:3.9" - it's Docker Hub
54
+ return registries["hub"]
55
+
56
+ parts = image.split("/")
57
+ first_part = parts[0]
58
+
59
+ # If first part has a dot, it's a custom registry (not Docker Hub)
60
+ # Exception: docker.io is Docker Hub
61
+ if "." in first_part and first_part != "docker.io":
62
+ return None
63
+
64
+ # Otherwise it's Docker Hub (e.g., "myuser/myimage" or "docker.io/myuser/myimage")
65
+ return registries["hub"]
66
+
67
+
68
+ @docker_config_app.command("hub")
69
+ def configure_hub():
70
+ """Configure Docker Hub credentials interactively."""
71
+ console.print("\n[bold]Docker Hub Configuration[/bold]")
72
+ console.print("[dim]These credentials will be used for private Docker Hub images.[/dim]\n")
73
+
74
+ username = typer.prompt("Docker Hub Username")
75
+ password = typer.prompt("Docker Hub Password/Token", hide_input=True)
76
+
77
+ config = load_docker_config()
78
+ config["registries"]["hub"] = {
79
+ "username": username,
80
+ "password": password,
81
+ }
82
+ save_docker_config(config)
83
+
84
+ console.print("\n[green]Docker Hub credentials saved![/green]")
85
+ console.print("[dim]Credentials stored in ~/.lyceum/docker-registries.json[/dim]")
86
+
87
+
88
+ @docker_config_app.command("show")
89
+ def show_config():
90
+ """Show configured docker registries."""
91
+ config = load_docker_config()
92
+ registries = config.get("registries", {})
93
+
94
+ if not registries:
95
+ console.print("[dim]No docker registries configured.[/dim]")
96
+ console.print("[dim]Run 'lyceum docker config hub' to configure Docker Hub.[/dim]")
97
+ console.print("[dim]For AWS ECR, use 'lyceum docker run <image> --aws' to auto-detect credentials.[/dim]")
98
+ return
99
+
100
+ table = Table(title="Configured Docker Registries")
101
+ table.add_column("Registry", style="cyan")
102
+ table.add_column("Details", style="dim")
103
+
104
+ if "hub" in registries:
105
+ hub = registries["hub"]
106
+ username = hub.get("username", "")
107
+ table.add_row("Docker Hub", f"User: {username}")
108
+
109
+ console.print(table)
110
+ console.print("\n[dim]For AWS ECR, use 'lyceum docker run <image> --aws' to auto-detect credentials.[/dim]")
111
+
112
+
113
+ @docker_config_app.command("clear")
114
+ def clear_config():
115
+ """Clear saved Docker Hub credentials."""
116
+ config = load_docker_config()
117
+
118
+ if "hub" in config.get("registries", {}):
119
+ del config["registries"]["hub"]
120
+ save_docker_config(config)
121
+ console.print("[green]Cleared Docker Hub credentials.[/green]")
122
+ else:
123
+ console.print("[yellow]No Docker Hub credentials found.[/yellow]")
@@ -0,0 +1,242 @@
1
+ """
2
+ Jupyter Notebook execution commands
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import httpx
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from ....shared.config import config
12
+ from ....shared.streaming import StatusLine
13
+
14
+ console = Console()
15
+
16
+ notebook_app = typer.Typer(name="notebook", help="Launch Jupyter notebooks on Lyceum Cloud")
17
+
18
+ # Pre-built Jupyter notebook image (linux/amd64)
19
+ JUPYTER_IMAGE = "jupyter/base-notebook:latest"
20
+
21
+
22
+ @notebook_app.command("launch")
23
+ def launch_notebook(
24
+ machine_type: str = typer.Option(
25
+ "cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"
26
+ ),
27
+ timeout: int = typer.Option(
28
+ 600, "--timeout", "-t", help="Session timeout in seconds (max: 600)"
29
+ ),
30
+ image: Optional[str] = typer.Option(
31
+ None, "--image", "-i", help="Custom Jupyter image (default: jupyter/base-notebook)"
32
+ ),
33
+ token: str = typer.Option(
34
+ "lyceum", "--token", help="Jupyter notebook token for authentication"
35
+ ),
36
+ port: int = typer.Option(
37
+ 8888, "--port", "-p", help="Port for Jupyter server"
38
+ ),
39
+ ):
40
+ """Launch a Jupyter notebook server on Lyceum Cloud.
41
+
42
+ Starts a Jupyter notebook that you can access in your browser.
43
+ The notebook URL will be printed once the server is ready.
44
+
45
+ Examples:
46
+ lyceum notebook launch
47
+ lyceum notebook launch -m h100
48
+ lyceum notebook launch -m a100 --timeout 7200
49
+ lyceum notebook launch --image jupyter/scipy-notebook
50
+ """
51
+ status = StatusLine()
52
+
53
+ try:
54
+ config.get_client()
55
+
56
+ status.start()
57
+ status.update("Preparing notebook environment...")
58
+
59
+ jupyter_image = image or JUPYTER_IMAGE
60
+
61
+ # Build the Jupyter start command
62
+ # Using start-notebook.sh with custom options
63
+ jupyter_cmd = [
64
+ "start-notebook.sh",
65
+ f"--NotebookApp.token={token}",
66
+ f"--port={port}",
67
+ "--ip=0.0.0.0",
68
+ "--no-browser",
69
+ ]
70
+
71
+ # Build request for v2 image API
72
+ image_request = {
73
+ "docker_image_ref": jupyter_image,
74
+ "docker_run_cmd": jupyter_cmd,
75
+ "timeout": timeout,
76
+ "execution_type": machine_type,
77
+ }
78
+
79
+ status.update(f"Starting Jupyter on {machine_type}...")
80
+
81
+ response = httpx.post(
82
+ f"{config.base_url}/api/v2/external/execution/image/start",
83
+ json=image_request,
84
+ headers={"Authorization": f"Bearer {config.api_key}"},
85
+ timeout=30.0,
86
+ )
87
+
88
+ if response.status_code != 200:
89
+ status.stop()
90
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
91
+ if response.status_code == 401:
92
+ console.print(
93
+ "[red]Authentication failed. Your session may have expired.[/red]"
94
+ )
95
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
96
+ elif response.content:
97
+ console.print(f"[red]{response.content.decode()}[/red]")
98
+ raise typer.Exit(1)
99
+
100
+ result = response.json()
101
+ execution_id = result.get("execution_id")
102
+
103
+ # Build the notebook URL immediately
104
+ notebook_url = f"https://{execution_id}-{port}.port.lyceum.technology"
105
+ full_url = f"{notebook_url}/?token={token}"
106
+
107
+ status.stop()
108
+
109
+ # Print URL immediately so user can click it
110
+ console.print()
111
+ console.print("[bold green]Notebook starting![/bold green]")
112
+ console.print()
113
+ console.print(f"[cyan]{full_url}[/cyan]")
114
+ console.print()
115
+ console.print(f"[dim]Execution ID:[/dim] {execution_id}")
116
+ console.print()
117
+ console.print("[dim]To stop:[/dim] lyceum notebook stop " + execution_id)
118
+
119
+ except typer.Exit:
120
+ status.stop()
121
+ raise
122
+ except Exception as e:
123
+ status.stop()
124
+ console.print(f"[red]Error: {e}[/red]")
125
+ raise typer.Exit(1)
126
+
127
+
128
+ @notebook_app.command("stop")
129
+ def stop_notebook(
130
+ execution_id: str = typer.Argument(..., help="Execution ID of the notebook to stop"),
131
+ ):
132
+ """Stop a running Jupyter notebook.
133
+
134
+ Examples:
135
+ lyceum notebook stop 9d73319c-6f1c-4b4c-90e4-044244353ce4
136
+ """
137
+ status = StatusLine()
138
+
139
+ try:
140
+ config.get_client()
141
+
142
+ status.start()
143
+ status.update("Stopping notebook...")
144
+
145
+ response = httpx.post(
146
+ f"{config.base_url}/api/v2/external/workloads/abort/{execution_id}",
147
+ headers={"Authorization": f"Bearer {config.api_key}"},
148
+ timeout=30.0,
149
+ )
150
+
151
+ status.stop()
152
+
153
+ if response.status_code == 200:
154
+ console.print(f"[green]Notebook {execution_id} stopped.[/green]")
155
+ elif response.status_code == 404:
156
+ console.print(f"[yellow]Notebook {execution_id} not found or already stopped.[/yellow]")
157
+ else:
158
+ console.print(f"[red]Error stopping notebook: HTTP {response.status_code}[/red]")
159
+ if response.content:
160
+ console.print(f"[red]{response.content.decode()}[/red]")
161
+ raise typer.Exit(1)
162
+
163
+ except typer.Exit:
164
+ status.stop()
165
+ raise
166
+ except Exception as e:
167
+ status.stop()
168
+ console.print(f"[red]Error: {e}[/red]")
169
+ raise typer.Exit(1)
170
+
171
+
172
+ @notebook_app.command("list")
173
+ def list_notebooks():
174
+ """List running Jupyter notebooks.
175
+
176
+ Shows all active notebook sessions with their URLs and status.
177
+ """
178
+ status = StatusLine()
179
+
180
+ try:
181
+ config.get_client()
182
+
183
+ status.start()
184
+ status.update("Fetching running notebooks...")
185
+
186
+ # Use the workloads API to get running executions
187
+ response = httpx.get(
188
+ f"{config.base_url}/api/v2/external/workloads/list",
189
+ headers={"Authorization": f"Bearer {config.api_key}"},
190
+ timeout=30.0,
191
+ )
192
+
193
+ status.stop()
194
+
195
+ if response.status_code != 200:
196
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
197
+ if response.content:
198
+ console.print(f"[red]{response.content.decode()}[/red]")
199
+ raise typer.Exit(1)
200
+
201
+ executions = response.json()
202
+
203
+ # Filter for notebook executions (those running on port 8888 or with jupyter)
204
+ # Since we don't have image info, show all running executions
205
+ notebooks = [
206
+ e for e in executions
207
+ if e.get("status") in ["running", "pending", "queued", "starting"]
208
+ ]
209
+
210
+ if not notebooks:
211
+ console.print("[dim]No running workloads found.[/dim]")
212
+ return
213
+
214
+ console.print(f"[bold]Running Workloads ({len(notebooks)})[/bold]")
215
+ console.print()
216
+
217
+ for nb in notebooks:
218
+ exec_id = nb.get("execution_id", "unknown")
219
+ status_val = nb.get("status", "unknown")
220
+ created = nb.get("created_at", "")
221
+ file_name = nb.get("file_name", "")
222
+
223
+ # Assume port 8888 for Jupyter
224
+ url = f"https://{exec_id}-8888.port.lyceum.technology"
225
+
226
+ console.print(f"[cyan]{exec_id}[/cyan]")
227
+ if file_name:
228
+ console.print(f" Name: {file_name}")
229
+ console.print(f" Status: {status_val}")
230
+ console.print(f" URL: {url}")
231
+ if created:
232
+ console.print(f" Created: {created}")
233
+ console.print()
234
+
235
+ except typer.Exit:
236
+ raise
237
+ except Exception as e:
238
+ status.stop()
239
+ console.print(f"[red]Error: {e}[/red]")
240
+ raise typer.Exit(1)
241
+
242
+
File without changes
@@ -0,0 +1,273 @@
1
+ """Storage file management commands"""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ...shared.config import config
12
+
13
+ console = Console()
14
+
15
+ storage_app = typer.Typer(name="storage", help="File storage commands")
16
+
17
+
18
+ def format_size(size_bytes: int) -> str:
19
+ """Format bytes into human-readable size."""
20
+ for unit in ["B", "KB", "MB", "GB"]:
21
+ if size_bytes < 1024:
22
+ return f"{size_bytes:.1f} {unit}"
23
+ size_bytes /= 1024
24
+ return f"{size_bytes:.1f} TB"
25
+
26
+
27
+ @storage_app.command("list")
28
+ def list_files(
29
+ prefix: str = typer.Option("", "--prefix", "-p", help="Filter by prefix/folder"),
30
+ limit: int = typer.Option(100, "--limit", "-n", help="Maximum files to list"),
31
+ ):
32
+ """List files in your storage bucket."""
33
+ try:
34
+ config.get_client()
35
+
36
+ params = {"prefix": prefix, "max_files": limit}
37
+
38
+ response = httpx.get(
39
+ f"{config.base_url}/api/v2/external/storage/list-files",
40
+ headers={"Authorization": f"Bearer {config.api_key}"},
41
+ params=params,
42
+ timeout=30.0,
43
+ )
44
+
45
+ if response.status_code != 200:
46
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
47
+ if response.status_code == 401:
48
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
49
+ else:
50
+ console.print(f"[red]{response.content.decode()}[/red]")
51
+ raise typer.Exit(1)
52
+
53
+ files = response.json()
54
+
55
+ if not files:
56
+ console.print("[dim]No files found.[/dim]")
57
+ if prefix:
58
+ console.print(f"[dim]Prefix filter: {prefix}[/dim]")
59
+ return
60
+
61
+ table = Table(title="Storage Files")
62
+ table.add_column("Key", style="cyan")
63
+ table.add_column("Size", style="green", justify="right")
64
+ table.add_column("Last Modified", style="dim")
65
+
66
+ for f in files:
67
+ last_mod = f.get("last_modified", "")
68
+ if last_mod:
69
+ try:
70
+ dt = datetime.fromisoformat(last_mod.replace("Z", "+00:00"))
71
+ last_mod = dt.strftime("%Y-%m-%d %H:%M")
72
+ except Exception:
73
+ pass
74
+
75
+ table.add_row(
76
+ f.get("key", ""),
77
+ format_size(f.get("size", 0)),
78
+ last_mod,
79
+ )
80
+
81
+ console.print(table)
82
+ console.print(f"\n[dim]Total: {len(files)} file(s)[/dim]")
83
+
84
+ except typer.Exit:
85
+ raise
86
+ except Exception as e:
87
+ console.print(f"[red]Error: {e}[/red]")
88
+ raise typer.Exit(1)
89
+
90
+
91
+ @storage_app.command("upload")
92
+ def upload_file(
93
+ file_path: Path = typer.Argument(..., help="Local file to upload"),
94
+ dest: str = typer.Option(None, "--dest", "-d", help="Destination path in storage (defaults to filename)"),
95
+ ):
96
+ """Upload a file to your storage bucket."""
97
+ try:
98
+ config.get_client()
99
+
100
+ if not file_path.exists():
101
+ console.print(f"[red]Error: File not found: {file_path}[/red]")
102
+ raise typer.Exit(1)
103
+
104
+ if not file_path.is_file():
105
+ console.print(f"[red]Error: Not a file: {file_path}[/red]")
106
+ raise typer.Exit(1)
107
+
108
+ remote_path = dest or file_path.name
109
+ file_size = file_path.stat().st_size
110
+
111
+ console.print(f"[dim]Uploading {file_path.name} ({format_size(file_size)})...[/dim]")
112
+
113
+ with open(file_path, "rb") as f:
114
+ files = {"file": (file_path.name, f)}
115
+ data = {"key": remote_path} if dest else {}
116
+
117
+ response = httpx.post(
118
+ f"{config.base_url}/api/v2/external/storage/upload",
119
+ headers={"Authorization": f"Bearer {config.api_key}"},
120
+ files=files,
121
+ data=data,
122
+ timeout=300.0, # 5 min timeout for large files
123
+ )
124
+
125
+ if response.status_code != 200:
126
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
127
+ if response.status_code == 401:
128
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
129
+ else:
130
+ console.print(f"[red]{response.content.decode()}[/red]")
131
+ raise typer.Exit(1)
132
+
133
+ result = response.json()
134
+ console.print(f"[green]Uploaded successfully![/green]")
135
+ console.print(f"[dim]Key: {result.get('key')}[/dim]")
136
+ console.print(f"[dim]Size: {format_size(result.get('size', 0))}[/dim]")
137
+
138
+ except typer.Exit:
139
+ raise
140
+ except Exception as e:
141
+ console.print(f"[red]Error: {e}[/red]")
142
+ raise typer.Exit(1)
143
+
144
+
145
+ @storage_app.command("download")
146
+ def download_file(
147
+ path: str = typer.Argument(..., help="File path in storage to download"),
148
+ output: Path = typer.Option(None, "--output", "-o", help="Local output path (defaults to filename)"),
149
+ ):
150
+ """Download a file from your storage bucket."""
151
+ try:
152
+ config.get_client()
153
+
154
+ # Default output to the filename part of the path
155
+ output_path = output or Path(path.split("/")[-1])
156
+
157
+ console.print(f"[dim]Downloading {path}...[/dim]")
158
+
159
+ response = httpx.get(
160
+ f"{config.base_url}/api/v2/external/storage/download/{path}",
161
+ headers={"Authorization": f"Bearer {config.api_key}"},
162
+ timeout=300.0,
163
+ )
164
+
165
+ if response.status_code == 404:
166
+ console.print(f"[red]Error: File not found: {path}[/red]")
167
+ raise typer.Exit(1)
168
+
169
+ if response.status_code != 200:
170
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
171
+ if response.status_code == 401:
172
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
173
+ else:
174
+ console.print(f"[red]{response.content.decode()}[/red]")
175
+ raise typer.Exit(1)
176
+
177
+ with open(output_path, "wb") as f:
178
+ f.write(response.content)
179
+
180
+ console.print(f"[green]Downloaded successfully![/green]")
181
+ console.print(f"[dim]Saved to: {output_path}[/dim]")
182
+ console.print(f"[dim]Size: {format_size(len(response.content))}[/dim]")
183
+
184
+ except typer.Exit:
185
+ raise
186
+ except Exception as e:
187
+ console.print(f"[red]Error: {e}[/red]")
188
+ raise typer.Exit(1)
189
+
190
+
191
+ @storage_app.command("delete")
192
+ def delete_file(
193
+ path: str = typer.Argument(..., help="File path in storage to delete"),
194
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
195
+ ):
196
+ """Delete a file from your storage bucket."""
197
+ try:
198
+ config.get_client()
199
+
200
+ if not force:
201
+ confirm = typer.confirm(f"Delete '{path}'?")
202
+ if not confirm:
203
+ console.print("[dim]Cancelled.[/dim]")
204
+ raise typer.Exit(0)
205
+
206
+ response = httpx.delete(
207
+ f"{config.base_url}/api/v2/external/storage/delete/{path}",
208
+ headers={"Authorization": f"Bearer {config.api_key}"},
209
+ timeout=30.0,
210
+ )
211
+
212
+ if response.status_code == 404:
213
+ console.print(f"[red]Error: File not found: {path}[/red]")
214
+ raise typer.Exit(1)
215
+
216
+ if response.status_code != 200:
217
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
218
+ if response.status_code == 401:
219
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
220
+ else:
221
+ console.print(f"[red]{response.content.decode()}[/red]")
222
+ raise typer.Exit(1)
223
+
224
+ console.print(f"[green]Deleted: {path}[/green]")
225
+
226
+ except typer.Exit:
227
+ raise
228
+ except Exception as e:
229
+ console.print(f"[red]Error: {e}[/red]")
230
+ raise typer.Exit(1)
231
+
232
+
233
+ @storage_app.command("delete-folder")
234
+ def delete_folder(
235
+ prefix: str = typer.Argument(..., help="Folder prefix to delete"),
236
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
237
+ ):
238
+ """Delete all files in a folder (by prefix)."""
239
+ try:
240
+ config.get_client()
241
+
242
+ if not force:
243
+ confirm = typer.confirm(f"Delete all files in '{prefix}/'?")
244
+ if not confirm:
245
+ console.print("[dim]Cancelled.[/dim]")
246
+ raise typer.Exit(0)
247
+
248
+ response = httpx.delete(
249
+ f"{config.base_url}/api/v2/external/storage/delete-folder/{prefix}",
250
+ headers={"Authorization": f"Bearer {config.api_key}"},
251
+ timeout=60.0,
252
+ )
253
+
254
+ if response.status_code == 404:
255
+ console.print(f"[yellow]No files found in folder: {prefix}/[/yellow]")
256
+ raise typer.Exit(0)
257
+
258
+ if response.status_code != 200:
259
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
260
+ if response.status_code == 401:
261
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
262
+ else:
263
+ console.print(f"[red]{response.content.decode()}[/red]")
264
+ raise typer.Exit(1)
265
+
266
+ result = response.json()
267
+ console.print(f"[green]{result.get('message')}[/green]")
268
+
269
+ except typer.Exit:
270
+ raise
271
+ except Exception as e:
272
+ console.print(f"[red]Error: {e}[/red]")
273
+ raise typer.Exit(1)
@@ -12,6 +12,7 @@ from .external.auth.login import auth_app
12
12
  from .external.compute.execution.python import python_app
13
13
  from .external.compute.execution.docker import docker_app
14
14
  from .external.compute.execution.workloads import workloads_app
15
+ from .external.compute.execution.notebook import notebook_app
15
16
 
16
17
  app = typer.Typer(
17
18
  name="lyceum",
@@ -26,6 +27,7 @@ app.add_typer(auth_app, name="auth")
26
27
  app.add_typer(python_app, name="python")
27
28
  app.add_typer(docker_app, name="docker")
28
29
  app.add_typer(workloads_app, name="workloads")
30
+ app.add_typer(notebook_app, name="notebook")
29
31
 
30
32
 
31
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lyceum-cli
3
- Version: 1.0.24
3
+ Version: 1.0.25
4
4
  Summary: Command-line interface for Lyceum Cloud Execution API
5
5
  Home-page: https://lyceum.technology
6
6
  Author: Lyceum Team
@@ -8,6 +8,8 @@ lyceum/external/compute/__init__.py
8
8
  lyceum/external/compute/execution/__init__.py
9
9
  lyceum/external/compute/execution/config.py
10
10
  lyceum/external/compute/execution/docker.py
11
+ lyceum/external/compute/execution/docker_config.py
12
+ lyceum/external/compute/execution/notebook.py
11
13
  lyceum/external/compute/execution/python.py
12
14
  lyceum/external/compute/execution/workloads.py
13
15
  lyceum/external/compute/inference/__init__.py
@@ -15,6 +17,8 @@ lyceum/external/compute/inference/batch.py
15
17
  lyceum/external/compute/inference/chat.py
16
18
  lyceum/external/compute/inference/models.py
17
19
  lyceum/external/general/__init__.py
20
+ lyceum/external/storage/__init__.py
21
+ lyceum/external/storage/files.py
18
22
  lyceum/shared/__init__.py
19
23
  lyceum/shared/config.py
20
24
  lyceum/shared/display.py
@@ -14,7 +14,7 @@ if readme_file.exists():
14
14
 
15
15
  setup(
16
16
  name="lyceum-cli",
17
- version="1.0.24",
17
+ version="1.0.25",
18
18
  description="Command-line interface for Lyceum Cloud Execution API",
19
19
  long_description=long_description,
20
20
  long_description_content_type="text/markdown",
File without changes