lyceum-cli 1.1.7__tar.gz → 1.2.2__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 (52) hide show
  1. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/PKG-INFO +7 -8
  2. lyceum_cli-1.2.2/lyceum/_version.py +10 -0
  3. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/auth/keys.py +1 -0
  4. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/auth/login.py +44 -27
  5. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/config.py +15 -14
  6. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/docker.py +30 -54
  7. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/docker_compose.py +28 -53
  8. lyceum_cli-1.2.2/lyceum/external/compute/execution/gpu_selection.py +59 -0
  9. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/jobs.py +1 -1
  10. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/notebook.py +11 -21
  11. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/python.py +20 -32
  12. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/inference/chat.py +19 -5
  13. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/inference/claude_code.py +6 -3
  14. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/inference/infer.py +133 -34
  15. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/orgs/management.py +25 -19
  16. lyceum_cli-1.2.2/lyceum/external/storage/block.py +114 -0
  17. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/storage/files.py +24 -11
  18. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/vms/management.py +189 -17
  19. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/main.py +2 -3
  20. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/imports.py +174 -46
  21. lyceum_cli-1.2.2/lyceum/shared/orgs.py +132 -0
  22. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/run_options.py +3 -9
  23. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/streaming.py +20 -12
  24. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/ui.py +10 -1
  25. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum_cli.egg-info/PKG-INFO +7 -8
  26. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum_cli.egg-info/SOURCES.txt +7 -2
  27. lyceum_cli-1.2.2/lyceum_cli.egg-info/requires.txt +5 -0
  28. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/pyproject.toml +12 -8
  29. lyceum_cli-1.2.2/tests/test_block_storage.py +399 -0
  30. lyceum_cli-1.2.2/tests/test_dedicated_deploy.py +105 -0
  31. lyceum_cli-1.2.2/tests/test_orgs.py +104 -0
  32. lyceum_cli-1.1.7/lyceum/__init__.py +0 -1
  33. lyceum_cli-1.1.7/lyceum/external/compute/execution/gpu_selection.py +0 -888
  34. lyceum_cli-1.1.7/lyceum_cli.egg-info/requires.txt +0 -5
  35. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/README.md +0 -0
  36. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/__main__.py +0 -0
  37. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/__init__.py +0 -0
  38. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/auth/__init__.py +0 -0
  39. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/auth/environment.py +0 -0
  40. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/__init__.py +0 -0
  41. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/execution/__init__.py +0 -0
  42. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/compute/inference/__init__.py +0 -0
  43. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/orgs/__init__.py +0 -0
  44. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/storage/__init__.py +0 -0
  45. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/external/vms/__init__.py +0 -0
  46. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/__init__.py +0 -0
  47. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/config.py +0 -0
  48. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum/shared/http.py +0 -0
  49. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum_cli.egg-info/dependency_links.txt +0 -0
  50. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum_cli.egg-info/entry_points.txt +0 -0
  51. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/lyceum_cli.egg-info/top_level.txt +0 -0
  52. {lyceum_cli-1.1.7 → lyceum_cli-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lyceum-cli
3
- Version: 1.1.7
3
+ Version: 1.2.2
4
4
  Summary: Command-line interface for Lyceum Cloud
5
5
  Author-email: Lyceum <support@lyceum.technology>
6
6
  License: MIT
@@ -13,20 +13,19 @@ Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
20
19
  Classifier: Topic :: Software Development
21
20
  Classifier: Topic :: System :: Distributed Computing
22
21
  Classifier: Topic :: Utilities
23
- Requires-Python: >=3.10
22
+ Requires-Python: >=3.11
24
23
  Description-Content-Type: text/markdown
25
- Requires-Dist: typer>=0.12.0
26
- Requires-Dist: rich>=13.0.0
27
- Requires-Dist: httpx>=0.24.0
28
- Requires-Dist: PyJWT>=2.8.0
29
- Requires-Dist: boto3>=1.35.0
24
+ Requires-Dist: typer==0.24.1
25
+ Requires-Dist: rich==14.3.3
26
+ Requires-Dist: httpx==0.28.1
27
+ Requires-Dist: PyJWT==2.13.0
28
+ Requires-Dist: boto3==1.42.62
30
29
 
31
30
  # Lyceum CLI
32
31
 
@@ -0,0 +1,10 @@
1
+ """The version lyceum-cli publishes under.
2
+
3
+ NOTE: This package deliberately has no __init__.py. 'lyceum' is a namespace
4
+ shared with lyceum-shared-models and lyceum-neph. An __init__.py here would
5
+ shadow theirs and break 'lyceum.shared_models' and 'lyceum.neph'. That is why
6
+ __version__ lives in this module. A future refactor could move these modules
7
+ under lyceum/cli/, claiming one subpackage the way the other two do.
8
+ """
9
+
10
+ __version__ = "1.2.2"
@@ -62,6 +62,7 @@ def create_key(
62
62
  console.print(f"[bold yellow]{res['api_key']}[/bold yellow]")
63
63
  console.print()
64
64
  console.print("[dim]This is the only time the key will be displayed. Copy it now.[/dim]")
65
+ console.print("[dim]This key follows --org and the active org; without either it acts as your personal org.[/dim]")
65
66
 
66
67
 
67
68
  @keys_app.command("revoke")
@@ -7,6 +7,7 @@ import threading
7
7
  import time
8
8
  import webbrowser
9
9
  from http.server import BaseHTTPRequestHandler, HTTPServer
10
+ from typing import Any
10
11
  from urllib.parse import parse_qs, urlparse
11
12
 
12
13
  import httpx
@@ -14,6 +15,7 @@ import typer
14
15
  from rich.console import Console
15
16
 
16
17
  from ...shared.config import config
18
+ from ...shared.orgs import explain, resolve_org_context
17
19
  from .keys import keys_app
18
20
 
19
21
  console = Console()
@@ -22,14 +24,14 @@ auth_app = typer.Typer(name="auth", help="Authentication commands")
22
24
  auth_app.add_typer(keys_app, name="keys")
23
25
 
24
26
  # Global state for the OAuth callback server
25
- callback_result = {"token": None, "error": None, "received": False}
27
+ callback_result: dict[str, Any] = {"token": None, "error": None, "received": False}
26
28
 
27
29
  # (svg path, tailwind color) per page kind
28
30
  _PAGE_ICONS = {
29
31
  "success": ("M5 13l4 4L19 7", "green"),
30
32
  "error": ("M6 18L18 6M6 6l12 12", "red"),
31
33
  "warning": (
32
- "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.268 16c-.77.833.192 2.5 1.732 2.5z",
34
+ "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.268 16c-.77.833.192 2.5 1.732 2.5z", # noqa: E501
33
35
  "yellow",
34
36
  ),
35
37
  }
@@ -94,7 +96,9 @@ class CallbackHandler(BaseHTTPRequestHandler):
94
96
  self._send_page(
95
97
  404,
96
98
  _render_callback_page(
97
- "warning", "Page Not Found", "Page Not Found",
99
+ "warning",
100
+ "Page Not Found",
101
+ "Page Not Found",
98
102
  "<p>The requested page could not be found.</p>",
99
103
  ),
100
104
  )
@@ -111,7 +115,9 @@ class CallbackHandler(BaseHTTPRequestHandler):
111
115
  self._send_page(
112
116
  200,
113
117
  _render_callback_page(
114
- "success", "Authentication Success", "Authentication Successful!",
118
+ "success",
119
+ "Authentication Success",
120
+ "Authentication Successful!",
115
121
  "<p class='text-lg'>Welcome to Lyceum</p>"
116
122
  "<p>You can now close this browser tab and return to the CLI.</p>",
117
123
  auto_close=True,
@@ -125,8 +131,10 @@ class CallbackHandler(BaseHTTPRequestHandler):
125
131
  self._send_page(
126
132
  400,
127
133
  _render_callback_page(
128
- "error", "Authentication Error", "Authentication Failed",
129
- f"<div class='bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md'><p class='text-sm'>{html.escape(error)}</p></div>"
134
+ "error",
135
+ "Authentication Error",
136
+ "Authentication Failed",
137
+ f"<div class='bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md'><p class='text-sm'>{html.escape(error)}</p></div>" # noqa: E501
130
138
  "<p>Please try again or contact support if the issue persists.</p>",
131
139
  ),
132
140
  )
@@ -137,7 +145,9 @@ class CallbackHandler(BaseHTTPRequestHandler):
137
145
  self._send_page(
138
146
  400,
139
147
  _render_callback_page(
140
- "warning", "Invalid Parameters", "Invalid Parameters",
148
+ "warning",
149
+ "Invalid Parameters",
150
+ "Invalid Parameters",
141
151
  "<p>The authentication callback received invalid parameters.</p>",
142
152
  ),
143
153
  )
@@ -148,8 +158,10 @@ class CallbackHandler(BaseHTTPRequestHandler):
148
158
  self._send_page(
149
159
  500,
150
160
  _render_callback_page(
151
- "error", "Server Error", "Server Error",
152
- f"<div class='bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md'><p class='text-sm'>{html.escape(str(e))}</p></div>",
161
+ "error",
162
+ "Server Error",
163
+ "Server Error",
164
+ f"<div class='bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md'><p class='text-sm'>{html.escape(str(e))}</p></div>", # noqa: E501,
153
165
  ),
154
166
  )
155
167
 
@@ -176,17 +188,16 @@ def _auto_pin_active_org(client):
176
188
  return
177
189
 
178
190
  if not sys.stdin.isatty():
179
- console.print(
180
- "[dim]You belong to multiple orgs. Pick one with 'lyceum org use <slug>'.[/dim]"
181
- )
191
+ console.print("[dim]You belong to multiple orgs. Pick one with 'lyceum org use <slug>'.[/dim]")
182
192
  return
183
193
 
184
194
  console.print()
185
195
  console.print("[bold]You belong to multiple orgs. Pick one to make active:[/bold]")
186
196
  for i, org in enumerate(orgs, start=1):
187
197
  role = org.get("role", "")
188
- console.print(f" [cyan]{i}[/cyan]. {org['name']} ([dim]{org['slug']}[/dim]) — {role}")
189
- console.print(" [cyan]0[/cyan]. [dim]Skip — fall back to default org[/dim]")
198
+ kind = "personal" if org.get("is_default") else "team"
199
+ console.print(f" [cyan]{i}[/cyan]. {org['name']} ([dim]{org['slug']}[/dim]) — {kind}, {role}")
200
+ console.print(" [cyan]0[/cyan]. [dim]Skip — use your personal org[/dim]")
190
201
 
191
202
  while True:
192
203
  raw = typer.prompt("Selection", default="0")
@@ -207,10 +218,20 @@ def _auto_pin_active_org(client):
207
218
  console.print(f"[yellow]Pick 0–{len(orgs)}.[/yellow]")
208
219
 
209
220
 
221
+ def _pin_key_org():
222
+ """An org key acts as its own org; make the active org say so."""
223
+ ctx = resolve_org_context()
224
+ if ctx.is_key_scoped and ctx.slug:
225
+ config.active_org_slug = ctx.slug
226
+ config.save()
227
+ for line in explain(ctx):
228
+ console.print(line)
229
+
230
+
210
231
  def get_available_port():
211
232
  """Find an available port for the callback server"""
212
233
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
213
- s.bind(('', 0))
234
+ s.bind(("", 0))
214
235
  s.listen(1)
215
236
  port = s.getsockname()[1]
216
237
  return port
@@ -220,12 +241,8 @@ def get_available_port():
220
241
  def login(
221
242
  base_url: str | None = typer.Option(None, "--url", help="API base URL (for development)"),
222
243
  dashboard_url: str | None = typer.Option(None, "--dashboard", help="Dashboard URL (for development)"),
223
- manual: bool = typer.Option(
224
- False, "--manual", help="Use manual API key login instead of browser"
225
- ),
226
- api_key: str | None = typer.Option(
227
- None, "--api-key", "-k", help="API key for manual login"
228
- ),
244
+ manual: bool = typer.Option(False, "--manual", help="Use manual API key login instead of browser"),
245
+ api_key: str | None = typer.Option(None, "--api-key", "-k", help="API key for manual login"),
229
246
  ):
230
247
  """Login to Lyceum via browser authentication"""
231
248
  global callback_result
@@ -255,6 +272,7 @@ def login(
255
272
  else:
256
273
  console.print(f"[red]Authentication failed: HTTP {response.status_code}[/red]")
257
274
  raise typer.Exit(1)
275
+ _pin_key_org()
258
276
  except typer.Exit:
259
277
  raise
260
278
  except Exception as e:
@@ -275,7 +293,7 @@ def login(
275
293
 
276
294
  # Start callback server
277
295
  callback_port = get_available_port()
278
- callback_server = HTTPServer(('localhost', callback_port), CallbackHandler)
296
+ callback_server = HTTPServer(("localhost", callback_port), CallbackHandler)
279
297
 
280
298
  console.print(f"[dim]Starting callback server on port {callback_port}...[/dim]")
281
299
 
@@ -366,6 +384,7 @@ def login(
366
384
  def logout():
367
385
  """Logout and remove stored credentials"""
368
386
  from ...shared.config import CONFIG_FILE
387
+
369
388
  if CONFIG_FILE.exists():
370
389
  CONFIG_FILE.unlink()
371
390
  console.print("[green]Logged out successfully![/green]")
@@ -375,6 +394,7 @@ def logout():
375
394
  def status():
376
395
  """Show current configuration and authentication status"""
377
396
  from ...shared.config import CONFIG_FILE
397
+
378
398
  console.print(f"[dim]Config file: {CONFIG_FILE}[/dim]")
379
399
  console.print(f"[dim]Base URL: {config.base_url}[/dim]")
380
400
 
@@ -385,11 +405,8 @@ def status():
385
405
  console.print("[green]Authenticated[/green]")
386
406
  console.print(f"[dim]API Key: {config.api_key[:8]}...[/dim]")
387
407
 
388
- active_org = config.get_active_org_slug()
389
- if active_org:
390
- console.print(f"[dim]Active org: {active_org}[/dim]")
391
- else:
392
- console.print("[dim]Active org: (none — falls back to default)[/dim]")
408
+ for line in explain(resolve_org_context()):
409
+ console.print(line)
393
410
 
394
411
  # Test connection
395
412
  try:
@@ -3,15 +3,14 @@
3
3
  import importlib.metadata as im
4
4
  import json
5
5
  from pathlib import Path
6
- from typing import Optional
7
6
 
8
7
  import typer
9
8
  from rich.console import Console
10
9
 
11
10
  from ....shared.imports import (
11
+ find_imports_in_file,
12
12
  is_stdlib_module_by_name,
13
13
  should_skip_path,
14
- find_imports_in_file,
15
14
  )
16
15
 
17
16
  console = Console()
@@ -38,7 +37,7 @@ def get_package_name(module_name: str) -> str:
38
37
  return PACKAGE_NAME_MAP.get(top_level, top_level)
39
38
 
40
39
 
41
- def get_installed_version(package_name: str) -> Optional[str]:
40
+ def get_installed_version(package_name: str) -> str | None:
42
41
  """Get installed version of a package."""
43
42
  try:
44
43
  return im.version(package_name)
@@ -99,15 +98,22 @@ def parse_requirements_file(req_file: Path) -> list[str]:
99
98
 
100
99
  def extract_package_name(dep: str) -> str:
101
100
  """Extract package name from a dependency string."""
102
- return dep.split("==")[0].split(">=")[0].split("<=")[0].split("<")[0].split(">")[0].split("[")[0].split("~=")[0].lower()
101
+ return (
102
+ dep.split("==")[0]
103
+ .split(">=")[0]
104
+ .split("<=")[0]
105
+ .split("<")[0]
106
+ .split(">")[0]
107
+ .split("[")[0]
108
+ .split("~=")[0]
109
+ .lower()
110
+ )
103
111
 
104
112
 
105
113
  @config_app.command("init")
106
114
  def init_config(
107
115
  workspace: Path = typer.Argument(".", help="Workspace directory to analyze"),
108
- requirements: Path | None = typer.Option(
109
- None, "--requirements", "-r", help="Path to requirements.txt"
110
- ),
116
+ requirements: Path | None = typer.Option(None, "--requirements", "-r", help="Path to requirements.txt"),
111
117
  force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
112
118
  ):
113
119
  """Initialize workspace configuration for Lyceum Cloud execution.
@@ -161,10 +167,7 @@ def init_config(
161
167
 
162
168
  # Filter to external dependencies
163
169
  local_package_names = {Path(p).parts[0] if "/" in p else p for p in local_packages.keys()}
164
- external_deps = {
165
- imp for imp in all_imports
166
- if not is_stdlib_module_by_name(imp) and imp not in local_package_names
167
- }
170
+ external_deps = {imp for imp in all_imports if not is_stdlib_module_by_name(imp) and imp not in local_package_names}
168
171
  console.print(f"[dim]Found {len(external_deps)} external dependencies from imports[/dim]")
169
172
 
170
173
  # Parse requirements.txt
@@ -249,9 +252,7 @@ def show_config(
249
252
  @config_app.command("refresh")
250
253
  def refresh_config(
251
254
  workspace: Path = typer.Argument(".", help="Workspace directory"),
252
- requirements: Path | None = typer.Option(
253
- None, "--requirements", "-r", help="Path to requirements.txt"
254
- ),
255
+ requirements: Path | None = typer.Option(None, "--requirements", "-r", help="Path to requirements.txt"),
255
256
  ):
256
257
  """Refresh workspace configuration (re-scans the workspace)."""
257
258
  init_config(workspace=workspace, requirements=requirements, force=True)
@@ -1,10 +1,7 @@
1
- """
2
- Docker execution commands
3
- """
1
+ """Docker execution commands"""
4
2
 
5
3
  import json
6
4
  import shlex
7
- from typing import Optional
8
5
 
9
6
  import httpx
10
7
  import typer
@@ -28,37 +25,25 @@ docker_app = typer.Typer(name="docker", help="Docker execution commands")
28
25
  @docker_app.command("run")
29
26
  def run_docker(
30
27
  image: str = typer.Argument(..., help="Docker image to run"),
31
- machine_type: str = typer.Option(
32
- "gpu.l40s", "--machine", "-m", help="Machine type (gpu.l40s, a100, h100, etc.)"
33
- ),
34
- timeout: int = typer.Option(
35
- 300, "--timeout", "-t", help="Execution timeout in seconds"
36
- ),
37
- file_name: Optional[str] = typer.Option(
38
- None, "--file-name", "-f", help="Name for the execution"
39
- ),
40
- run_name: Optional[str] = RUN_NAME_OPTION,
41
- label: Optional[list[str]] = LABEL_OPTION,
42
- metadata: Optional[list[str]] = METADATA_OPTION,
43
- command: Optional[str] = typer.Option(
28
+ machine_type: str = typer.Option("gpu.l40s", "--machine", "-m", help="Machine type (gpu.l40s, a100, h100, etc.)"),
29
+ timeout: int = typer.Option(300, "--timeout", "-t", help="Execution timeout in seconds"),
30
+ file_name: str | None = typer.Option(None, "--file-name", "-f", help="Name for the execution"),
31
+ run_name: str | None = RUN_NAME_OPTION,
32
+ label: list[str] | None = LABEL_OPTION,
33
+ metadata: list[str] | None = METADATA_OPTION,
34
+ command: str | None = typer.Option(
44
35
  None,
45
36
  "--command",
46
37
  "-c",
47
38
  help="Command to run in container (e.g., 'python app.py')",
48
39
  ),
49
- env: Optional[list[str]] = typer.Option(
50
- None, "--env", "-e", help="Environment variables (e.g., KEY=value)"
51
- ),
52
- detach: bool = typer.Option(
53
- False, "--detach", "-d", help="Run container in background and print execution ID"
54
- ),
55
- callback_url: Optional[str] = typer.Option(
56
- None, "--callback", help="Webhook URL for completion notification"
57
- ),
58
- registry_creds: Optional[str] = typer.Option(
40
+ env: list[str] | None = typer.Option(None, "--env", "-e", help="Environment variables (e.g., KEY=value)"),
41
+ detach: bool = typer.Option(False, "--detach", "-d", help="Run container in background and print execution ID"),
42
+ callback_url: str | None = typer.Option(None, "--callback", help="Webhook URL for completion notification"),
43
+ registry_creds: str | None = typer.Option(
59
44
  None, "--registry-creds", help="Docker registry credentials as JSON string"
60
45
  ),
61
- registry_type: Optional[str] = typer.Option(
46
+ registry_type: str | None = typer.Option(
62
47
  None, "--registry-type", help="Registry credential type: basic, aws, etc."
63
48
  ),
64
49
  s3_mount: bool = typer.Option(
@@ -67,7 +52,7 @@ def run_docker(
67
52
  s3_mount_path: str = typer.Option(
68
53
  "/mnt/s3", "--s3-mount-path", help="Path where S3 bucket will be mounted inside the container"
69
54
  ),
70
- graceful_timeout: Optional[int] = typer.Option(
55
+ graceful_timeout: int | None = typer.Option(
71
56
  None, "--graceful-timeout", help="Seconds to wait for graceful shutdown on cancel (default: 10)"
72
57
  ),
73
58
  ):
@@ -98,9 +83,7 @@ def run_docker(
98
83
  docker_env[key] = value
99
84
  else:
100
85
  status.stop()
101
- console.print(
102
- f"[yellow]Warning: Ignoring invalid env var format: {env_var}[/yellow]"
103
- )
86
+ console.print(f"[yellow]Warning: Ignoring invalid env var format: {env_var}[/yellow]")
104
87
  status.start()
105
88
 
106
89
  # Parse registry credentials
@@ -110,19 +93,13 @@ def run_docker(
110
93
  registry_credentials = json.loads(registry_creds)
111
94
  except json.JSONDecodeError:
112
95
  status.stop()
113
- console.print(
114
- "[red]Error: Invalid JSON format for registry credentials[/red]"
115
- )
96
+ console.print("[red]Error: Invalid JSON format for registry credentials[/red]")
116
97
  raise typer.Exit(1)
117
98
 
118
99
  # Validate registry credentials and type
119
- if (registry_creds and not registry_type) or (
120
- registry_type and not registry_creds
121
- ):
100
+ if (registry_creds and not registry_type) or (registry_type and not registry_creds):
122
101
  status.stop()
123
- console.print(
124
- "[red]Error: Both --registry-creds and --registry-type must be provided together[/red]"
125
- )
102
+ console.print("[red]Error: Both --registry-creds and --registry-type must be provided together[/red]")
126
103
  raise typer.Exit(1)
127
104
 
128
105
  # Validate S3 mount options
@@ -194,9 +171,7 @@ def run_docker(
194
171
  status.stop()
195
172
  console.print(f"[red]Error: HTTP {response.status_code}[/red]")
196
173
  if response.status_code == 401:
197
- console.print(
198
- "[red]Authentication failed. Your session may have expired.[/red]"
199
- )
174
+ console.print("[red]Authentication failed. Your session may have expired.[/red]")
200
175
  console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
201
176
  elif response.content:
202
177
  console.print(f"[red]{response.content.decode()}[/red]")
@@ -277,32 +252,33 @@ def show_registry_examples():
277
252
 
278
253
  console.print("[bold]1. Docker Hub (basic)[/bold]")
279
254
  console.print("Type: [green]basic[/green]")
280
- console.print(
281
- 'Credentials: [yellow]\'{"username": "myuser", "password": "mypassword"}\'[/yellow]\n'
282
- )
255
+ console.print('Credentials: [yellow]\'{"username": "myuser", "password": "mypassword"}\'[/yellow]\n')
283
256
 
284
257
  console.print("[bold]2. AWS ECR (aws)[/bold]")
285
258
  console.print("Type: [green]aws[/green]")
286
259
  console.print(
287
- 'Credentials: [yellow]\'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", "aws_secret_access_key": "wJalrX...", "session_token": "optional..."}\'[/yellow]\n'
260
+ 'Credentials: [yellow]\'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", '
261
+ '"aws_secret_access_key": "wJalrX...", "session_token": "optional..."}\'[/yellow]\n'
288
262
  )
289
263
 
290
264
  console.print("[bold]3. Private Registry (basic)[/bold]")
291
265
  console.print("Type: [green]basic[/green]")
292
- console.print(
293
- 'Credentials: [yellow]\'{"username": "admin", "password": "secret"}\'[/yellow]\n'
294
- )
266
+ console.print('Credentials: [yellow]\'{"username": "admin", "password": "secret"}\'[/yellow]\n')
295
267
 
296
268
  console.print("[bold]Example Commands:[/bold]")
297
269
  console.print("# Docker Hub:")
298
270
  console.print(
299
- "[dim]lyceum docker run myuser/myapp:latest --registry-type basic --registry-creds '{\"username\": \"myuser\", \"password\": \"mytoken\"}'[/dim]"
271
+ "[dim]lyceum docker run myuser/myapp:latest --registry-type basic "
272
+ '--registry-creds \'{"username": "myuser", "password": "mytoken"}\'[/dim]'
300
273
  )
301
274
  console.print("\n# AWS ECR:")
302
275
  console.print(
303
- "[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]"
276
+ "[dim]lyceum docker run 123456789012.dkr.ecr.us-west-2.amazonaws.com/myapp:latest "
277
+ '--registry-type aws --registry-creds \'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", '
278
+ '"aws_secret_access_key": "wJalrX..."}\'[/dim]'
304
279
  )
305
280
  console.print("\n# Private Registry:")
306
281
  console.print(
307
- "[dim]lyceum docker run myregistry.com/myapp:latest --registry-type basic --registry-creds '{\"username\": \"admin\", \"password\": \"secret\"}'[/dim]"
282
+ "[dim]lyceum docker run myregistry.com/myapp:latest --registry-type basic "
283
+ '--registry-creds \'{"username": "admin", "password": "secret"}\'[/dim]'
308
284
  )
@@ -1,10 +1,7 @@
1
- """
2
- Docker Compose execution commands
3
- """
1
+ """Docker Compose execution commands"""
4
2
 
5
3
  import json
6
4
  from pathlib import Path
7
- from typing import Optional
8
5
 
9
6
  import httpx
10
7
  import typer
@@ -28,38 +25,23 @@ compose_app = typer.Typer(name="compose", help="Docker Compose execution command
28
25
  @compose_app.command("run")
29
26
  def run_docker_compose(
30
27
  compose_file: Path = typer.Argument(
31
- ...,
32
- help="Path to docker-compose.yml file",
33
- exists=True,
34
- file_okay=True,
35
- dir_okay=False,
36
- readable=True
28
+ ..., help="Path to docker-compose.yml file", exists=True, file_okay=True, dir_okay=False, readable=True
37
29
  ),
38
- machine_type: str = typer.Option(
39
- "gpu.l40s", "--machine", "-m", help="Machine type (gpu.l40s, a100, h100, etc.)"
40
- ),
41
- timeout: int = typer.Option(
42
- 300, "--timeout", "-t", help="Execution timeout in seconds"
43
- ),
44
- file_name: Optional[str] = typer.Option(
45
- None, "--file-name", "-f", help="Name for the execution"
46
- ),
47
- run_name: Optional[str] = RUN_NAME_OPTION,
48
- label: Optional[list[str]] = LABEL_OPTION,
49
- metadata: Optional[list[str]] = METADATA_OPTION,
50
- detach: bool = typer.Option(
51
- False, "--detach", "-d", help="Run in background and print execution ID"
52
- ),
53
- callback_url: Optional[str] = typer.Option(
54
- None, "--callback", help="Webhook URL for completion notification"
55
- ),
56
- registry_creds: Optional[str] = typer.Option(
30
+ machine_type: str = typer.Option("gpu.l40s", "--machine", "-m", help="Machine type (gpu.l40s, a100, h100, etc.)"),
31
+ timeout: int = typer.Option(300, "--timeout", "-t", help="Execution timeout in seconds"),
32
+ file_name: str | None = typer.Option(None, "--file-name", "-f", help="Name for the execution"),
33
+ run_name: str | None = RUN_NAME_OPTION,
34
+ label: list[str] | None = LABEL_OPTION,
35
+ metadata: list[str] | None = METADATA_OPTION,
36
+ detach: bool = typer.Option(False, "--detach", "-d", help="Run in background and print execution ID"),
37
+ callback_url: str | None = typer.Option(None, "--callback", help="Webhook URL for completion notification"),
38
+ registry_creds: str | None = typer.Option(
57
39
  None, "--registry-creds", help="Docker registry credentials as JSON string"
58
40
  ),
59
- registry_type: Optional[str] = typer.Option(
41
+ registry_type: str | None = typer.Option(
60
42
  None, "--registry-type", help="Registry credential type: basic, aws, etc."
61
43
  ),
62
- graceful_timeout: Optional[int] = typer.Option(
44
+ graceful_timeout: int | None = typer.Option(
63
45
  None, "--graceful-timeout", help="Seconds to wait for graceful shutdown on cancel (default: 10)"
64
46
  ),
65
47
  ):
@@ -96,19 +78,13 @@ def run_docker_compose(
96
78
  registry_credentials = json.loads(registry_creds)
97
79
  except json.JSONDecodeError:
98
80
  status.stop()
99
- console.print(
100
- "[red]Error: Invalid JSON format for registry credentials[/red]"
101
- )
81
+ console.print("[red]Error: Invalid JSON format for registry credentials[/red]")
102
82
  raise typer.Exit(1)
103
83
 
104
84
  # Validate registry credentials and type
105
- if (registry_creds and not registry_type) or (
106
- registry_type and not registry_creds
107
- ):
85
+ if (registry_creds and not registry_type) or (registry_type and not registry_creds):
108
86
  status.stop()
109
- console.print(
110
- "[red]Error: Both --registry-creds and --registry-type must be provided together[/red]"
111
- )
87
+ console.print("[red]Error: Both --registry-creds and --registry-type must be provided together[/red]")
112
88
  raise typer.Exit(1)
113
89
 
114
90
  status.update(f"Starting Docker Compose from {compose_file.name}...")
@@ -169,9 +145,7 @@ def run_docker_compose(
169
145
  status.stop()
170
146
  console.print(f"[red]Error: HTTP {response.status_code}[/red]")
171
147
  if response.status_code == 401:
172
- console.print(
173
- "[red]Authentication failed. Your session may have expired.[/red]"
174
- )
148
+ console.print("[red]Authentication failed. Your session may have expired.[/red]")
175
149
  console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
176
150
  elif response.content:
177
151
  console.print(f"[red]{response.content.decode()}[/red]")
@@ -251,32 +225,33 @@ def show_registry_examples():
251
225
 
252
226
  console.print("[bold]1. Docker Hub (basic)[/bold]")
253
227
  console.print("Type: [green]basic[/green]")
254
- console.print(
255
- 'Credentials: [yellow]\'{"username": "myuser", "password": "mypassword"}\'[/yellow]\n'
256
- )
228
+ console.print('Credentials: [yellow]\'{"username": "myuser", "password": "mypassword"}\'[/yellow]\n')
257
229
 
258
230
  console.print("[bold]2. AWS ECR (aws)[/bold]")
259
231
  console.print("Type: [green]aws[/green]")
260
232
  console.print(
261
- 'Credentials: [yellow]\'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", "aws_secret_access_key": "wJalrX...", "session_token": "optional..."}\'[/yellow]\n'
233
+ 'Credentials: [yellow]\'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", '
234
+ '"aws_secret_access_key": "wJalrX...", "session_token": "optional..."}\'[/yellow]\n'
262
235
  )
263
236
 
264
237
  console.print("[bold]3. Private Registry (basic)[/bold]")
265
238
  console.print("Type: [green]basic[/green]")
266
- console.print(
267
- 'Credentials: [yellow]\'{"username": "admin", "password": "secret"}\'[/yellow]\n'
268
- )
239
+ console.print('Credentials: [yellow]\'{"username": "admin", "password": "secret"}\'[/yellow]\n')
269
240
 
270
241
  console.print("[bold]Example Commands:[/bold]")
271
242
  console.print("# Docker Hub:")
272
243
  console.print(
273
- "[dim]lyceum compose run docker-compose.yml --registry-type basic --registry-creds '{\"username\": \"myuser\", \"password\": \"mytoken\"}'[/dim]"
244
+ "[dim]lyceum compose run docker-compose.yml --registry-type basic "
245
+ '--registry-creds \'{"username": "myuser", "password": "mytoken"}\'[/dim]'
274
246
  )
275
247
  console.print("\n# AWS ECR:")
276
248
  console.print(
277
- "[dim]lyceum compose run docker-compose.yml --registry-type aws --registry-creds '{\"region\": \"us-west-2\", \"aws_access_key_id\": \"AKIAI...\", \"aws_secret_access_key\": \"wJalrX...\"}'[/dim]"
249
+ "[dim]lyceum compose run docker-compose.yml --registry-type aws "
250
+ '--registry-creds \'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", '
251
+ '"aws_secret_access_key": "wJalrX..."}\'[/dim]'
278
252
  )
279
253
  console.print("\n# Private Registry:")
280
254
  console.print(
281
- "[dim]lyceum compose run docker-compose.yml --registry-type basic --registry-creds '{\"username\": \"admin\", \"password\": \"secret\"}'[/dim]"
255
+ "[dim]lyceum compose run docker-compose.yml --registry-type basic "
256
+ '--registry-creds \'{"username": "admin", "password": "secret"}\'[/dim]'
282
257
  )