lightning-sdk 0.2.15__py3-none-any.whl → 0.2.16__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.
- lightning_sdk/__init__.py +1 -1
- lightning_sdk/api/base_studio_api.py +7 -1
- lightning_sdk/api/cluster_api.py +83 -1
- lightning_sdk/api/llm_api.py +24 -5
- lightning_sdk/api/studio_api.py +3 -0
- lightning_sdk/api/teamspace_api.py +127 -1
- lightning_sdk/api/utils.py +4 -0
- lightning_sdk/base_studio.py +14 -1
- lightning_sdk/cli/create.py +21 -1
- lightning_sdk/cli/deploy/__init__.py +0 -0
- lightning_sdk/cli/deploy/_auth.py +189 -0
- lightning_sdk/cli/deploy/devbox.py +157 -0
- lightning_sdk/cli/{serve.py → deploy/serve.py} +11 -322
- lightning_sdk/cli/download.py +44 -16
- lightning_sdk/cli/entrypoint.py +1 -1
- lightning_sdk/cli/open.py +21 -2
- lightning_sdk/cli/start.py +12 -3
- lightning_sdk/cli/upload.py +2 -4
- lightning_sdk/lightning_cloud/openapi/__init__.py +18 -0
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +121 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +747 -105
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +93 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +18 -0
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/cloudspaces_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body.py +331 -0
- lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body1.py +305 -0
- lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/models_id_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_source_type.py +103 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_tagging_options.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_deployment_alerting_policy_response.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_event.py +487 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy.py +383 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_frequency.py +105 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_operation.py +105 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_severity.py +106 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_type.py +111 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_ge_list_deployment_routing_telemetry_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_instance_open_ports_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_deployment_routing_telemetry_content_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_organization_storage_metadata_response.py +331 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_events_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_policies_response.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_membership.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_project.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_project_membership.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_project_storage.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_routing_telemetry.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_sleep_server_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_user_requested_compute_config.py +27 -1
- lightning_sdk/llm/llm.py +52 -8
- lightning_sdk/studio.py +32 -1
- lightning_sdk/teamspace.py +68 -0
- {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/RECORD +74 -53
- {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import webbrowser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
11
|
+
from rich.prompt import Confirm
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
|
|
14
|
+
from lightning_sdk import Machine
|
|
15
|
+
from lightning_sdk.cli.deploy._auth import _AuthMode, _Onboarding, authenticate, poll_verified_status, select_teamspace
|
|
16
|
+
from lightning_sdk.cli.upload import (
|
|
17
|
+
_dump_current_upload_state,
|
|
18
|
+
_resolve_previous_upload_state,
|
|
19
|
+
_single_file_upload,
|
|
20
|
+
)
|
|
21
|
+
from lightning_sdk.lightning_cloud.openapi import V1CloudSpaceSourceType
|
|
22
|
+
from lightning_sdk.studio import Studio
|
|
23
|
+
from lightning_sdk.utils.resolve import _get_studio_url
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# TODO: Move the rest of the devbox logic here
|
|
27
|
+
class _LitServeDevbox:
|
|
28
|
+
"""Build LitServe API in a Studio."""
|
|
29
|
+
|
|
30
|
+
def resolve_previous_upload(self, studio: Studio, folder: str) -> Dict[str, str]:
|
|
31
|
+
remote_path = "."
|
|
32
|
+
pairs = {}
|
|
33
|
+
for root, _, files in os.walk(folder):
|
|
34
|
+
rel_root = os.path.relpath(root, folder)
|
|
35
|
+
for f in files:
|
|
36
|
+
pairs[os.path.join(root, f)] = os.path.join(remote_path, rel_root, f)
|
|
37
|
+
return _resolve_previous_upload_state(studio, remote_path, pairs)
|
|
38
|
+
|
|
39
|
+
def upload_folder(self, studio: Studio, folder: str, upload_state: Dict[str, str]) -> None:
|
|
40
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
41
|
+
futures = []
|
|
42
|
+
for k, v in upload_state.items():
|
|
43
|
+
futures.append(
|
|
44
|
+
executor.submit(_single_file_upload, studio=studio, local_path=k, remote_path=v, progress_bar=False)
|
|
45
|
+
)
|
|
46
|
+
total_files = len(upload_state)
|
|
47
|
+
|
|
48
|
+
with Progress(
|
|
49
|
+
SpinnerColumn(),
|
|
50
|
+
TextColumn("[progress.description]{task.description}"),
|
|
51
|
+
TimeElapsedColumn(),
|
|
52
|
+
console=Console(),
|
|
53
|
+
transient=True,
|
|
54
|
+
) as progress:
|
|
55
|
+
upload_task = progress.add_task(f"[cyan]Uploading {total_files} files to Studio...", total=total_files)
|
|
56
|
+
for f in concurrent.futures.as_completed(futures):
|
|
57
|
+
upload_state.pop(f.result())
|
|
58
|
+
_dump_current_upload_state(studio, ".", upload_state)
|
|
59
|
+
progress.update(upload_task, advance=1)
|
|
60
|
+
|
|
61
|
+
def _detect_port(self, script_path: Path) -> int:
|
|
62
|
+
with open(script_path) as f:
|
|
63
|
+
content = f.read()
|
|
64
|
+
|
|
65
|
+
# Try to match server.run first and then any variable name and then default port=8000
|
|
66
|
+
match = re.search(r"server\.run\s*\([^)]*port\s*=\s*(\d+)", content) or re.search(
|
|
67
|
+
r"\w+\.run\s*\([^)]*port\s*=\s*(\d+)", content
|
|
68
|
+
)
|
|
69
|
+
return int(match.group(1)) if match else 8000
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _handle_devbox(
|
|
73
|
+
name: str,
|
|
74
|
+
script_path: Path,
|
|
75
|
+
console: Console,
|
|
76
|
+
non_interactive: bool = False,
|
|
77
|
+
machine: Machine = Machine.CPU,
|
|
78
|
+
interruptible: bool = False,
|
|
79
|
+
teamspace: Optional[str] = None,
|
|
80
|
+
org: Optional[str] = None,
|
|
81
|
+
user: Optional[str] = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
if script_path.suffix != ".py":
|
|
84
|
+
console.print("❌ Error: Only Python files (.py) are supported for development servers", style="red")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
from_onboarding = False
|
|
88
|
+
authenticate(_AuthMode.DEVBOX, shall_confirm=not non_interactive)
|
|
89
|
+
user_status = poll_verified_status()
|
|
90
|
+
if not user_status["verified"]:
|
|
91
|
+
console.print("❌ Verify phone number to continue. Visit lightning.ai.", style="red")
|
|
92
|
+
return
|
|
93
|
+
if not user_status["onboarded"]:
|
|
94
|
+
console.print("onboarding user")
|
|
95
|
+
onboarding = _Onboarding(console)
|
|
96
|
+
resolved_teamspace = onboarding.select_teamspace(teamspace, org, user)
|
|
97
|
+
from_onboarding = True
|
|
98
|
+
else:
|
|
99
|
+
resolved_teamspace = select_teamspace(teamspace, org, user)
|
|
100
|
+
studio = Studio(name=name, teamspace=resolved_teamspace, source=V1CloudSpaceSourceType.LITSERVE)
|
|
101
|
+
studio.install_plugin("custom-port")
|
|
102
|
+
lit_devbox = _LitServeDevbox()
|
|
103
|
+
|
|
104
|
+
studio_url = _get_studio_url(studio, turn_on=True)
|
|
105
|
+
pathlib_path = Path(script_path).resolve()
|
|
106
|
+
browser_opened = False
|
|
107
|
+
studio_path = f"{studio.owner.name}/{studio.teamspace.name}/{studio.name}"
|
|
108
|
+
|
|
109
|
+
console.print("\n=== Lightning Studio Setup ===")
|
|
110
|
+
console.print(f"🔧 [bold]Setting up Studio:[/bold] {studio_path}")
|
|
111
|
+
console.print(f"📁 [bold]Local project:[/bold] {pathlib_path.parent}")
|
|
112
|
+
|
|
113
|
+
upload_state = lit_devbox.resolve_previous_upload(studio, str(pathlib_path.parent))
|
|
114
|
+
if non_interactive:
|
|
115
|
+
console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
116
|
+
browser_opened = webbrowser.open(studio_url)
|
|
117
|
+
elif not from_onboarding:
|
|
118
|
+
if Confirm.ask("Would you like to open your Studio in the browser?", default=True):
|
|
119
|
+
console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
120
|
+
browser_opened = webbrowser.open(studio_url)
|
|
121
|
+
|
|
122
|
+
if not browser_opened:
|
|
123
|
+
console.print(f"🔗 [bold]Access Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
124
|
+
|
|
125
|
+
# Start the Studio in the background and return immediately using threading
|
|
126
|
+
console.print("\n⚡ Initializing Studio in the background...")
|
|
127
|
+
studio_thread = Thread(target=studio.start, args=(machine, interruptible))
|
|
128
|
+
studio_thread.start()
|
|
129
|
+
|
|
130
|
+
console.print("📤 Syncing project files to Studio...")
|
|
131
|
+
lit_devbox.upload_folder(studio, pathlib_path.parent, upload_state)
|
|
132
|
+
|
|
133
|
+
# Wait for the Studio to start
|
|
134
|
+
console.print("⚡ Waiting for Studio to start...")
|
|
135
|
+
studio_thread.join()
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
console.print("🚀 Starting server...")
|
|
139
|
+
studio.run_and_detach(f"python {script_path}", timeout=10)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
console.print("❌ Error while starting server", style="red")
|
|
142
|
+
syntax = Syntax(f"{e}", "bash", theme="monokai")
|
|
143
|
+
console.print(syntax)
|
|
144
|
+
console.print(f"\n🔄 [bold]To fix:[/bold] Edit your code in Studio and run with: [u]python {script_path}[/u]")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
port = lit_devbox._detect_port(pathlib_path)
|
|
148
|
+
console.print("🔌 Configuring server port...")
|
|
149
|
+
port_url = studio.run_plugin("custom-port", port=port)
|
|
150
|
+
|
|
151
|
+
# Add completion message with next steps
|
|
152
|
+
console.print("\n✅ Studio ready!")
|
|
153
|
+
console.print("\n📋 [bold]Next steps:[/bold]")
|
|
154
|
+
console.print(" [bold]1.[/bold] Server code will be available in the Studio")
|
|
155
|
+
console.print(" [bold]2.[/bold] The Studio is now running with the specified configuration")
|
|
156
|
+
console.print(" [bold]3.[/bold] Modify and run your server directly in the Studio")
|
|
157
|
+
console.print(f" [bold]4.[/bold] Your server will be accessible on [link={port_url}]{port_url}[/link]")
|
|
@@ -1,44 +1,31 @@
|
|
|
1
|
-
import concurrent.futures
|
|
2
1
|
import os
|
|
3
|
-
import re
|
|
4
2
|
import socket
|
|
5
3
|
import subprocess
|
|
6
|
-
import time
|
|
7
4
|
import webbrowser
|
|
8
5
|
from datetime import datetime
|
|
9
|
-
from enum import Enum
|
|
10
6
|
from pathlib import Path
|
|
11
7
|
from threading import Thread
|
|
12
|
-
from typing import
|
|
13
|
-
from urllib.parse import urlencode
|
|
8
|
+
from typing import Optional, Union
|
|
14
9
|
|
|
15
10
|
import click
|
|
16
11
|
from rich.console import Console
|
|
17
12
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
18
13
|
from rich.prompt import Confirm
|
|
19
|
-
from rich.syntax import Syntax
|
|
20
14
|
|
|
21
15
|
from lightning_sdk import Machine, Teamspace
|
|
22
|
-
from lightning_sdk.api import UserApi
|
|
23
16
|
from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
24
17
|
from lightning_sdk.api.utils import _get_registry_url
|
|
25
|
-
from lightning_sdk.cli.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
from lightning_sdk.cli.deploy._auth import (
|
|
19
|
+
_AuthMode,
|
|
20
|
+
_Onboarding,
|
|
21
|
+
authenticate,
|
|
22
|
+
poll_verified_status,
|
|
23
|
+
select_teamspace,
|
|
30
24
|
)
|
|
31
|
-
from lightning_sdk.
|
|
32
|
-
from lightning_sdk.lightning_cloud.login import Auth, AuthServer
|
|
33
|
-
from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
|
|
34
|
-
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
25
|
+
from lightning_sdk.cli.deploy.devbox import _handle_devbox
|
|
35
26
|
from lightning_sdk.serve import _LitServeDeployer
|
|
36
|
-
from lightning_sdk.studio import Studio
|
|
37
|
-
from lightning_sdk.utils.resolve import _get_authed_user, _get_studio_url, _resolve_teamspace
|
|
38
27
|
|
|
39
28
|
_MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
|
|
40
|
-
_POLL_TIMEOUT = 600
|
|
41
|
-
LITSERVE_CODE = os.environ.get("LITSERVE_CODE", "j39bzk903h")
|
|
42
29
|
|
|
43
30
|
|
|
44
31
|
class _ServeGroup(click.Group):
|
|
@@ -219,7 +206,7 @@ def api_impl(
|
|
|
219
206
|
timestr = datetime.now().strftime("%b-%d-%H_%M")
|
|
220
207
|
name = f"litserve-{timestr}".lower()
|
|
221
208
|
|
|
222
|
-
if not cloud:
|
|
209
|
+
if not cloud and not devbox:
|
|
223
210
|
try:
|
|
224
211
|
subprocess.run(
|
|
225
212
|
["python", str(script_path)],
|
|
@@ -232,7 +219,8 @@ def api_impl(
|
|
|
232
219
|
raise RuntimeError(error_msg) from None
|
|
233
220
|
|
|
234
221
|
if devbox:
|
|
235
|
-
|
|
222
|
+
machine = Machine.from_str(devbox)
|
|
223
|
+
return _handle_devbox(name, script_path, console, non_interactive, machine, interruptible, teamspace, org, user)
|
|
236
224
|
|
|
237
225
|
machine = Machine.from_str(machine)
|
|
238
226
|
return _handle_cloud(
|
|
@@ -255,175 +243,6 @@ def api_impl(
|
|
|
255
243
|
)
|
|
256
244
|
|
|
257
245
|
|
|
258
|
-
class _AuthMode(Enum):
|
|
259
|
-
DEVBOX = "dev"
|
|
260
|
-
DEPLOY = "deploy"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
class _AuthServer(AuthServer):
|
|
264
|
-
def __init__(self, mode: _AuthMode, *args: Any, **kwargs: Any) -> None:
|
|
265
|
-
self._mode = mode
|
|
266
|
-
super().__init__(*args, **kwargs)
|
|
267
|
-
|
|
268
|
-
def get_auth_url(self, port: int) -> str:
|
|
269
|
-
redirect_uri = f"http://localhost:{port}/login-complete"
|
|
270
|
-
params = urlencode({"redirectTo": redirect_uri, "mode": self._mode.value, "okbhrt": LITSERVE_CODE})
|
|
271
|
-
return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class _Auth(Auth):
|
|
275
|
-
def __init__(self, mode: _AuthMode, shall_confirm: bool = False) -> None:
|
|
276
|
-
super().__init__()
|
|
277
|
-
self._mode = mode
|
|
278
|
-
self._shall_confirm = shall_confirm
|
|
279
|
-
|
|
280
|
-
def _run_server(self) -> None:
|
|
281
|
-
if self._shall_confirm:
|
|
282
|
-
proceed = Confirm.ask(
|
|
283
|
-
"Authenticating with Lightning AI. This will open a browser window. Continue?", default=True
|
|
284
|
-
)
|
|
285
|
-
if not proceed:
|
|
286
|
-
raise RuntimeError(
|
|
287
|
-
"Login cancelled. Please login to Lightning AI to deploy the API."
|
|
288
|
-
" Run `lightning login` to login."
|
|
289
|
-
) from None
|
|
290
|
-
print("Opening browser for authentication...")
|
|
291
|
-
print("Please come back to the terminal after logging in.")
|
|
292
|
-
time.sleep(3)
|
|
293
|
-
_AuthServer(self._mode).login_with_browser(self)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
|
|
297
|
-
auth = _Auth(mode, shall_confirm)
|
|
298
|
-
auth.authenticate()
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
|
|
302
|
-
if teamspace is None:
|
|
303
|
-
user = _get_authed_user()
|
|
304
|
-
menu = _TeamspacesMenu()
|
|
305
|
-
possible_teamspaces = menu._get_possible_teamspaces(user)
|
|
306
|
-
if len(possible_teamspaces) == 1:
|
|
307
|
-
name = next(iter(possible_teamspaces.values()))["name"]
|
|
308
|
-
return Teamspace(name=name, org=org, user=user)
|
|
309
|
-
|
|
310
|
-
return menu._resolve_teamspace(teamspace)
|
|
311
|
-
|
|
312
|
-
return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
class _UserStatus(TypedDict):
|
|
316
|
-
verified: bool
|
|
317
|
-
onboarded: bool
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def poll_verified_status(timeout: int = _POLL_TIMEOUT) -> _UserStatus:
|
|
321
|
-
"""Polls the verified status of the user until it is True or a timeout occurs."""
|
|
322
|
-
user_api = UserApi()
|
|
323
|
-
user = _get_authed_user()
|
|
324
|
-
start_time = datetime.now()
|
|
325
|
-
result = {"onboarded": False, "verified": False}
|
|
326
|
-
while True:
|
|
327
|
-
user_resp = user_api.get_user(name=user.name)
|
|
328
|
-
result["onboarded"] = user_resp.status.completed_project_onboarding
|
|
329
|
-
result["verified"] = user_resp.status.verified
|
|
330
|
-
if user_resp.status.verified:
|
|
331
|
-
return result
|
|
332
|
-
if (datetime.now() - start_time).total_seconds() > timeout:
|
|
333
|
-
break
|
|
334
|
-
time.sleep(5)
|
|
335
|
-
return result
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
class _OnboardingStatus(Enum):
|
|
339
|
-
NOT_VERIFIED = "not_verified"
|
|
340
|
-
ONBOARDING = "onboarding"
|
|
341
|
-
ONBOARDED = "onboarded"
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
class _Onboarding:
|
|
345
|
-
def __init__(self, console: Console) -> None:
|
|
346
|
-
self.console = console
|
|
347
|
-
self.user = _get_authed_user()
|
|
348
|
-
self.user_api = UserApi()
|
|
349
|
-
self.client = LightningClient(max_tries=7)
|
|
350
|
-
|
|
351
|
-
@property
|
|
352
|
-
def verified(self) -> bool:
|
|
353
|
-
return self.user_api.get_user(name=self.user.name).status.verified
|
|
354
|
-
|
|
355
|
-
@property
|
|
356
|
-
def is_onboarded(self) -> bool:
|
|
357
|
-
return self.user_api.get_user(name=self.user.name).status.completed_project_onboarding
|
|
358
|
-
|
|
359
|
-
@property
|
|
360
|
-
def can_join_org(self) -> bool:
|
|
361
|
-
return len(self.client.organizations_service_list_joinable_organizations().joinable_organizations) > 0
|
|
362
|
-
|
|
363
|
-
@property
|
|
364
|
-
def status(self) -> _OnboardingStatus:
|
|
365
|
-
if not self.verified:
|
|
366
|
-
return _OnboardingStatus.NOT_VERIFIED
|
|
367
|
-
if self.is_onboarded:
|
|
368
|
-
return _OnboardingStatus.ONBOARDED
|
|
369
|
-
return _OnboardingStatus.ONBOARDING
|
|
370
|
-
|
|
371
|
-
def _wait_user_onboarding(self, timeout: int = _POLL_TIMEOUT) -> None:
|
|
372
|
-
"""Wait for user onboarding if they can join the teamspace otherwise move to select a teamspace."""
|
|
373
|
-
status = self.status
|
|
374
|
-
if status == _OnboardingStatus.ONBOARDED:
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
self.console.print("Waiting for account setup. Visit lightning.ai")
|
|
378
|
-
start_time = datetime.now()
|
|
379
|
-
while self.status != _OnboardingStatus.ONBOARDED:
|
|
380
|
-
time.sleep(5)
|
|
381
|
-
if self.is_onboarded:
|
|
382
|
-
return
|
|
383
|
-
if (datetime.now() - start_time).total_seconds() > timeout:
|
|
384
|
-
break
|
|
385
|
-
|
|
386
|
-
raise RuntimeError("Timed out waiting for onboarding status")
|
|
387
|
-
|
|
388
|
-
def get_cloudspace_id(self, teamspace: Teamspace) -> Optional[str]:
|
|
389
|
-
cloudspaces: List[V1CloudSpace] = self.client.cloud_space_service_list_cloud_spaces(teamspace.id).cloudspaces
|
|
390
|
-
cloudspaces = sorted(cloudspaces, key=lambda cloudspace: cloudspace.created_at, reverse=True)
|
|
391
|
-
if len(cloudspaces) == 0:
|
|
392
|
-
raise RuntimeError("Error creating deployment! Finish account setup at lightning.ai first.")
|
|
393
|
-
# get the first cloudspace
|
|
394
|
-
cloudspace = cloudspaces[0]
|
|
395
|
-
if "scratch-studio" in cloudspace.name or "scratch-studio" in cloudspace.display_name:
|
|
396
|
-
return cloudspace.id
|
|
397
|
-
return None
|
|
398
|
-
|
|
399
|
-
def select_teamspace(self, teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
|
|
400
|
-
"""Select a teamspace while onboarding.
|
|
401
|
-
|
|
402
|
-
If user is being onboarded and can't join any org, the teamspace it will be resolved to the default
|
|
403
|
-
personal teamspace.
|
|
404
|
-
If user is being onboarded and can join an org then it will select default teamspace from the org.
|
|
405
|
-
"""
|
|
406
|
-
if self.is_onboarded:
|
|
407
|
-
return select_teamspace(teamspace, org, user)
|
|
408
|
-
|
|
409
|
-
# Run only when user hasn't completed onboarding yet.
|
|
410
|
-
menu = _TeamspacesMenu()
|
|
411
|
-
self._wait_user_onboarding()
|
|
412
|
-
# Onboarding has been completed - user already selected organization if they could
|
|
413
|
-
possible_teamspaces = menu._get_possible_teamspaces(self.user)
|
|
414
|
-
if len(possible_teamspaces) == 1:
|
|
415
|
-
# User didn't select any org
|
|
416
|
-
value = next(iter(possible_teamspaces.values()))
|
|
417
|
-
return Teamspace(name=value["name"], org=value["org"], user=value["user"])
|
|
418
|
-
|
|
419
|
-
for _, value in possible_teamspaces.items():
|
|
420
|
-
# User select an org
|
|
421
|
-
# Onboarding teamspace will be the default teamspace in the selected org
|
|
422
|
-
if value["org"]:
|
|
423
|
-
return Teamspace(name=value["name"], org=value["org"], user=value["user"])
|
|
424
|
-
raise RuntimeError("Unable to select teamspace. Visit lightning.ai")
|
|
425
|
-
|
|
426
|
-
|
|
427
246
|
def is_connected(host: str = "8.8.8.8", port: int = 53, timeout: int = 10) -> bool:
|
|
428
247
|
try:
|
|
429
248
|
socket.setdefaulttimeout(timeout)
|
|
@@ -465,136 +284,6 @@ def _upload_container(
|
|
|
465
284
|
return True
|
|
466
285
|
|
|
467
286
|
|
|
468
|
-
# TODO: Move the rest of the devbox logic here
|
|
469
|
-
class _LitServeDevbox:
|
|
470
|
-
"""Build LitServe API in a Studio."""
|
|
471
|
-
|
|
472
|
-
def resolve_previous_upload(self, studio: Studio, folder: str) -> Dict[str, str]:
|
|
473
|
-
remote_path = "."
|
|
474
|
-
pairs = {}
|
|
475
|
-
for root, _, files in os.walk(folder):
|
|
476
|
-
rel_root = os.path.relpath(root, folder)
|
|
477
|
-
for f in files:
|
|
478
|
-
pairs[os.path.join(root, f)] = os.path.join(remote_path, rel_root, f)
|
|
479
|
-
return _resolve_previous_upload_state(studio, remote_path, pairs)
|
|
480
|
-
|
|
481
|
-
def upload_folder(self, studio: Studio, folder: str, upload_state: Dict[str, str]) -> None:
|
|
482
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
483
|
-
futures = _start_parallel_upload(executor, studio, upload_state)
|
|
484
|
-
total_files = len(upload_state)
|
|
485
|
-
|
|
486
|
-
with Progress(
|
|
487
|
-
SpinnerColumn(),
|
|
488
|
-
TextColumn("[progress.description]{task.description}"),
|
|
489
|
-
TimeElapsedColumn(),
|
|
490
|
-
console=Console(),
|
|
491
|
-
transient=True,
|
|
492
|
-
) as progress:
|
|
493
|
-
upload_task = progress.add_task(f"[cyan]Uploading {total_files} files to Studio...", total=total_files)
|
|
494
|
-
for f in concurrent.futures.as_completed(futures):
|
|
495
|
-
upload_state.pop(f.result())
|
|
496
|
-
_dump_current_upload_state(studio, ".", upload_state)
|
|
497
|
-
progress.update(upload_task, advance=1)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
def _detect_port(script_path: Path) -> int:
|
|
501
|
-
with open(script_path) as f:
|
|
502
|
-
content = f.read()
|
|
503
|
-
|
|
504
|
-
# Try to match server.run first and then any variable name and then default port=8000
|
|
505
|
-
match = re.search(r"server\.run\s*\([^)]*port\s*=\s*(\d+)", content) or re.search(
|
|
506
|
-
r"\w+\.run\s*\([^)]*port\s*=\s*(\d+)", content
|
|
507
|
-
)
|
|
508
|
-
return int(match.group(1)) if match else 8000
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def _handle_devbox(
|
|
512
|
-
name: str,
|
|
513
|
-
script_path: Path,
|
|
514
|
-
console: Console,
|
|
515
|
-
non_interactive: bool = False,
|
|
516
|
-
devbox: Union[Machine, str] = Machine.CPU,
|
|
517
|
-
interruptible: bool = False,
|
|
518
|
-
teamspace: Optional[str] = None,
|
|
519
|
-
org: Optional[str] = None,
|
|
520
|
-
user: Optional[str] = None,
|
|
521
|
-
) -> None:
|
|
522
|
-
if script_path.suffix != ".py":
|
|
523
|
-
console.print("❌ Error: Only Python files (.py) are supported for development servers", style="red")
|
|
524
|
-
return
|
|
525
|
-
|
|
526
|
-
authenticate(_AuthMode.DEVBOX, shall_confirm=not non_interactive)
|
|
527
|
-
user_status = poll_verified_status()
|
|
528
|
-
if not user_status["verified"]:
|
|
529
|
-
console.print("❌ Verify phone number to continue. Visit lightning.ai.", style="red")
|
|
530
|
-
return
|
|
531
|
-
if not user_status["onboarded"]:
|
|
532
|
-
console.print("onboarding user")
|
|
533
|
-
onboarding = _Onboarding(console)
|
|
534
|
-
resolved_teamspace = onboarding.select_teamspace(teamspace, org, user)
|
|
535
|
-
else:
|
|
536
|
-
resolved_teamspace = select_teamspace(teamspace, org, user)
|
|
537
|
-
studio = Studio(name=name, teamspace=resolved_teamspace)
|
|
538
|
-
studio.install_plugin("custom-port")
|
|
539
|
-
lit_devbox = _LitServeDevbox()
|
|
540
|
-
|
|
541
|
-
studio_url = _get_studio_url(studio, turn_on=True)
|
|
542
|
-
pathlib_path = Path(script_path).resolve()
|
|
543
|
-
browser_opened = False
|
|
544
|
-
studio_path = f"{studio.owner.name}/{studio.teamspace.name}/{studio.name}"
|
|
545
|
-
|
|
546
|
-
console.print("\n=== Lightning Studio Setup ===")
|
|
547
|
-
console.print(f"🔧 [bold]Setting up Studio:[/bold] {studio_path}")
|
|
548
|
-
console.print(f"📁 [bold]Local project:[/bold] {pathlib_path.parent}")
|
|
549
|
-
|
|
550
|
-
upload_state = lit_devbox.resolve_previous_upload(studio, str(pathlib_path.parent))
|
|
551
|
-
if non_interactive:
|
|
552
|
-
console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
553
|
-
browser_opened = webbrowser.open(studio_url)
|
|
554
|
-
else:
|
|
555
|
-
if Confirm.ask("Would you like to open your Studio in the browser?", default=True):
|
|
556
|
-
console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
557
|
-
browser_opened = webbrowser.open(studio_url)
|
|
558
|
-
|
|
559
|
-
if not browser_opened:
|
|
560
|
-
console.print(f"🔗 [bold]Access Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
561
|
-
|
|
562
|
-
# Start the Studio in the background and return immediately using threading
|
|
563
|
-
console.print("\n⚡ Initializing Studio in the background...")
|
|
564
|
-
studio_thread = Thread(target=studio.start, args=(devbox, interruptible))
|
|
565
|
-
studio_thread.start()
|
|
566
|
-
|
|
567
|
-
console.print("📤 Syncing project files to Studio...")
|
|
568
|
-
lit_devbox.upload_folder(studio, pathlib_path.parent, upload_state)
|
|
569
|
-
|
|
570
|
-
# Wait for the Studio to start
|
|
571
|
-
console.print("⚡ Waiting for Studio to start...")
|
|
572
|
-
studio_thread.join()
|
|
573
|
-
|
|
574
|
-
try:
|
|
575
|
-
console.print("🚀 Starting server...")
|
|
576
|
-
studio.run_and_detach(f"python {script_path}", timeout=10)
|
|
577
|
-
except Exception as e:
|
|
578
|
-
console.print("❌ Error while starting server", style="red")
|
|
579
|
-
syntax = Syntax(f"{e}", "bash", theme="monokai")
|
|
580
|
-
console.print(syntax)
|
|
581
|
-
console.print(f"\n🔄 [bold]To fix:[/bold] Edit your code in Studio and run with: [u]python {script_path}[/u]")
|
|
582
|
-
return
|
|
583
|
-
|
|
584
|
-
port = _detect_port(pathlib_path)
|
|
585
|
-
console.print("🔌 Configuring server port...")
|
|
586
|
-
port_url = studio.run_plugin("custom-port", port=port)
|
|
587
|
-
|
|
588
|
-
# Add completion message with next steps
|
|
589
|
-
console.print("\n✅ Studio ready!")
|
|
590
|
-
console.print("\n📋 [bold]Next steps:[/bold]")
|
|
591
|
-
console.print(" [bold]1.[/bold] Server code will be available in the Studio")
|
|
592
|
-
console.print(" [bold]2.[/bold] The Studio is now running with the specified configuration")
|
|
593
|
-
console.print(" [bold]3.[/bold] Modify and run your server directly in the Studio")
|
|
594
|
-
console.print(f" [bold]4.[/bold] Your server will be accessible on [link={port_url}]{port_url}[/link]")
|
|
595
|
-
# TODO: Once server running is implemented
|
|
596
|
-
|
|
597
|
-
|
|
598
287
|
def _handle_cloud(
|
|
599
288
|
script_path: Union[str, Path],
|
|
600
289
|
console: Console,
|
lightning_sdk/cli/download.py
CHANGED
|
@@ -48,13 +48,18 @@ def model(name: str, download_dir: str = ".") -> None:
|
|
|
48
48
|
"--studio",
|
|
49
49
|
default=None,
|
|
50
50
|
help=(
|
|
51
|
-
"The name of the studio to
|
|
51
|
+
"The name of the studio to download from. "
|
|
52
52
|
"Will show a menu with user's owned studios for selection if not specified. "
|
|
53
53
|
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
|
|
54
54
|
"The teamspace and studio names can be regular expressions to match, "
|
|
55
55
|
"a menu filtered studios will be shown for final selection."
|
|
56
56
|
),
|
|
57
57
|
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--teamspace",
|
|
60
|
+
default=None,
|
|
61
|
+
help="The teamspace the drive folder is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
|
|
62
|
+
)
|
|
58
63
|
@click.option(
|
|
59
64
|
"--local-path",
|
|
60
65
|
"--local_path",
|
|
@@ -62,32 +67,41 @@ def model(name: str, download_dir: str = ".") -> None:
|
|
|
62
67
|
type=click.Path(file_okay=False, dir_okay=True),
|
|
63
68
|
help="The path to the directory you want to download the folder to.",
|
|
64
69
|
)
|
|
65
|
-
def folder(
|
|
66
|
-
""
|
|
70
|
+
def folder(
|
|
71
|
+
path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = "."
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Download a folder from a Studio or a Teamspace drive folder.
|
|
67
74
|
|
|
68
75
|
Example:
|
|
69
76
|
lightning download folder PATH
|
|
70
77
|
|
|
71
|
-
PATH: The relative path within the Studio you want to download.
|
|
72
|
-
Defaults to the entire
|
|
78
|
+
PATH: The relative path within the Studio or drive folder you want to download.
|
|
79
|
+
Defaults to the entire Studio or drive folder.
|
|
73
80
|
"""
|
|
74
81
|
local_path = Path(local_path)
|
|
75
82
|
if not local_path.is_dir():
|
|
76
83
|
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
if studio and teamspace:
|
|
86
|
+
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
87
|
+
|
|
88
|
+
if studio:
|
|
89
|
+
resolved_downloader = _resolve_studio(studio)
|
|
90
|
+
elif teamspace:
|
|
91
|
+
menu = _TeamspacesMenu()
|
|
92
|
+
resolved_downloader = menu._resolve_teamspace(teamspace)
|
|
79
93
|
|
|
80
94
|
if not path:
|
|
81
|
-
local_path /=
|
|
95
|
+
local_path /= resolved_downloader.name
|
|
82
96
|
path = ""
|
|
83
97
|
|
|
84
98
|
try:
|
|
85
99
|
if not path:
|
|
86
100
|
raise FileNotFoundError()
|
|
87
|
-
|
|
101
|
+
resolved_downloader.download_folder(remote_path=path, target_path=str(local_path))
|
|
88
102
|
except Exception as e:
|
|
89
103
|
raise StudioCliError(
|
|
90
|
-
f"Could not download the folder from the given Studio {studio}. "
|
|
104
|
+
f"Could not download the folder from the given Studio {studio} or Teamspace {teamspace}. "
|
|
91
105
|
"Please contact Lightning AI directly to resolve this issue."
|
|
92
106
|
) from e
|
|
93
107
|
|
|
@@ -105,6 +119,11 @@ def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".")
|
|
|
105
119
|
"a menu filtered studios will be shown for final selection."
|
|
106
120
|
),
|
|
107
121
|
)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--teamspace",
|
|
124
|
+
default=None,
|
|
125
|
+
help="The teamspace the file is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
|
|
126
|
+
)
|
|
108
127
|
@click.option(
|
|
109
128
|
"--local-path",
|
|
110
129
|
"--local_path",
|
|
@@ -112,31 +131,40 @@ def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".")
|
|
|
112
131
|
type=click.Path(file_okay=False, dir_okay=True),
|
|
113
132
|
help="The path to the directory you want to download the file to.",
|
|
114
133
|
)
|
|
115
|
-
def file(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
116
|
-
"""Download a file from a Studio.
|
|
134
|
+
def file(path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = ".") -> None:
|
|
135
|
+
"""Download a file from a Studio or Teamspace drive file.
|
|
117
136
|
|
|
118
137
|
Example:
|
|
119
138
|
lightning download file PATH
|
|
120
139
|
|
|
121
|
-
PATH: The relative path to the file within the Studio you want to download.
|
|
140
|
+
PATH: The relative path to the file within the Studio or Teamspace drive file you want to download.
|
|
122
141
|
"""
|
|
123
142
|
local_path = Path(local_path)
|
|
124
143
|
if not local_path.is_dir():
|
|
125
144
|
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
126
145
|
|
|
127
|
-
|
|
146
|
+
if studio and teamspace:
|
|
147
|
+
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
148
|
+
|
|
149
|
+
if studio:
|
|
150
|
+
resolved_downloader = _resolve_studio(studio)
|
|
151
|
+
elif teamspace:
|
|
152
|
+
menu = _TeamspacesMenu()
|
|
153
|
+
resolved_downloader = menu._resolve_teamspace(teamspace)
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError("Either --studio or --teamspace must be provided")
|
|
128
156
|
|
|
129
157
|
if not path:
|
|
130
|
-
local_path /=
|
|
158
|
+
local_path /= resolved_downloader.name
|
|
131
159
|
path = ""
|
|
132
160
|
|
|
133
161
|
try:
|
|
134
162
|
if not path:
|
|
135
163
|
raise FileNotFoundError()
|
|
136
|
-
|
|
164
|
+
resolved_downloader.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
|
|
137
165
|
except Exception as e:
|
|
138
166
|
raise StudioCliError(
|
|
139
|
-
f"Could not download the file from the given Studio {studio}. "
|
|
167
|
+
f"Could not download the file from the given Studio {studio} or Teamspace {teamspace}. "
|
|
140
168
|
"Please contact Lightning AI directly to resolve this issue."
|
|
141
169
|
) from e
|
|
142
170
|
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -14,6 +14,7 @@ from lightning_sdk.cli.configure import configure
|
|
|
14
14
|
from lightning_sdk.cli.connect import connect
|
|
15
15
|
from lightning_sdk.cli.create import create
|
|
16
16
|
from lightning_sdk.cli.delete import delete
|
|
17
|
+
from lightning_sdk.cli.deploy.serve import deploy
|
|
17
18
|
from lightning_sdk.cli.docker_cli import dockerize
|
|
18
19
|
from lightning_sdk.cli.download import download
|
|
19
20
|
from lightning_sdk.cli.generate import generate
|
|
@@ -21,7 +22,6 @@ from lightning_sdk.cli.inspect import inspect
|
|
|
21
22
|
from lightning_sdk.cli.list import list_cli
|
|
22
23
|
from lightning_sdk.cli.open import open
|
|
23
24
|
from lightning_sdk.cli.run import run
|
|
24
|
-
from lightning_sdk.cli.serve import deploy
|
|
25
25
|
from lightning_sdk.cli.start import start
|
|
26
26
|
from lightning_sdk.cli.stop import stop
|
|
27
27
|
from lightning_sdk.cli.switch import switch
|