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.
Files changed (74) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/base_studio_api.py +7 -1
  3. lightning_sdk/api/cluster_api.py +83 -1
  4. lightning_sdk/api/llm_api.py +24 -5
  5. lightning_sdk/api/studio_api.py +3 -0
  6. lightning_sdk/api/teamspace_api.py +127 -1
  7. lightning_sdk/api/utils.py +4 -0
  8. lightning_sdk/base_studio.py +14 -1
  9. lightning_sdk/cli/create.py +21 -1
  10. lightning_sdk/cli/deploy/__init__.py +0 -0
  11. lightning_sdk/cli/deploy/_auth.py +189 -0
  12. lightning_sdk/cli/deploy/devbox.py +157 -0
  13. lightning_sdk/cli/{serve.py → deploy/serve.py} +11 -322
  14. lightning_sdk/cli/download.py +44 -16
  15. lightning_sdk/cli/entrypoint.py +1 -1
  16. lightning_sdk/cli/open.py +21 -2
  17. lightning_sdk/cli/start.py +12 -3
  18. lightning_sdk/cli/upload.py +2 -4
  19. lightning_sdk/lightning_cloud/openapi/__init__.py +18 -0
  20. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +121 -0
  21. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +105 -0
  22. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
  23. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +747 -105
  24. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +93 -0
  25. lightning_sdk/lightning_cloud/openapi/models/__init__.py +18 -0
  26. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +27 -1
  27. lightning_sdk/lightning_cloud/openapi/models/cloudspaces_id_body.py +53 -1
  28. lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body.py +331 -0
  29. lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body1.py +305 -0
  30. lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +53 -1
  31. lightning_sdk/lightning_cloud/openapi/models/models_id_body.py +123 -0
  32. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +105 -1
  33. lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +29 -3
  35. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +53 -1
  36. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_source_type.py +103 -0
  37. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_tagging_options.py +27 -1
  38. lightning_sdk/lightning_cloud/openapi/models/v1_delete_deployment_alerting_policy_response.py +175 -0
  39. lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +53 -1
  40. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_event.py +487 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy.py +383 -0
  42. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_frequency.py +105 -0
  43. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_operation.py +105 -0
  44. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_severity.py +106 -0
  45. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_type.py +111 -0
  46. lightning_sdk/lightning_cloud/openapi/models/v1_ge_list_deployment_routing_telemetry_response.py +27 -1
  47. lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_instance_open_ports_response.py +123 -0
  48. lightning_sdk/lightning_cloud/openapi/models/v1_get_deployment_routing_telemetry_content_response.py +123 -0
  49. lightning_sdk/lightning_cloud/openapi/models/v1_get_organization_storage_metadata_response.py +331 -0
  50. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +1 -27
  51. lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +27 -1
  52. lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_events_response.py +123 -0
  53. lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_policies_response.py +175 -0
  54. lightning_sdk/lightning_cloud/openapi/models/v1_membership.py +27 -1
  55. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +105 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_project.py +27 -1
  57. lightning_sdk/lightning_cloud/openapi/models/v1_project_membership.py +27 -1
  58. lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +29 -3
  59. lightning_sdk/lightning_cloud/openapi/models/v1_project_storage.py +53 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_routing_telemetry.py +253 -0
  61. lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_type.py +1 -0
  62. lightning_sdk/lightning_cloud/openapi/models/v1_sleep_server_response.py +97 -0
  63. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +1 -27
  64. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -27
  65. lightning_sdk/lightning_cloud/openapi/models/v1_user_requested_compute_config.py +27 -1
  66. lightning_sdk/llm/llm.py +52 -8
  67. lightning_sdk/studio.py +32 -1
  68. lightning_sdk/teamspace.py +68 -0
  69. {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/METADATA +1 -1
  70. {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/RECORD +74 -53
  71. {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/LICENSE +0 -0
  72. {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/WHEEL +0 -0
  73. {lightning_sdk-0.2.15.dist-info → lightning_sdk-0.2.16.dist-info}/entry_points.txt +0 -0
  74. {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 Any, Dict, List, Optional, TypedDict, Union
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.teamspace_menu import _TeamspacesMenu
26
- from lightning_sdk.cli.upload import (
27
- _dump_current_upload_state,
28
- _resolve_previous_upload_state,
29
- _start_parallel_upload,
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.lightning_cloud import env
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
- return _handle_devbox(name, script_path, console, non_interactive, devbox, interruptible, teamspace, org, user)
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,
@@ -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 upload 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(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
66
- """Download a folder from a Studio.
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 studio.
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
- resolved_studio = _resolve_studio(studio)
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 /= resolved_studio.name
95
+ local_path /= resolved_downloader.name
82
96
  path = ""
83
97
 
84
98
  try:
85
99
  if not path:
86
100
  raise FileNotFoundError()
87
- resolved_studio.download_folder(remote_path=path, target_path=str(local_path))
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
- resolved_studio = _resolve_studio(studio)
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 /= resolved_studio.name
158
+ local_path /= resolved_downloader.name
131
159
  path = ""
132
160
 
133
161
  try:
134
162
  if not path:
135
163
  raise FileNotFoundError()
136
- resolved_studio.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
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
 
@@ -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