lyceum-cli 1.0.24__py3-none-any.whl → 1.0.25__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.
- lyceum/external/compute/execution/docker_config.py +123 -0
- lyceum/external/compute/execution/notebook.py +242 -0
- lyceum/external/storage/__init__.py +0 -0
- lyceum/external/storage/files.py +273 -0
- lyceum/main.py +2 -0
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.25.dist-info}/METADATA +1 -1
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.25.dist-info}/RECORD +10 -6
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.25.dist-info}/WHEEL +0 -0
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.25.dist-info}/entry_points.txt +0 -0
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.25.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
lyceum/main.py
CHANGED
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
lyceum/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
-
lyceum/main.py,sha256=
|
|
2
|
+
lyceum/main.py,sha256=jE4P4SvHTeguLKL9wRBfOBxGHzDKxH_PcM46ouY-xnY,921
|
|
3
3
|
lyceum/external/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
4
4
|
lyceum/external/auth/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
5
|
lyceum/external/auth/login.py,sha256=T-V6pzi3s0ZynpUUeLnN2y-cxJI4msj5z26S3_wJ1AE,23526
|
|
@@ -7,6 +7,8 @@ lyceum/external/compute/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YB
|
|
|
7
7
|
lyceum/external/compute/execution/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
8
|
lyceum/external/compute/execution/config.py,sha256=6JJgLJnDPTwevEaNdB1nEICih_qbBmws5u5_S9gj7k0,8866
|
|
9
9
|
lyceum/external/compute/execution/docker.py,sha256=lATtJzsmsu9iCUH-ynXZsfrIg-_aCkYE5r2l3DolisQ,9603
|
|
10
|
+
lyceum/external/compute/execution/docker_config.py,sha256=tIiEVlp3yYZxLqhRsBqri7wV9_K0fPm6bvTRhRnKEd4,4071
|
|
11
|
+
lyceum/external/compute/execution/notebook.py,sha256=fbN546-j1Cw88j7jbDKkup8J1xaws0eBYZJgt1NTJVg,7544
|
|
10
12
|
lyceum/external/compute/execution/python.py,sha256=1ekNuF8_j-YG0-oNFP3C6Jjyw7IUlIngvL06C1usyu8,12864
|
|
11
13
|
lyceum/external/compute/execution/workloads.py,sha256=4fsRWbYGmsQMGPPIN1jUG8cG5NPG9yV26ANJ-DtaXqc,5844
|
|
12
14
|
lyceum/external/compute/inference/__init__.py,sha256=4YLoUKDEzitexynJv_Q5O0w1lty8CJ6uyRxuc1LiaBw,89
|
|
@@ -14,6 +16,8 @@ lyceum/external/compute/inference/batch.py,sha256=HWwS6XHDZOG7QftNTNGvzao8Y6DzkE
|
|
|
14
16
|
lyceum/external/compute/inference/chat.py,sha256=hITj_UGLaxCJQskU-YbeaEerM5Xt_eJpEsYrTJoUpk4,8485
|
|
15
17
|
lyceum/external/compute/inference/models.py,sha256=BkCEdvyliezGOUulj557e-Eoif0_HKR3CxqpEhdAZaA,10339
|
|
16
18
|
lyceum/external/general/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
19
|
+
lyceum/external/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
lyceum/external/storage/files.py,sha256=WdSp6t90jLzChOkg2uI4rZ5pq6bmHDCSo7CDUDVXWyk,9311
|
|
17
21
|
lyceum/shared/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
18
22
|
lyceum/shared/config.py,sha256=mZXbdwKxbkRA6RCnTPruaNWtLOaheYO4MnCxXTFNLu8,5548
|
|
19
23
|
lyceum/shared/display.py,sha256=n9QQy-JVvoAgzss9D9cGfdw7GUY6t9qTYWr81v9q1fk,4855
|
|
@@ -35,8 +39,8 @@ tests/unit/external/compute/execution/test_python_run.py,sha256=fKuWyTVshC3iGvoW
|
|
|
35
39
|
tests/unit/shared/__init__.py,sha256=ouZueovCsCdgfuKoMP1Uh69E3fmn0U8ltllHWhXhpgg,39
|
|
36
40
|
tests/unit/shared/test_config.py,sha256=rokbpv_S9J-aUtg41eddn0ZuVWyTYZNXO9VtCZdXstU,11315
|
|
37
41
|
tests/unit/shared/test_streaming.py,sha256=sFjkla5SvaWWPi42lQ6vHSTor_htLMMbBttnCyGsB98,8706
|
|
38
|
-
lyceum_cli-1.0.
|
|
39
|
-
lyceum_cli-1.0.
|
|
40
|
-
lyceum_cli-1.0.
|
|
41
|
-
lyceum_cli-1.0.
|
|
42
|
-
lyceum_cli-1.0.
|
|
42
|
+
lyceum_cli-1.0.25.dist-info/METADATA,sha256=mC4h00m-5ihgHqd9gdNoIpET5Ok0EQv5cAqLKaVLc2c,1482
|
|
43
|
+
lyceum_cli-1.0.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
44
|
+
lyceum_cli-1.0.25.dist-info/entry_points.txt,sha256=Oq-9wDkxVd6MHgNiUTYwXI9SGhvR3VkD7Mvk0xhiUZo,43
|
|
45
|
+
lyceum_cli-1.0.25.dist-info/top_level.txt,sha256=-546wowhLvi8w6Gef9vwO0T1i0ME2PanTrgC0YX29y4,47
|
|
46
|
+
lyceum_cli-1.0.25.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|