lyceum-cli 1.0.14__tar.gz → 1.0.18__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.14 → lyceum_cli-1.0.18}/PKG-INFO +1 -1
- lyceum_cli-1.0.18/lyceum/__init__.py +0 -0
- lyceum_cli-1.0.18/lyceum/external/__init__.py +0 -0
- lyceum_cli-1.0.18/lyceum/external/auth/__init__.py +0 -0
- lyceum_cli-1.0.18/lyceum/external/compute/__init__.py +0 -0
- lyceum_cli-1.0.18/lyceum/external/compute/execution/__init__.py +0 -0
- lyceum_cli-1.0.18/lyceum/external/compute/execution/python.py +99 -0
- lyceum_cli-1.0.18/lyceum/external/compute/inference/__init__.py +2 -0
- lyceum_cli-1.0.18/lyceum/external/general/__init__.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum/main.py +0 -8
- lyceum_cli-1.0.18/lyceum/shared/__init__.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum/shared/config.py +7 -8
- lyceum_cli-1.0.18/lyceum/shared/display.py +195 -0
- lyceum_cli-1.0.18/lyceum/shared/streaming.py +134 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum_cli.egg-info/PKG-INFO +1 -1
- lyceum_cli-1.0.18/lyceum_cli.egg-info/SOURCES.txt +27 -0
- lyceum_cli-1.0.18/lyceum_cli.egg-info/top_level.txt +2 -0
- lyceum_cli-1.0.18/lyceum_cloud_execution_api_client/__init__.py +0 -0
- lyceum_cli-1.0.18/lyceum_cloud_execution_api_client/api/__init__.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/setup.py +1 -1
- lyceum_cli-1.0.14/lyceum/external/compute/inference/__init__.py +0 -5
- lyceum_cli-1.0.14/lyceum_cli.egg-info/SOURCES.txt +0 -15
- lyceum_cli-1.0.14/lyceum_cli.egg-info/top_level.txt +0 -1
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum/external/auth/login.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum/external/compute/inference/batch.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum/external/compute/inference/chat.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum/external/compute/inference/models.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum_cli.egg-info/dependency_links.txt +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum_cli.egg-info/entry_points.txt +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum_cli.egg-info/requires.txt +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum_cloud_execution_api_client/models/__init__.py +0 -0
- {lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/setup.cfg +0 -0
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python execution commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ....shared.config import config
|
|
12
|
+
from ....shared.streaming import stream_execution_output
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
python_app = typer.Typer(name="python", help="Python execution commands")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@python_app.command("run")
|
|
20
|
+
def run_python(
|
|
21
|
+
code_or_file: str = typer.Argument(..., help="Python code to execute or path to Python file"),
|
|
22
|
+
machine_type: str = typer.Option("cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"),
|
|
23
|
+
timeout: int = typer.Option(60, "--timeout", "-t", help="Execution timeout in seconds"),
|
|
24
|
+
file_name: Optional[str] = typer.Option(None, "--file-name", "-f", help="Name for the execution"),
|
|
25
|
+
requirements: Optional[str] = typer.Option(None, "--requirements", "-r", help="Requirements file path or pip requirements string"),
|
|
26
|
+
imports: Optional[list[str]] = typer.Option(None, "--import", help="Pre-import modules (can be used multiple times)"),
|
|
27
|
+
):
|
|
28
|
+
"""Execute Python code or file on Lyceum Cloud"""
|
|
29
|
+
try:
|
|
30
|
+
# Check if it's a file path
|
|
31
|
+
code_to_execute = code_or_file
|
|
32
|
+
if Path(code_or_file).exists():
|
|
33
|
+
console.print(f"[dim]Reading code from file: {code_or_file}[/dim]")
|
|
34
|
+
with open(code_or_file, 'r') as f:
|
|
35
|
+
code_to_execute = f.read()
|
|
36
|
+
# Use filename as execution name if not provided
|
|
37
|
+
if not file_name:
|
|
38
|
+
file_name = Path(code_or_file).name
|
|
39
|
+
|
|
40
|
+
# Handle requirements
|
|
41
|
+
requirements_content = None
|
|
42
|
+
if requirements:
|
|
43
|
+
# Check if it's a file path
|
|
44
|
+
if Path(requirements).exists():
|
|
45
|
+
console.print(f"[dim]Reading requirements from file: {requirements}[/dim]")
|
|
46
|
+
with open(requirements, 'r') as f:
|
|
47
|
+
requirements_content = f.read()
|
|
48
|
+
else:
|
|
49
|
+
# Treat as direct pip requirements string
|
|
50
|
+
requirements_content = requirements
|
|
51
|
+
|
|
52
|
+
# Create execution request payload
|
|
53
|
+
payload = {
|
|
54
|
+
"code": code_to_execute,
|
|
55
|
+
"nbcode": 0,
|
|
56
|
+
"execution_type": machine_type,
|
|
57
|
+
"timeout": timeout,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if file_name:
|
|
61
|
+
payload["file_name"] = file_name
|
|
62
|
+
if requirements_content:
|
|
63
|
+
payload["requirements_content"] = requirements_content
|
|
64
|
+
if imports:
|
|
65
|
+
payload["prior_imports"] = imports
|
|
66
|
+
|
|
67
|
+
# Make API request
|
|
68
|
+
response = httpx.post(
|
|
69
|
+
f"{config.base_url}/api/v2/external/execution/streaming/start",
|
|
70
|
+
headers={"Authorization": f"Bearer {config.api_key}"},
|
|
71
|
+
json=payload,
|
|
72
|
+
timeout=30.0
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if response.status_code != 200:
|
|
76
|
+
console.print(f"[red]Error: HTTP {response.status_code}[/red]")
|
|
77
|
+
console.print(f"[red]{response.content.decode()}[/red]")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
data = response.json()
|
|
81
|
+
execution_id = data['execution_id']
|
|
82
|
+
streaming_url = data.get('streaming_url')
|
|
83
|
+
|
|
84
|
+
console.print(f"[green]✅ Execution started![/green]")
|
|
85
|
+
console.print(f"[dim]Execution ID: {execution_id}[/dim]")
|
|
86
|
+
|
|
87
|
+
if 'pythia_decision' in data:
|
|
88
|
+
console.print(f"[dim]Pythia recommendation: {data['pythia_decision']}[/dim]")
|
|
89
|
+
|
|
90
|
+
# Stream the execution output
|
|
91
|
+
success = stream_execution_output(execution_id, streaming_url)
|
|
92
|
+
|
|
93
|
+
if not success:
|
|
94
|
+
console.print(f"[yellow]💡 You can check the execution later with: lyceum status[/yellow]")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
99
|
+
raise typer.Exit(1)
|
|
File without changes
|
|
@@ -9,14 +9,10 @@ from rich.console import Console
|
|
|
9
9
|
|
|
10
10
|
# Import all command modules
|
|
11
11
|
from .external.auth.login import auth_app
|
|
12
|
-
from .external.auth.api_keys import api_keys_app
|
|
13
12
|
from .external.compute.execution.python import python_app
|
|
14
|
-
from .external.compute.execution.docker import docker_app
|
|
15
|
-
from .external.compute.execution.workloads import workloads_app
|
|
16
13
|
from .external.compute.inference.batch import batch_app
|
|
17
14
|
from .external.compute.inference.chat import chat_app
|
|
18
15
|
from .external.compute.inference.models import models_app
|
|
19
|
-
from .external.general.resources import resources_app
|
|
20
16
|
|
|
21
17
|
app = typer.Typer(
|
|
22
18
|
name="lyceum",
|
|
@@ -28,14 +24,10 @@ console = Console()
|
|
|
28
24
|
|
|
29
25
|
# Add all command groups
|
|
30
26
|
app.add_typer(auth_app, name="auth")
|
|
31
|
-
app.add_typer(api_keys_app, name="api-keys")
|
|
32
27
|
app.add_typer(python_app, name="python")
|
|
33
|
-
app.add_typer(docker_app, name="docker")
|
|
34
|
-
app.add_typer(workloads_app, name="workloads")
|
|
35
28
|
app.add_typer(batch_app, name="batch")
|
|
36
29
|
app.add_typer(chat_app, name="chat")
|
|
37
30
|
app.add_typer(models_app, name="models")
|
|
38
|
-
app.add_typer(resources_app, name="resources")
|
|
39
31
|
|
|
40
32
|
# Legacy aliases for backward compatibility
|
|
41
33
|
|
|
File without changes
|
|
@@ -17,7 +17,8 @@ from supabase import create_client
|
|
|
17
17
|
import sys
|
|
18
18
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "lyceum-cloud-execution-api-client"))
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Commented out - using httpx directly instead
|
|
21
|
+
# from lyceum_cloud_execution_api_client.client import AuthenticatedClient
|
|
21
22
|
|
|
22
23
|
console = Console()
|
|
23
24
|
|
|
@@ -122,23 +123,21 @@ class Config:
|
|
|
122
123
|
console.print(f"[yellow]⚠️ Token refresh error: {e}[/yellow]")
|
|
123
124
|
return False
|
|
124
125
|
|
|
125
|
-
def get_client(self)
|
|
126
|
+
def get_client(self):
|
|
126
127
|
"""Get authenticated API client with automatic token refresh"""
|
|
127
128
|
if not self.api_key:
|
|
128
129
|
console.print("[red]Error: Not authenticated. Run 'lyceum login' first.[/red]")
|
|
129
130
|
raise typer.Exit(1)
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
# Check if token is expired and try to refresh
|
|
132
133
|
if self.is_token_expired():
|
|
133
134
|
console.print("[dim]🔄 Token expired, attempting refresh...[/dim]")
|
|
134
135
|
if not self.refresh_access_token():
|
|
135
136
|
console.print("[red]❌ Token refresh failed. Please run 'lyceum login' again.[/red]")
|
|
136
137
|
raise typer.Exit(1)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
token=self.api_key
|
|
141
|
-
)
|
|
138
|
+
|
|
139
|
+
# Return config instance - commands use httpx directly
|
|
140
|
+
return self
|
|
142
141
|
|
|
143
142
|
|
|
144
143
|
# Global config instance
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Display utilities for CLI output formatting
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_table(title: str, columns: list[tuple[str, str]]) -> Table:
|
|
11
|
+
"""
|
|
12
|
+
Create a Rich table with the given title and columns.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
title: Table title
|
|
16
|
+
columns: List of (column_name, style) tuples
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Rich Table instance
|
|
20
|
+
"""
|
|
21
|
+
table = Table(title=title)
|
|
22
|
+
for col_name, style in columns:
|
|
23
|
+
table.add_column(col_name, style=style)
|
|
24
|
+
return table
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_timestamp(timestamp: str | int | datetime, relative: bool = False) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Format a timestamp for display.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
timestamp: Unix timestamp (int/float), ISO string, or datetime object
|
|
33
|
+
relative: If True, show relative time (e.g., "2 hours ago")
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Formatted timestamp string
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Convert to datetime if needed
|
|
40
|
+
if isinstance(timestamp, (int, float)):
|
|
41
|
+
dt = datetime.fromtimestamp(timestamp)
|
|
42
|
+
elif isinstance(timestamp, str):
|
|
43
|
+
# Try parsing ISO format
|
|
44
|
+
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
45
|
+
elif isinstance(timestamp, datetime):
|
|
46
|
+
dt = timestamp
|
|
47
|
+
else:
|
|
48
|
+
return str(timestamp)
|
|
49
|
+
|
|
50
|
+
if relative:
|
|
51
|
+
# Calculate relative time
|
|
52
|
+
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
|
|
53
|
+
diff = now - dt
|
|
54
|
+
|
|
55
|
+
seconds = diff.total_seconds()
|
|
56
|
+
if seconds < 60:
|
|
57
|
+
return f"{int(seconds)}s ago"
|
|
58
|
+
elif seconds < 3600:
|
|
59
|
+
return f"{int(seconds / 60)}m ago"
|
|
60
|
+
elif seconds < 86400:
|
|
61
|
+
return f"{int(seconds / 3600)}h ago"
|
|
62
|
+
elif seconds < 604800:
|
|
63
|
+
return f"{int(seconds / 86400)}d ago"
|
|
64
|
+
else:
|
|
65
|
+
return dt.strftime("%Y-%m-%d")
|
|
66
|
+
else:
|
|
67
|
+
# Return absolute time
|
|
68
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
69
|
+
|
|
70
|
+
except Exception:
|
|
71
|
+
return str(timestamp)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def truncate_id(id_str: str, length: int = 8) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Truncate an ID string for display.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
id_str: The ID string to truncate
|
|
80
|
+
length: Number of characters to keep from the start
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Truncated ID string
|
|
84
|
+
"""
|
|
85
|
+
if not id_str:
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
if len(id_str) <= length:
|
|
89
|
+
return id_str
|
|
90
|
+
|
|
91
|
+
return f"{id_str[:length]}..."
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_file_size(bytes: int) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Format file size in human-readable format.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
bytes: Size in bytes
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Formatted size string (e.g., "1.5 MB")
|
|
103
|
+
"""
|
|
104
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
105
|
+
if bytes < 1024.0:
|
|
106
|
+
return f"{bytes:.1f} {unit}"
|
|
107
|
+
bytes /= 1024.0
|
|
108
|
+
return f"{bytes:.1f} PB"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def format_duration(seconds: float) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Format duration in human-readable format.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
seconds: Duration in seconds
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Formatted duration string (e.g., "2h 15m 30s")
|
|
120
|
+
"""
|
|
121
|
+
if seconds < 60:
|
|
122
|
+
return f"{seconds:.1f}s"
|
|
123
|
+
|
|
124
|
+
minutes = int(seconds / 60)
|
|
125
|
+
secs = int(seconds % 60)
|
|
126
|
+
|
|
127
|
+
if minutes < 60:
|
|
128
|
+
return f"{minutes}m {secs}s"
|
|
129
|
+
|
|
130
|
+
hours = int(minutes / 60)
|
|
131
|
+
mins = int(minutes % 60)
|
|
132
|
+
|
|
133
|
+
if hours < 24:
|
|
134
|
+
return f"{hours}h {mins}m"
|
|
135
|
+
|
|
136
|
+
days = int(hours / 24)
|
|
137
|
+
hrs = int(hours % 24)
|
|
138
|
+
|
|
139
|
+
return f"{days}d {hrs}h"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def status_color(status: str) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Get color for status display.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
status: Status string
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Color name for Rich formatting
|
|
151
|
+
"""
|
|
152
|
+
status_lower = status.lower()
|
|
153
|
+
|
|
154
|
+
if status_lower in ['completed', 'success', 'running']:
|
|
155
|
+
return 'green'
|
|
156
|
+
elif status_lower in ['failed', 'error', 'failed_user', 'failed_system']:
|
|
157
|
+
return 'red'
|
|
158
|
+
elif status_lower in ['pending', 'queued', 'in_progress', 'validating', 'finalizing']:
|
|
159
|
+
return 'yellow'
|
|
160
|
+
elif status_lower in ['cancelled', 'expired', 'timeout']:
|
|
161
|
+
return 'dim'
|
|
162
|
+
else:
|
|
163
|
+
return 'white'
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def status_emoji(status: str) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Get emoji for status display.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
status: Status string
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Emoji character
|
|
175
|
+
"""
|
|
176
|
+
status_lower = status.lower()
|
|
177
|
+
|
|
178
|
+
if status_lower in ['completed', 'success']:
|
|
179
|
+
return '✅'
|
|
180
|
+
elif status_lower in ['failed', 'error', 'failed_user', 'failed_system']:
|
|
181
|
+
return '❌'
|
|
182
|
+
elif status_lower in ['running', 'in_progress']:
|
|
183
|
+
return '🔄'
|
|
184
|
+
elif status_lower in ['pending', 'queued']:
|
|
185
|
+
return '⏳'
|
|
186
|
+
elif status_lower in ['validating']:
|
|
187
|
+
return '🔍'
|
|
188
|
+
elif status_lower in ['finalizing']:
|
|
189
|
+
return '📦'
|
|
190
|
+
elif status_lower in ['cancelled']:
|
|
191
|
+
return '🛑'
|
|
192
|
+
elif status_lower in ['expired', 'timeout']:
|
|
193
|
+
return '⏰'
|
|
194
|
+
else:
|
|
195
|
+
return '⚪'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Streaming utilities for execution output
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import httpx
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .config import config
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def strip_ansi_codes(text: str) -> str:
|
|
16
|
+
"""Remove ANSI escape codes from text"""
|
|
17
|
+
ansi_escape = re.compile(r'\x1b\[[0-9;]*m')
|
|
18
|
+
return ansi_escape.sub('', text)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def stream_execution_output(execution_id: str, streaming_url: str = None) -> bool:
|
|
22
|
+
"""Stream execution output in real-time. Returns True if successful, False if failed."""
|
|
23
|
+
if not streaming_url:
|
|
24
|
+
# Fallback to old endpoint if no streaming URL provided
|
|
25
|
+
stream_url = f"{config.base_url}/api/v2/external/execution/streaming/{execution_id}"
|
|
26
|
+
else:
|
|
27
|
+
stream_url = streaming_url
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
console.print(f"[dim]🔗 Connecting to execution stream...[/dim]")
|
|
31
|
+
console.print(f"[dim]Stream URL: {stream_url}[/dim]")
|
|
32
|
+
|
|
33
|
+
with httpx.stream("GET", stream_url, headers={"Authorization": f"Bearer {config.api_key}"}, timeout=600.0) as response:
|
|
34
|
+
if response.status_code != 200:
|
|
35
|
+
console.print(f"[red]❌ Stream failed: HTTP {response.status_code}[/red]")
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
console.print("[dim]📡 Streaming output...[/dim]")
|
|
39
|
+
|
|
40
|
+
for line in response.iter_lines():
|
|
41
|
+
if line.strip():
|
|
42
|
+
# Parse Server-Sent Events format
|
|
43
|
+
if line.startswith("data: "):
|
|
44
|
+
data_json = line[6:] # Remove "data: " prefix
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(data_json)
|
|
47
|
+
event_type = data.get("type", "unknown")
|
|
48
|
+
|
|
49
|
+
if event_type == "output":
|
|
50
|
+
# Print output without extra formatting, stripping ANSI codes
|
|
51
|
+
output = data.get("content", "") # Fixed: server sends "content" not "output"
|
|
52
|
+
if output:
|
|
53
|
+
clean_output = strip_ansi_codes(output)
|
|
54
|
+
console.print(clean_output, end="")
|
|
55
|
+
|
|
56
|
+
elif event_type == "completed":
|
|
57
|
+
status = data.get("status", "unknown")
|
|
58
|
+
exec_time = data.get("execution_time", 0)
|
|
59
|
+
|
|
60
|
+
if status == "completed":
|
|
61
|
+
console.print(f"\n[green]✅ Execution completed successfully in {exec_time:.1f}s[/green]")
|
|
62
|
+
elif status in ["failed_user", "failed_system"]:
|
|
63
|
+
console.print(f"\n[red]❌ Execution failed: {status}[/red]")
|
|
64
|
+
# Show errors if available
|
|
65
|
+
errors = data.get("errors")
|
|
66
|
+
if errors:
|
|
67
|
+
console.print(f"[red]Error: {errors}[/red]")
|
|
68
|
+
elif status == "timeout":
|
|
69
|
+
console.print(f"\n[yellow]⏰ Execution timed out after {exec_time:.1f}s[/yellow]")
|
|
70
|
+
elif status == "cancelled":
|
|
71
|
+
console.print(f"\n[yellow]🛑 Execution was cancelled[/yellow]")
|
|
72
|
+
|
|
73
|
+
return status == "completed"
|
|
74
|
+
|
|
75
|
+
elif event_type == "error":
|
|
76
|
+
error_msg = data.get("message", "Unknown error")
|
|
77
|
+
console.print(f"\n[red]❌ Error: {error_msg}[/red]")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
except json.JSONDecodeError:
|
|
81
|
+
# Skip malformed JSON
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
console.print(f"\n[yellow]⚠️ Stream ended without completion signal[/yellow]")
|
|
85
|
+
# Fallback: poll execution status
|
|
86
|
+
return check_execution_status(execution_id)
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
console.print(f"\n[red]❌ Streaming error: {e}[/red]")
|
|
90
|
+
# Fallback: poll execution status
|
|
91
|
+
return check_execution_status(execution_id)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def check_execution_status(execution_id: str) -> bool:
|
|
95
|
+
"""Check execution status as fallback when streaming fails."""
|
|
96
|
+
import time
|
|
97
|
+
|
|
98
|
+
console.print("[dim]⏳ Checking execution status...[/dim]")
|
|
99
|
+
|
|
100
|
+
for _ in range(30): # Poll for up to 30 seconds
|
|
101
|
+
try:
|
|
102
|
+
response = httpx.get(
|
|
103
|
+
f"{config.base_url}/api/v2/external/execution/streaming/{execution_id}/status",
|
|
104
|
+
headers={"Authorization": f"Bearer {config.api_key}"},
|
|
105
|
+
timeout=10.0
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if response.status_code == 200:
|
|
109
|
+
data = response.json()
|
|
110
|
+
status = data.get('status', 'unknown')
|
|
111
|
+
|
|
112
|
+
if status == 'completed':
|
|
113
|
+
console.print(f"[green]✅ Execution completed successfully![/green]")
|
|
114
|
+
return True
|
|
115
|
+
elif status in ['failed_user', 'failed_system', 'failed']:
|
|
116
|
+
console.print(f"[red]❌ Execution failed: {status}[/red]")
|
|
117
|
+
errors = data.get('errors')
|
|
118
|
+
if errors:
|
|
119
|
+
console.print(f"[red]Error: {errors}[/red]")
|
|
120
|
+
return False
|
|
121
|
+
elif status in ['timeout', 'cancelled']:
|
|
122
|
+
console.print(f"[yellow]⚠️ Execution {status}[/yellow]")
|
|
123
|
+
return False
|
|
124
|
+
elif status in ['running', 'pending', 'queued']:
|
|
125
|
+
# Still running, continue polling
|
|
126
|
+
time.sleep(1)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
console.print(f"[red]Error checking status: {e}[/red]")
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
console.print("[yellow]⚠️ Status check timed out[/yellow]")
|
|
134
|
+
return False
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
lyceum/__init__.py
|
|
3
|
+
lyceum/main.py
|
|
4
|
+
lyceum/external/__init__.py
|
|
5
|
+
lyceum/external/auth/__init__.py
|
|
6
|
+
lyceum/external/auth/login.py
|
|
7
|
+
lyceum/external/compute/__init__.py
|
|
8
|
+
lyceum/external/compute/execution/__init__.py
|
|
9
|
+
lyceum/external/compute/execution/python.py
|
|
10
|
+
lyceum/external/compute/inference/__init__.py
|
|
11
|
+
lyceum/external/compute/inference/batch.py
|
|
12
|
+
lyceum/external/compute/inference/chat.py
|
|
13
|
+
lyceum/external/compute/inference/models.py
|
|
14
|
+
lyceum/external/general/__init__.py
|
|
15
|
+
lyceum/shared/__init__.py
|
|
16
|
+
lyceum/shared/config.py
|
|
17
|
+
lyceum/shared/display.py
|
|
18
|
+
lyceum/shared/streaming.py
|
|
19
|
+
lyceum_cli.egg-info/PKG-INFO
|
|
20
|
+
lyceum_cli.egg-info/SOURCES.txt
|
|
21
|
+
lyceum_cli.egg-info/dependency_links.txt
|
|
22
|
+
lyceum_cli.egg-info/entry_points.txt
|
|
23
|
+
lyceum_cli.egg-info/requires.txt
|
|
24
|
+
lyceum_cli.egg-info/top_level.txt
|
|
25
|
+
lyceum_cloud_execution_api_client/__init__.py
|
|
26
|
+
lyceum_cloud_execution_api_client/api/__init__.py
|
|
27
|
+
lyceum_cloud_execution_api_client/models/__init__.py
|
|
File without changes
|
|
File without changes
|
|
@@ -15,7 +15,7 @@ if readme_file.exists():
|
|
|
15
15
|
|
|
16
16
|
setup(
|
|
17
17
|
name="lyceum-cli",
|
|
18
|
-
version="1.0.
|
|
18
|
+
version="1.0.18",
|
|
19
19
|
description="Command-line interface for Lyceum Cloud Execution API",
|
|
20
20
|
long_description=long_description,
|
|
21
21
|
long_description_content_type="text/markdown",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
setup.py
|
|
2
|
-
lyceum/main.py
|
|
3
|
-
lyceum/external/auth/login.py
|
|
4
|
-
lyceum/external/compute/inference/__init__.py
|
|
5
|
-
lyceum/external/compute/inference/batch.py
|
|
6
|
-
lyceum/external/compute/inference/chat.py
|
|
7
|
-
lyceum/external/compute/inference/models.py
|
|
8
|
-
lyceum/shared/config.py
|
|
9
|
-
lyceum_cli.egg-info/PKG-INFO
|
|
10
|
-
lyceum_cli.egg-info/SOURCES.txt
|
|
11
|
-
lyceum_cli.egg-info/dependency_links.txt
|
|
12
|
-
lyceum_cli.egg-info/entry_points.txt
|
|
13
|
-
lyceum_cli.egg-info/requires.txt
|
|
14
|
-
lyceum_cli.egg-info/top_level.txt
|
|
15
|
-
lyceum_cloud_execution_api_client/models/__init__.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lyceum_cli-1.0.14 → lyceum_cli-1.0.18}/lyceum_cloud_execution_api_client/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|