lyceum-cli 1.0.25__py3-none-any.whl → 1.0.27__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/auth/login.py +18 -18
- lyceum/external/compute/execution/docker.py +4 -2
- lyceum/external/compute/execution/docker_compose.py +263 -0
- lyceum/external/compute/execution/notebook.py +0 -2
- lyceum/external/compute/execution/python.py +2 -1
- lyceum/external/compute/inference/batch.py +8 -10
- lyceum/external/vms/instances.py +301 -0
- lyceum/external/vms/management.py +383 -0
- lyceum/main.py +3 -0
- lyceum/shared/config.py +19 -24
- lyceum/shared/display.py +12 -31
- lyceum/shared/streaming.py +17 -45
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/METADATA +1 -1
- lyceum_cli-1.0.27.dist-info/RECORD +34 -0
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/WHEEL +1 -1
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/top_level.txt +0 -1
- lyceum/external/compute/execution/docker_config.py +0 -123
- lyceum/external/storage/files.py +0 -273
- lyceum_cli-1.0.25.dist-info/RECORD +0 -46
- tests/__init__.py +0 -1
- tests/conftest.py +0 -200
- tests/unit/__init__.py +0 -1
- tests/unit/external/__init__.py +0 -1
- tests/unit/external/compute/__init__.py +0 -1
- tests/unit/external/compute/execution/__init__.py +0 -1
- tests/unit/external/compute/execution/test_data.py +0 -33
- tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
- tests/unit/external/compute/execution/test_python_helpers.py +0 -406
- tests/unit/external/compute/execution/test_python_run.py +0 -289
- tests/unit/shared/__init__.py +0 -1
- tests/unit/shared/test_config.py +0 -341
- tests/unit/shared/test_streaming.py +0 -259
- /lyceum/external/{storage → vms}/__init__.py +0 -0
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/entry_points.txt +0 -0
lyceum/main.py
CHANGED
|
@@ -11,8 +11,10 @@ from rich.console import Console
|
|
|
11
11
|
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
|
+
from .external.compute.execution.docker_compose import compose_app
|
|
14
15
|
from .external.compute.execution.workloads import workloads_app
|
|
15
16
|
from .external.compute.execution.notebook import notebook_app
|
|
17
|
+
from .external.vms.management import vms_app
|
|
16
18
|
|
|
17
19
|
app = typer.Typer(
|
|
18
20
|
name="lyceum",
|
|
@@ -26,6 +28,7 @@ console = Console()
|
|
|
26
28
|
app.add_typer(auth_app, name="auth")
|
|
27
29
|
app.add_typer(python_app, name="python")
|
|
28
30
|
app.add_typer(docker_app, name="docker")
|
|
31
|
+
app.add_typer(compose_app, name="compose")
|
|
29
32
|
app.add_typer(workloads_app, name="workloads")
|
|
30
33
|
app.add_typer(notebook_app, name="notebook")
|
|
31
34
|
|
lyceum/shared/config.py
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
"""Configuration management for Lyceum CLI"""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
|
|
5
|
-
# Add the generated client to the path
|
|
6
|
-
import sys
|
|
4
|
+
import os
|
|
7
5
|
import time
|
|
8
6
|
from pathlib import Path
|
|
9
7
|
|
|
10
8
|
import jwt
|
|
11
9
|
import typer
|
|
12
10
|
from rich.console import Console
|
|
13
|
-
|
|
14
11
|
from supabase import create_client
|
|
15
12
|
|
|
16
|
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "lyceum-cloud-execution-api-client"))
|
|
17
|
-
|
|
18
|
-
# Commented out - using httpx directly instead
|
|
19
|
-
# from lyceum_cloud_execution_api_client.client import AuthenticatedClient
|
|
20
|
-
|
|
21
13
|
console = Console()
|
|
22
14
|
|
|
23
15
|
# Configuration
|
|
24
16
|
CONFIG_DIR = Path.home() / ".lyceum"
|
|
25
17
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
26
18
|
|
|
19
|
+
# Supabase configuration from environment variables (required)
|
|
20
|
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
|
21
|
+
SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
|
|
24
|
+
class _Config:
|
|
25
|
+
"""Configuration management for Lyceum CLI (private - use the global 'config' instance)"""
|
|
30
26
|
|
|
31
27
|
def __init__(self):
|
|
32
28
|
"""Initialize configuration with default values"""
|
|
@@ -92,17 +88,16 @@ class Config:
|
|
|
92
88
|
return False
|
|
93
89
|
|
|
94
90
|
try:
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
supabase = create_client(supabase_url, supabase_service_key)
|
|
91
|
+
# Check if Supabase credentials are configured
|
|
92
|
+
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
|
|
93
|
+
console.print(
|
|
94
|
+
"[red]Error: SUPABASE_URL and SUPABASE_ANON_KEY environment variables must be set[/red]"
|
|
95
|
+
)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# Use Supabase's refresh_session method with the public anon key
|
|
99
|
+
# This is safe for client-side use and properly scoped for user token refresh
|
|
100
|
+
supabase = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
|
|
106
101
|
|
|
107
102
|
# Use refresh_session method
|
|
108
103
|
response = supabase.auth.refresh_session(refresh_token=self.refresh_token)
|
|
@@ -131,7 +126,7 @@ class Config:
|
|
|
131
126
|
def get_client(self):
|
|
132
127
|
"""Get authenticated API client with automatic token refresh"""
|
|
133
128
|
if not self.api_key:
|
|
134
|
-
console.print("[red]Error: Not authenticated. Run 'lyceum login' first.[/red]")
|
|
129
|
+
console.print("[red]Error: Not authenticated. Run 'lyceum auth login' first.[/red]")
|
|
135
130
|
raise typer.Exit(1)
|
|
136
131
|
|
|
137
132
|
# Check if token is expired and try to refresh
|
|
@@ -146,4 +141,4 @@ class Config:
|
|
|
146
141
|
|
|
147
142
|
|
|
148
143
|
# Global config instance
|
|
149
|
-
config =
|
|
144
|
+
config = _Config()
|
lyceum/shared/display.py
CHANGED
|
@@ -2,25 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
|
-
from rich.table import Table
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def create_table(title: str, columns: list[tuple[str, str]]) -> Table:
|
|
9
|
-
"""
|
|
10
|
-
Create a Rich table with the given title and columns.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
title: Table title
|
|
14
|
-
columns: List of (column_name, style) tuples
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Rich Table instance
|
|
18
|
-
"""
|
|
19
|
-
table = Table(title=title)
|
|
20
|
-
for col_name, style in columns:
|
|
21
|
-
table.add_column(col_name, style=style)
|
|
22
|
-
return table
|
|
23
|
-
|
|
24
5
|
|
|
25
6
|
def format_timestamp(timestamp: str | int | datetime, relative: bool = False) -> str:
|
|
26
7
|
"""
|
|
@@ -161,33 +142,33 @@ def status_color(status: str) -> str:
|
|
|
161
142
|
return 'white'
|
|
162
143
|
|
|
163
144
|
|
|
164
|
-
def
|
|
145
|
+
def status_icon(status: str) -> str:
|
|
165
146
|
"""
|
|
166
|
-
Get
|
|
147
|
+
Get text icon for status display.
|
|
167
148
|
|
|
168
149
|
Args:
|
|
169
150
|
status: Status string
|
|
170
151
|
|
|
171
152
|
Returns:
|
|
172
|
-
|
|
153
|
+
Text-based status indicator
|
|
173
154
|
"""
|
|
174
155
|
status_lower = status.lower()
|
|
175
156
|
|
|
176
157
|
if status_lower in ['completed', 'success']:
|
|
177
|
-
return '
|
|
158
|
+
return '[OK]'
|
|
178
159
|
elif status_lower in ['failed', 'error', 'failed_user', 'failed_system']:
|
|
179
|
-
return '
|
|
160
|
+
return '[FAIL]'
|
|
180
161
|
elif status_lower in ['running', 'in_progress']:
|
|
181
|
-
return '
|
|
162
|
+
return '[RUN]'
|
|
182
163
|
elif status_lower in ['pending', 'queued']:
|
|
183
|
-
return '
|
|
164
|
+
return '[WAIT]'
|
|
184
165
|
elif status_lower in ['validating']:
|
|
185
|
-
return '
|
|
166
|
+
return '[VAL]'
|
|
186
167
|
elif status_lower in ['finalizing']:
|
|
187
|
-
return '
|
|
168
|
+
return '[FIN]'
|
|
188
169
|
elif status_lower in ['cancelled']:
|
|
189
|
-
return '
|
|
170
|
+
return '[STOP]'
|
|
190
171
|
elif status_lower in ['expired', 'timeout']:
|
|
191
|
-
return '
|
|
172
|
+
return '[TIME]'
|
|
192
173
|
else:
|
|
193
|
-
return '
|
|
174
|
+
return '[--]'
|
lyceum/shared/streaming.py
CHANGED
|
@@ -60,7 +60,7 @@ def normalize_newlines(text: str) -> str:
|
|
|
60
60
|
return re.sub(r'\n{2,}', '\n', text)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def stream_execution_output(execution_id: str, streaming_url: str = None
|
|
63
|
+
def stream_execution_output(execution_id: str, streaming_url: str = None) -> bool:
|
|
64
64
|
"""Stream execution output in real-time. Returns True if successful, False if failed."""
|
|
65
65
|
if not streaming_url:
|
|
66
66
|
# Fallback to stream endpoint if no streaming URL provided
|
|
@@ -69,25 +69,15 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
|
|
|
69
69
|
stream_url = streaming_url
|
|
70
70
|
|
|
71
71
|
try:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
headers =
|
|
76
|
-
"Accept": "text/event-stream",
|
|
77
|
-
"Cache-Control": "no-cache",
|
|
78
|
-
}
|
|
79
|
-
with httpx.stream("POST", stream_url, headers=headers, timeout=600.0) as response:
|
|
72
|
+
console.print("[dim]Connecting to execution stream...[/dim]")
|
|
73
|
+
|
|
74
|
+
headers = {"Authorization": f"Bearer {config.api_key}"}
|
|
75
|
+
with httpx.stream("GET", stream_url, headers=headers, timeout=600.0) as response:
|
|
80
76
|
if response.status_code != 200:
|
|
81
|
-
|
|
82
|
-
status.stop()
|
|
83
|
-
if response.status_code == 404:
|
|
84
|
-
console.print("[yellow]Stream not found - execution may have already completed[/yellow]")
|
|
85
|
-
else:
|
|
86
|
-
console.print(f"[red]Stream failed: HTTP {response.status_code}[/red]")
|
|
77
|
+
console.print(f"[red]Stream failed: HTTP {response.status_code}[/red]")
|
|
87
78
|
return False
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
status.update("Waiting for output...")
|
|
80
|
+
console.print("[dim]Streaming output...[/dim]")
|
|
91
81
|
|
|
92
82
|
first_output = True
|
|
93
83
|
for line in response.iter_lines():
|
|
@@ -103,10 +93,6 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
|
|
|
103
93
|
output_data = data["output"]
|
|
104
94
|
content = output_data.get("content", "")
|
|
105
95
|
if content:
|
|
106
|
-
# Stop status spinner on first output
|
|
107
|
-
if first_output and status:
|
|
108
|
-
status.stop()
|
|
109
|
-
first_output = False
|
|
110
96
|
clean_output = strip_ansi_codes(content)
|
|
111
97
|
# Normalize newlines to avoid excessive blank lines
|
|
112
98
|
clean_output = normalize_newlines(clean_output)
|
|
@@ -114,8 +100,6 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
|
|
|
114
100
|
|
|
115
101
|
# Handle job finished event
|
|
116
102
|
elif "jobFinished" in data:
|
|
117
|
-
if status:
|
|
118
|
-
status.stop()
|
|
119
103
|
job_data = data["jobFinished"]
|
|
120
104
|
job = job_data.get("job", {})
|
|
121
105
|
result = job.get("result", {})
|
|
@@ -126,7 +110,6 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
|
|
|
126
110
|
if not first_output:
|
|
127
111
|
print()
|
|
128
112
|
|
|
129
|
-
# Check for system failure (error field present)
|
|
130
113
|
if error:
|
|
131
114
|
console.print(f"[red]{error}[/red]")
|
|
132
115
|
return False
|
|
@@ -143,46 +126,34 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
|
|
|
143
126
|
if event_type == "output":
|
|
144
127
|
output = data.get("content", "")
|
|
145
128
|
if output:
|
|
146
|
-
if first_output and status:
|
|
147
|
-
status.stop()
|
|
148
|
-
first_output = False
|
|
149
129
|
clean_output = strip_ansi_codes(output)
|
|
150
130
|
clean_output = normalize_newlines(clean_output)
|
|
151
131
|
print(clean_output, end="", flush=True)
|
|
152
132
|
|
|
153
133
|
elif event_type == "completed":
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if not first_output:
|
|
158
|
-
print()
|
|
159
|
-
if status_val == "completed":
|
|
134
|
+
status = data.get("status", "unknown")
|
|
135
|
+
if status == "completed":
|
|
136
|
+
console.print("\n[green]Execution completed successfully[/green]")
|
|
160
137
|
return True
|
|
161
138
|
else:
|
|
162
|
-
console.print(f"[red]
|
|
139
|
+
console.print(f"\n[red]Execution failed: {status}[/red]")
|
|
163
140
|
return False
|
|
164
141
|
|
|
165
142
|
elif event_type == "error":
|
|
166
|
-
if status:
|
|
167
|
-
status.stop()
|
|
168
143
|
error_msg = data.get("message", "Unknown error")
|
|
169
|
-
console.print(f"[red]Error: {error_msg}[/red]")
|
|
144
|
+
console.print(f"\n[red]Error: {error_msg}[/red]")
|
|
170
145
|
return False
|
|
171
146
|
|
|
172
147
|
except json.JSONDecodeError:
|
|
173
148
|
# Skip malformed JSON
|
|
174
149
|
continue
|
|
175
150
|
|
|
176
|
-
|
|
177
|
-
status.stop()
|
|
178
|
-
console.print("[yellow]Stream ended without completion signal[/yellow]")
|
|
151
|
+
console.print("\n[yellow]Stream ended without completion signal[/yellow]")
|
|
179
152
|
# Fallback: poll execution status
|
|
180
153
|
return check_execution_status(execution_id)
|
|
181
154
|
|
|
182
155
|
except Exception as e:
|
|
183
|
-
|
|
184
|
-
status.stop()
|
|
185
|
-
console.print(f"[red]Streaming error: {e}[/red]")
|
|
156
|
+
console.print(f"\n[red]Streaming error: {e}[/red]")
|
|
186
157
|
# Fallback: poll execution status
|
|
187
158
|
return check_execution_status(execution_id)
|
|
188
159
|
|
|
@@ -206,15 +177,16 @@ def check_execution_status(execution_id: str) -> bool:
|
|
|
206
177
|
status = data.get('status', 'unknown')
|
|
207
178
|
|
|
208
179
|
if status == 'completed':
|
|
180
|
+
console.print("[green]Execution completed successfully[/green]")
|
|
209
181
|
return True
|
|
210
182
|
elif status in ['failed_user', 'failed_system', 'failed']:
|
|
211
|
-
console.print(f"[red]
|
|
183
|
+
console.print(f"[red]Execution failed: {status}[/red]")
|
|
212
184
|
errors = data.get('errors')
|
|
213
185
|
if errors:
|
|
214
186
|
console.print(f"[red]Error: {errors}[/red]")
|
|
215
187
|
return False
|
|
216
188
|
elif status in ['timeout', 'cancelled']:
|
|
217
|
-
console.print(f"[yellow]{status
|
|
189
|
+
console.print(f"[yellow]Execution {status}[/yellow]")
|
|
218
190
|
return False
|
|
219
191
|
elif status in ['running', 'pending', 'queued']:
|
|
220
192
|
# Still running, continue polling
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
lyceum/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
lyceum/main.py,sha256=s4ZvNPGMeGoV9AIq68hEwfo9ioFfu2qIoRXb2upjNDk,1076
|
|
3
|
+
lyceum/external/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
4
|
+
lyceum/external/auth/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
lyceum/external/auth/login.py,sha256=-yJ0aEV8_vDXiT6BXzjpqZ2uDdnTnkop4qhagw2dSZA,23447
|
|
6
|
+
lyceum/external/compute/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
7
|
+
lyceum/external/compute/execution/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
lyceum/external/compute/execution/config.py,sha256=6JJgLJnDPTwevEaNdB1nEICih_qbBmws5u5_S9gj7k0,8866
|
|
9
|
+
lyceum/external/compute/execution/docker.py,sha256=0Y6lxJAm56Jrl0HxeNz1mX6DGs556i2iMN9_U1JQP0c,9635
|
|
10
|
+
lyceum/external/compute/execution/docker_compose.py,sha256=YsWPnw5nB1ZpqjU9X8o_klT78I5m46PapVwVEeWra_8,9189
|
|
11
|
+
lyceum/external/compute/execution/notebook.py,sha256=Gw9UhJ-UjYhpjdIYQ4IMYhVjhSkAFpOQ9aFYj1qOeww,7542
|
|
12
|
+
lyceum/external/compute/execution/python.py,sha256=8Y9ElWs9RdauQbhECKcBPSoT0XZeGhXZ_pkEpr3sGro,12878
|
|
13
|
+
lyceum/external/compute/execution/workloads.py,sha256=4fsRWbYGmsQMGPPIN1jUG8cG5NPG9yV26ANJ-DtaXqc,5844
|
|
14
|
+
lyceum/external/compute/inference/__init__.py,sha256=4YLoUKDEzitexynJv_Q5O0w1lty8CJ6uyRxuc1LiaBw,89
|
|
15
|
+
lyceum/external/compute/inference/batch.py,sha256=mgEndr02UM1j00o-iRLUpDqS5KFvyg0Htc0Gg0s3hTU,11394
|
|
16
|
+
lyceum/external/compute/inference/chat.py,sha256=hITj_UGLaxCJQskU-YbeaEerM5Xt_eJpEsYrTJoUpk4,8485
|
|
17
|
+
lyceum/external/compute/inference/models.py,sha256=BkCEdvyliezGOUulj557e-Eoif0_HKR3CxqpEhdAZaA,10339
|
|
18
|
+
lyceum/external/general/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
19
|
+
lyceum/external/vms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
lyceum/external/vms/instances.py,sha256=8DKpI8PbyZFzk5RT-IPgoMDjkf_-HC-2pJKuSFs-5BA,11007
|
|
21
|
+
lyceum/external/vms/management.py,sha256=dYEkN5Qiur-SG4G5CLOk2Rbr0HW3rK1BROSp0K6KxC8,15405
|
|
22
|
+
lyceum/shared/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
23
|
+
lyceum/shared/config.py,sha256=gz2AjjdOsi6WbJApIh_GYRs_GYjsORQ6h3YJiHEIOxI,5384
|
|
24
|
+
lyceum/shared/display.py,sha256=-VSAfoa0yivTvxRrN2RYr2Sq1x_msZqENjnkSedmbhQ,4444
|
|
25
|
+
lyceum/shared/imports.py,sha256=wEG4wfVTIqJ6MBWDRAN96iGmVCb9ST2aOqSjkbvajug,11768
|
|
26
|
+
lyceum/shared/streaming.py,sha256=x3zA8Pn9ia06t8nKJfP6hztxOVKPUC3Nk3qmAiIsl9M,8194
|
|
27
|
+
lyceum_cloud_execution_api_client/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
28
|
+
lyceum_cloud_execution_api_client/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
29
|
+
lyceum_cloud_execution_api_client/models/__init__.py,sha256=AMlb9R9O9aNC9hvKz_8TFpEfOolYC3VtFS5JX17kYks,4888
|
|
30
|
+
lyceum_cli-1.0.27.dist-info/METADATA,sha256=r__wR3k1hw1LzdIwKOVpNoOWRwYmJ1pmYhonGMC73Sc,1482
|
|
31
|
+
lyceum_cli-1.0.27.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
32
|
+
lyceum_cli-1.0.27.dist-info/entry_points.txt,sha256=Oq-9wDkxVd6MHgNiUTYwXI9SGhvR3VkD7Mvk0xhiUZo,43
|
|
33
|
+
lyceum_cli-1.0.27.dist-info/top_level.txt,sha256=CR7FEMloAXgLsHUR6ti3mWNcpgje27HRHSfq8doIils,41
|
|
34
|
+
lyceum_cli-1.0.27.dist-info/RECORD,,
|
|
@@ -1,123 +0,0 @@
|
|
|
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]")
|