lyceum-cli 1.0.23__tar.gz → 1.0.24__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.
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/PKG-INFO +1 -1
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/auth/login.py +1 -1
- lyceum_cli-1.0.24/lyceum/external/compute/execution/config.py +258 -0
- lyceum_cli-1.0.24/lyceum/external/compute/execution/docker.py +272 -0
- lyceum_cli-1.0.24/lyceum/external/compute/execution/python.py +371 -0
- lyceum_cli-1.0.24/lyceum/external/compute/execution/workloads.py +184 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/main.py +4 -11
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/shared/config.py +7 -7
- lyceum_cli-1.0.24/lyceum/shared/imports.py +312 -0
- lyceum_cli-1.0.24/lyceum/shared/streaming.py +229 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cli.egg-info/PKG-INFO +1 -1
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cli.egg-info/SOURCES.txt +18 -4
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cli.egg-info/top_level.txt +1 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/setup.py +1 -1
- lyceum_cli-1.0.24/tests/__init__.py +1 -0
- lyceum_cli-1.0.24/tests/conftest.py +200 -0
- lyceum_cli-1.0.24/tests/unit/__init__.py +1 -0
- lyceum_cli-1.0.24/tests/unit/external/__init__.py +1 -0
- lyceum_cli-1.0.24/tests/unit/external/compute/__init__.py +1 -0
- lyceum_cli-1.0.24/tests/unit/external/compute/execution/__init__.py +1 -0
- lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_data.py +33 -0
- lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_dependency_resolver.py +257 -0
- lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_python_helpers.py +406 -0
- lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_python_run.py +289 -0
- lyceum_cli-1.0.24/tests/unit/shared/__init__.py +1 -0
- lyceum_cli-1.0.24/tests/unit/shared/test_config.py +341 -0
- lyceum_cli-1.0.24/tests/unit/shared/test_streaming.py +259 -0
- lyceum_cli-1.0.23/lyceum/external/compute/execution/python.py +0 -103
- lyceum_cli-1.0.23/lyceum/external/vms/__init__.py +0 -0
- lyceum_cli-1.0.23/lyceum/external/vms/instances.py +0 -303
- lyceum_cli-1.0.23/lyceum/external/vms/management.py +0 -253
- lyceum_cli-1.0.23/lyceum/shared/streaming.py +0 -150
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/auth/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/compute/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/compute/execution/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/compute/inference/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/compute/inference/batch.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/compute/inference/chat.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/compute/inference/models.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/external/general/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/shared/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum/shared/display.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cli.egg-info/dependency_links.txt +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cli.egg-info/entry_points.txt +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cli.egg-info/requires.txt +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cloud_execution_api_client/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cloud_execution_api_client/api/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/lyceum_cloud_execution_api_client/models/__init__.py +0 -0
- {lyceum_cli-1.0.23 → lyceum_cli-1.0.24}/setup.cfg +0 -0
|
@@ -483,4 +483,4 @@ def status():
|
|
|
483
483
|
except Exception as e:
|
|
484
484
|
console.print(f"[red]❌ API connection failed: {e}[/red]")
|
|
485
485
|
else:
|
|
486
|
-
console.print("[red]❌ Not authenticated. Run 'lyceum
|
|
486
|
+
console.print("[red]❌ Not authenticated. Run 'lyceum login' first.[/red]")
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Workspace configuration commands for Python execution"""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata as im
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from ....shared.imports import (
|
|
12
|
+
is_stdlib_module_by_name,
|
|
13
|
+
should_skip_path,
|
|
14
|
+
find_imports_in_file,
|
|
15
|
+
SKIP_DIRS,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
config_app = typer.Typer(name="config", help="Workspace configuration commands")
|
|
21
|
+
|
|
22
|
+
# Known package name mismatches (import name -> pip name)
|
|
23
|
+
PACKAGE_NAME_MAP = {
|
|
24
|
+
"sklearn": "scikit-learn",
|
|
25
|
+
"cv2": "opencv-python",
|
|
26
|
+
"PIL": "Pillow",
|
|
27
|
+
"bs4": "beautifulsoup4",
|
|
28
|
+
"yaml": "PyYAML",
|
|
29
|
+
"Crypto": "pycryptodome",
|
|
30
|
+
"OpenSSL": "pyOpenSSL",
|
|
31
|
+
"cudf": "cudf-cu12",
|
|
32
|
+
"cpuinfo": "py-cpuinfo",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_package_name(module_name: str) -> str:
|
|
37
|
+
"""Convert import name to pip package name."""
|
|
38
|
+
top_level = module_name.split(".")[0]
|
|
39
|
+
return PACKAGE_NAME_MAP.get(top_level, top_level)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_installed_version(package_name: str) -> Optional[str]:
|
|
43
|
+
"""Get installed version of a package."""
|
|
44
|
+
try:
|
|
45
|
+
return im.version(package_name)
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_local_packages(workspace: Path) -> dict[str, Path]:
|
|
51
|
+
"""Find all local Python packages in the workspace recursively."""
|
|
52
|
+
packages = {}
|
|
53
|
+
|
|
54
|
+
for init_file in workspace.rglob("__init__.py"):
|
|
55
|
+
if should_skip_path(init_file):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
package_dir = init_file.parent
|
|
59
|
+
parent_init = package_dir.parent / "__init__.py"
|
|
60
|
+
if parent_init.exists() and not should_skip_path(parent_init):
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
rel_path = package_dir.relative_to(workspace)
|
|
65
|
+
packages[str(rel_path)] = package_dir
|
|
66
|
+
except ValueError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
for item in workspace.iterdir():
|
|
70
|
+
if should_skip_path(item):
|
|
71
|
+
continue
|
|
72
|
+
if item.is_file() and item.suffix == ".py" and item.stem != "__init__":
|
|
73
|
+
packages[item.stem] = item
|
|
74
|
+
|
|
75
|
+
return packages
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def collect_all_python_files(workspace: Path) -> list[Path]:
|
|
79
|
+
"""Collect all Python files in the workspace."""
|
|
80
|
+
py_files = []
|
|
81
|
+
for py_file in workspace.rglob("*.py"):
|
|
82
|
+
if not should_skip_path(py_file):
|
|
83
|
+
py_files.append(py_file)
|
|
84
|
+
return py_files
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_requirements_file(req_file: Path) -> list[str]:
|
|
88
|
+
"""Parse a requirements.txt file into a list of dependencies."""
|
|
89
|
+
deps = []
|
|
90
|
+
try:
|
|
91
|
+
with open(req_file) as f:
|
|
92
|
+
for line in f:
|
|
93
|
+
line = line.strip()
|
|
94
|
+
if line and not line.startswith("#") and not line.startswith("-"):
|
|
95
|
+
deps.append(line)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return deps
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def extract_package_name(dep: str) -> str:
|
|
102
|
+
"""Extract package name from a dependency string."""
|
|
103
|
+
return dep.split("==")[0].split(">=")[0].split("<=")[0].split("<")[0].split(">")[0].split("[")[0].split("~=")[0].lower()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@config_app.command("init")
|
|
107
|
+
def init_config(
|
|
108
|
+
workspace: Path = typer.Argument(".", help="Workspace directory to analyze"),
|
|
109
|
+
requirements: Path | None = typer.Option(
|
|
110
|
+
None, "--requirements", "-r", help="Path to requirements.txt"
|
|
111
|
+
),
|
|
112
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
113
|
+
):
|
|
114
|
+
"""Initialize workspace configuration for Lyceum Cloud execution.
|
|
115
|
+
|
|
116
|
+
Scans the workspace to detect:
|
|
117
|
+
- Local Python packages (for import resolution at runtime)
|
|
118
|
+
- External dependencies (from imports and requirements.txt)
|
|
119
|
+
|
|
120
|
+
Creates .lyceum/config.json with workspace metadata.
|
|
121
|
+
Local file contents are resolved at runtime based on what each script imports.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
lyceum python config init
|
|
125
|
+
lyceum python config init ./my-project
|
|
126
|
+
lyceum python config init -r requirements.txt
|
|
127
|
+
"""
|
|
128
|
+
workspace = Path(workspace).resolve()
|
|
129
|
+
|
|
130
|
+
if not workspace.exists():
|
|
131
|
+
console.print(f"[red]Error: Workspace directory does not exist: {workspace}[/red]")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
config_dir = workspace / ".lyceum"
|
|
135
|
+
config_file = config_dir / "config.json"
|
|
136
|
+
|
|
137
|
+
if config_file.exists() and not force:
|
|
138
|
+
console.print(f"[yellow]Config already exists: {config_file}[/yellow]")
|
|
139
|
+
console.print("[dim]Use --force to overwrite[/dim]")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
console.print(f"[dim]Scanning workspace: {workspace}[/dim]")
|
|
143
|
+
|
|
144
|
+
# Find local packages
|
|
145
|
+
local_packages = find_local_packages(workspace)
|
|
146
|
+
console.print(f"[dim]Found {len(local_packages)} local packages/modules[/dim]")
|
|
147
|
+
|
|
148
|
+
if local_packages:
|
|
149
|
+
for name in sorted(local_packages.keys())[:10]:
|
|
150
|
+
console.print(f"[dim] - {name}[/dim]")
|
|
151
|
+
if len(local_packages) > 10:
|
|
152
|
+
console.print(f"[dim] ... and {len(local_packages) - 10} more[/dim]")
|
|
153
|
+
|
|
154
|
+
# Collect all Python files for import scanning
|
|
155
|
+
py_files = collect_all_python_files(workspace)
|
|
156
|
+
console.print(f"[dim]Found {len(py_files)} Python files[/dim]")
|
|
157
|
+
|
|
158
|
+
# Scan all files for imports to detect external dependencies
|
|
159
|
+
all_imports = set()
|
|
160
|
+
for py_file in py_files:
|
|
161
|
+
all_imports.update(find_imports_in_file(py_file))
|
|
162
|
+
|
|
163
|
+
# Filter to external dependencies
|
|
164
|
+
local_package_names = {Path(p).parts[0] if "/" in p else p for p in local_packages.keys()}
|
|
165
|
+
external_deps = {
|
|
166
|
+
imp for imp in all_imports
|
|
167
|
+
if not is_stdlib_module_by_name(imp) and imp not in local_package_names
|
|
168
|
+
}
|
|
169
|
+
console.print(f"[dim]Found {len(external_deps)} external dependencies from imports[/dim]")
|
|
170
|
+
|
|
171
|
+
# Parse requirements.txt
|
|
172
|
+
requirements_deps = []
|
|
173
|
+
req_file = requirements or workspace / "requirements.txt"
|
|
174
|
+
if req_file.exists():
|
|
175
|
+
console.print(f"[dim]Reading requirements from: {req_file}[/dim]")
|
|
176
|
+
requirements_deps = parse_requirements_file(req_file)
|
|
177
|
+
|
|
178
|
+
# Build merged dependencies with versions
|
|
179
|
+
merged_deps = []
|
|
180
|
+
req_names = {extract_package_name(dep) for dep in requirements_deps}
|
|
181
|
+
|
|
182
|
+
for dep in requirements_deps:
|
|
183
|
+
merged_deps.append(dep)
|
|
184
|
+
|
|
185
|
+
for imp in sorted(external_deps):
|
|
186
|
+
pkg_name = get_package_name(imp)
|
|
187
|
+
if pkg_name.lower() not in req_names:
|
|
188
|
+
version = get_installed_version(pkg_name)
|
|
189
|
+
if version:
|
|
190
|
+
merged_deps.append(f"{pkg_name}=={version}")
|
|
191
|
+
else:
|
|
192
|
+
merged_deps.append(pkg_name)
|
|
193
|
+
|
|
194
|
+
# Build config (no local_imports - resolved at runtime)
|
|
195
|
+
config_data = {
|
|
196
|
+
"workspace": str(workspace),
|
|
197
|
+
"local_packages": {name: str(path) for name, path in sorted(local_packages.items())},
|
|
198
|
+
"dependencies": {
|
|
199
|
+
"from_imports": sorted(external_deps),
|
|
200
|
+
"from_requirements": requirements_deps,
|
|
201
|
+
"merged": sorted(set(merged_deps), key=str.lower),
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Write config
|
|
206
|
+
config_dir.mkdir(exist_ok=True)
|
|
207
|
+
with open(config_file, "w") as f:
|
|
208
|
+
json.dump(config_data, f, indent=2)
|
|
209
|
+
|
|
210
|
+
console.print(f"[green]Created config: {config_file}[/green]")
|
|
211
|
+
console.print(f"[dim] - {len(local_packages)} local packages[/dim]")
|
|
212
|
+
console.print(f"[dim] - {len(config_data['dependencies']['merged'])} dependencies[/dim]")
|
|
213
|
+
console.print("[dim] - Local imports resolved at runtime per-file[/dim]")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@config_app.command("show")
|
|
217
|
+
def show_config(
|
|
218
|
+
workspace: Path = typer.Argument(".", help="Workspace directory"),
|
|
219
|
+
):
|
|
220
|
+
"""Show current workspace configuration."""
|
|
221
|
+
workspace = Path(workspace).resolve()
|
|
222
|
+
config_file = workspace / ".lyceum" / "config.json"
|
|
223
|
+
|
|
224
|
+
if not config_file.exists():
|
|
225
|
+
console.print(f"[yellow]No config found at: {config_file}[/yellow]")
|
|
226
|
+
console.print("[dim]Run 'lyceum python config init' to create one[/dim]")
|
|
227
|
+
raise typer.Exit(1)
|
|
228
|
+
|
|
229
|
+
with open(config_file) as f:
|
|
230
|
+
config_data = json.load(f)
|
|
231
|
+
|
|
232
|
+
console.print(f"[bold]Workspace:[/bold] {config_data.get('workspace', 'unknown')}")
|
|
233
|
+
|
|
234
|
+
local_packages = config_data.get("local_packages", {})
|
|
235
|
+
console.print(f"\n[bold]Local packages ({len(local_packages)}):[/bold]")
|
|
236
|
+
for pkg in list(local_packages.keys())[:15]:
|
|
237
|
+
console.print(f" - {pkg}")
|
|
238
|
+
if len(local_packages) > 15:
|
|
239
|
+
console.print(f" ... and {len(local_packages) - 15} more")
|
|
240
|
+
|
|
241
|
+
deps = config_data.get("dependencies", {})
|
|
242
|
+
merged = deps.get("merged", [])
|
|
243
|
+
console.print(f"\n[bold]Dependencies ({len(merged)}):[/bold]")
|
|
244
|
+
for dep in merged[:20]:
|
|
245
|
+
console.print(f" - {dep}")
|
|
246
|
+
if len(merged) > 20:
|
|
247
|
+
console.print(f" ... and {len(merged) - 20} more")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@config_app.command("refresh")
|
|
251
|
+
def refresh_config(
|
|
252
|
+
workspace: Path = typer.Argument(".", help="Workspace directory"),
|
|
253
|
+
requirements: Path | None = typer.Option(
|
|
254
|
+
None, "--requirements", "-r", help="Path to requirements.txt"
|
|
255
|
+
),
|
|
256
|
+
):
|
|
257
|
+
"""Refresh workspace configuration (re-scans the workspace)."""
|
|
258
|
+
init_config(workspace=workspace, requirements=requirements, force=True)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker execution commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shlex
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from ....shared.config import config
|
|
14
|
+
from ....shared.streaming import StatusLine, stream_execution_output
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
docker_app = typer.Typer(name="docker", help="Docker execution commands")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@docker_app.command("run")
|
|
23
|
+
def run_docker(
|
|
24
|
+
image: str = typer.Argument(..., help="Docker image to run"),
|
|
25
|
+
machine_type: str = typer.Option(
|
|
26
|
+
"cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"
|
|
27
|
+
),
|
|
28
|
+
timeout: int = typer.Option(
|
|
29
|
+
300, "--timeout", "-t", help="Execution timeout in seconds"
|
|
30
|
+
),
|
|
31
|
+
file_name: Optional[str] = typer.Option(
|
|
32
|
+
None, "--file-name", "-f", help="Name for the execution"
|
|
33
|
+
),
|
|
34
|
+
command: Optional[str] = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--command",
|
|
37
|
+
"-c",
|
|
38
|
+
help="Command to run in container (e.g., 'python app.py')",
|
|
39
|
+
),
|
|
40
|
+
env: Optional[list[str]] = typer.Option(
|
|
41
|
+
None, "--env", "-e", help="Environment variables (e.g., KEY=value)"
|
|
42
|
+
),
|
|
43
|
+
detach: bool = typer.Option(
|
|
44
|
+
False, "--detach", "-d", help="Run container in background and print execution ID"
|
|
45
|
+
),
|
|
46
|
+
callback_url: Optional[str] = typer.Option(
|
|
47
|
+
None, "--callback", help="Webhook URL for completion notification"
|
|
48
|
+
),
|
|
49
|
+
registry_creds: Optional[str] = typer.Option(
|
|
50
|
+
None, "--registry-creds", help="Docker registry credentials as JSON string"
|
|
51
|
+
),
|
|
52
|
+
registry_type: Optional[str] = typer.Option(
|
|
53
|
+
None, "--registry-type", help="Registry credential type: basic, aws, etc."
|
|
54
|
+
),
|
|
55
|
+
):
|
|
56
|
+
"""Execute a Docker container on Lyceum Cloud.
|
|
57
|
+
|
|
58
|
+
By default, streams container output in real-time.
|
|
59
|
+
Use --detach to run in background and return immediately.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
lyceum docker run python:3.11 -c "python -c 'print(1+1)'"
|
|
63
|
+
lyceum docker run myapp:latest -e "DEBUG=true"
|
|
64
|
+
lyceum docker run nvidia/cuda:12.0-base -m a100 -d
|
|
65
|
+
"""
|
|
66
|
+
status = StatusLine()
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
config.get_client()
|
|
70
|
+
|
|
71
|
+
status.start()
|
|
72
|
+
status.update("Validating configuration...")
|
|
73
|
+
|
|
74
|
+
# Parse environment variables
|
|
75
|
+
docker_env = {}
|
|
76
|
+
if env:
|
|
77
|
+
for env_var in env:
|
|
78
|
+
if "=" in env_var:
|
|
79
|
+
key, value = env_var.split("=", 1)
|
|
80
|
+
docker_env[key] = value
|
|
81
|
+
else:
|
|
82
|
+
status.stop()
|
|
83
|
+
console.print(
|
|
84
|
+
f"[yellow]Warning: Ignoring invalid env var format: {env_var}[/yellow]"
|
|
85
|
+
)
|
|
86
|
+
status.start()
|
|
87
|
+
|
|
88
|
+
# Parse registry credentials
|
|
89
|
+
registry_credentials = None
|
|
90
|
+
if registry_creds:
|
|
91
|
+
try:
|
|
92
|
+
registry_credentials = json.loads(registry_creds)
|
|
93
|
+
except json.JSONDecodeError:
|
|
94
|
+
status.stop()
|
|
95
|
+
console.print(
|
|
96
|
+
"[red]Error: Invalid JSON format for registry credentials[/red]"
|
|
97
|
+
)
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
# Validate registry credentials and type
|
|
101
|
+
if (registry_creds and not registry_type) or (
|
|
102
|
+
registry_type and not registry_creds
|
|
103
|
+
):
|
|
104
|
+
status.stop()
|
|
105
|
+
console.print(
|
|
106
|
+
"[red]Error: Both --registry-creds and --registry-type must be provided together[/red]"
|
|
107
|
+
)
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
|
|
110
|
+
status.update(f"Starting container {image}...")
|
|
111
|
+
|
|
112
|
+
# Build request for v2 image API
|
|
113
|
+
image_request = {
|
|
114
|
+
"docker_image_ref": image,
|
|
115
|
+
"timeout": timeout,
|
|
116
|
+
"execution_type": machine_type,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if command:
|
|
120
|
+
image_request["docker_run_cmd"] = shlex.split(command)
|
|
121
|
+
if file_name:
|
|
122
|
+
image_request["file_name"] = file_name
|
|
123
|
+
if env:
|
|
124
|
+
# Join with newlines - the execlet parses env vars by splitting on \n
|
|
125
|
+
image_request["docker_run_env"] = "\n".join(env)
|
|
126
|
+
|
|
127
|
+
# Handle registry credentials
|
|
128
|
+
if registry_type and registry_credentials:
|
|
129
|
+
image_request["docker_registry_credential_type"] = registry_type
|
|
130
|
+
|
|
131
|
+
if registry_type == "aws":
|
|
132
|
+
creds = registry_credentials
|
|
133
|
+
image_request.update(
|
|
134
|
+
{
|
|
135
|
+
"aws_access_key_id": creds.get("aws_access_key_id"),
|
|
136
|
+
"aws_secret_access_key": creds.get("aws_secret_access_key"),
|
|
137
|
+
"aws_session_token": creds.get("aws_session_token"),
|
|
138
|
+
"aws_region": creds.get("region", "us-east-1"),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
elif registry_type == "basic":
|
|
142
|
+
creds = registry_credentials
|
|
143
|
+
image_request.update(
|
|
144
|
+
{
|
|
145
|
+
"docker_username": creds.get("username"),
|
|
146
|
+
"docker_password": creds.get("password"),
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Call the v2 image endpoint
|
|
151
|
+
status.update("Submitting execution...")
|
|
152
|
+
|
|
153
|
+
response = httpx.post(
|
|
154
|
+
f"{config.base_url}/api/v2/external/execution/image/start",
|
|
155
|
+
json=image_request,
|
|
156
|
+
headers={"Authorization": f"Bearer {config.api_key}"},
|
|
157
|
+
timeout=30.0,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if response.status_code != 200:
|
|
161
|
+
status.stop()
|
|
162
|
+
console.print(f"[red]Error: HTTP {response.status_code}[/red]")
|
|
163
|
+
if response.status_code == 401:
|
|
164
|
+
console.print(
|
|
165
|
+
"[red]Authentication failed. Your session may have expired.[/red]"
|
|
166
|
+
)
|
|
167
|
+
console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
|
|
168
|
+
elif response.content:
|
|
169
|
+
console.print(f"[red]{response.content.decode()}[/red]")
|
|
170
|
+
raise typer.Exit(1)
|
|
171
|
+
|
|
172
|
+
result = response.json()
|
|
173
|
+
execution_id = result.get("execution_id")
|
|
174
|
+
streaming_url = result.get("streaming_url")
|
|
175
|
+
|
|
176
|
+
if detach:
|
|
177
|
+
# Detached mode - just return the execution info
|
|
178
|
+
status.stop()
|
|
179
|
+
console.print(f"[dim]Image: {image}[/dim]")
|
|
180
|
+
console.print(f"[dim]Machine: {machine_type}[/dim]")
|
|
181
|
+
console.print(f"[dim]Execution ID: {execution_id}[/dim]")
|
|
182
|
+
console.print("")
|
|
183
|
+
console.print(f"[dim]To stream logs:[/dim] lyceum docker logs {execution_id}")
|
|
184
|
+
else:
|
|
185
|
+
# Default: stream output in real-time (like docker run)
|
|
186
|
+
success = stream_execution_output(execution_id, streaming_url, status)
|
|
187
|
+
|
|
188
|
+
# Show execution ID at the end
|
|
189
|
+
console.print(f"[dim]Execution ID: {execution_id}[/dim]")
|
|
190
|
+
|
|
191
|
+
if not success:
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
except typer.Exit:
|
|
195
|
+
status.stop()
|
|
196
|
+
raise
|
|
197
|
+
except Exception as e:
|
|
198
|
+
status.stop()
|
|
199
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@docker_app.command("logs")
|
|
204
|
+
def docker_logs(
|
|
205
|
+
execution_id: str = typer.Argument(..., help="Execution ID to stream logs from"),
|
|
206
|
+
):
|
|
207
|
+
"""Stream logs from a running or completed Docker execution.
|
|
208
|
+
|
|
209
|
+
Examples:
|
|
210
|
+
lyceum docker logs 9d73319c-6f1c-4b4c-90e4-044244353ce4
|
|
211
|
+
"""
|
|
212
|
+
status = StatusLine()
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
config.get_client()
|
|
216
|
+
|
|
217
|
+
status.start()
|
|
218
|
+
status.update("Connecting to execution...")
|
|
219
|
+
|
|
220
|
+
# Pass None for streaming_url to use the default v2 endpoint
|
|
221
|
+
success = stream_execution_output(execution_id, None, status)
|
|
222
|
+
|
|
223
|
+
console.print(f"[dim]Execution ID: {execution_id}[/dim]")
|
|
224
|
+
|
|
225
|
+
if not success:
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
except typer.Exit:
|
|
229
|
+
status.stop()
|
|
230
|
+
raise
|
|
231
|
+
except Exception as e:
|
|
232
|
+
status.stop()
|
|
233
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@docker_app.command("registry-examples")
|
|
238
|
+
def show_registry_examples():
|
|
239
|
+
"""Show examples of Docker registry credential formats"""
|
|
240
|
+
console.print("[bold cyan]Docker Registry Credential Examples[/bold cyan]\n")
|
|
241
|
+
|
|
242
|
+
console.print("[bold]1. Docker Hub (basic)[/bold]")
|
|
243
|
+
console.print("Type: [green]basic[/green]")
|
|
244
|
+
console.print(
|
|
245
|
+
'Credentials: [yellow]\'{"username": "myuser", "password": "mypassword"}\'[/yellow]\n'
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
console.print("[bold]2. AWS ECR (aws)[/bold]")
|
|
249
|
+
console.print("Type: [green]aws[/green]")
|
|
250
|
+
console.print(
|
|
251
|
+
'Credentials: [yellow]\'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", "aws_secret_access_key": "wJalrX...", "session_token": "optional..."}\'[/yellow]\n'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
console.print("[bold]3. Private Registry (basic)[/bold]")
|
|
255
|
+
console.print("Type: [green]basic[/green]")
|
|
256
|
+
console.print(
|
|
257
|
+
'Credentials: [yellow]\'{"username": "admin", "password": "secret"}\'[/yellow]\n'
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
console.print("[bold]Example Commands:[/bold]")
|
|
261
|
+
console.print("# Docker Hub:")
|
|
262
|
+
console.print(
|
|
263
|
+
"[dim]lyceum docker run myuser/myapp:latest --registry-type basic --registry-creds '{\"username\": \"myuser\", \"password\": \"mytoken\"}'[/dim]"
|
|
264
|
+
)
|
|
265
|
+
console.print("\n# AWS ECR:")
|
|
266
|
+
console.print(
|
|
267
|
+
"[dim]lyceum docker run 123456789012.dkr.ecr.us-west-2.amazonaws.com/myapp:latest --registry-type aws --registry-creds '{\"region\": \"us-west-2\", \"aws_access_key_id\": \"AKIAI...\", \"aws_secret_access_key\": \"wJalrX...\"}'[/dim]"
|
|
268
|
+
)
|
|
269
|
+
console.print("\n# Private Registry:")
|
|
270
|
+
console.print(
|
|
271
|
+
"[dim]lyceum docker run myregistry.com/myapp:latest --registry-type basic --registry-creds '{\"username\": \"admin\", \"password\": \"secret\"}'[/dim]"
|
|
272
|
+
)
|