lightning-sdk 0.2.14__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 (122) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/base_studio_api.py +79 -0
  3. lightning_sdk/api/cluster_api.py +83 -1
  4. lightning_sdk/api/license_api.py +48 -0
  5. lightning_sdk/api/llm_api.py +73 -12
  6. lightning_sdk/api/studio_api.py +50 -1
  7. lightning_sdk/api/teamspace_api.py +127 -1
  8. lightning_sdk/api/utils.py +4 -0
  9. lightning_sdk/base_studio.py +83 -0
  10. lightning_sdk/cli/create.py +21 -1
  11. lightning_sdk/cli/delete.py +6 -8
  12. lightning_sdk/cli/deploy/__init__.py +0 -0
  13. lightning_sdk/cli/deploy/_auth.py +189 -0
  14. lightning_sdk/cli/deploy/devbox.py +157 -0
  15. lightning_sdk/cli/{serve.py → deploy/serve.py} +22 -281
  16. lightning_sdk/cli/download.py +69 -16
  17. lightning_sdk/cli/entrypoint.py +1 -1
  18. lightning_sdk/cli/open.py +21 -2
  19. lightning_sdk/cli/start.py +12 -3
  20. lightning_sdk/cli/teamspace_menu.py +9 -1
  21. lightning_sdk/cli/upload.py +2 -5
  22. lightning_sdk/lightning_cloud/openapi/__init__.py +29 -0
  23. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  24. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +121 -0
  25. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +9 -1
  26. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +226 -0
  27. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
  28. lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +178 -0
  29. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +984 -101
  30. lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +525 -0
  31. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +93 -0
  32. lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
  33. lightning_sdk/lightning_cloud/openapi/models/__init__.py +28 -0
  34. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +79 -1
  35. lightning_sdk/lightning_cloud/openapi/models/cloudspaces_id_body.py +53 -1
  36. lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body.py +331 -0
  37. lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body1.py +305 -0
  38. lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +53 -1
  39. lightning_sdk/lightning_cloud/openapi/models/endpoints_id_body.py +27 -1
  40. lightning_sdk/lightning_cloud/openapi/models/model_id_versions_body.py +27 -1
  41. lightning_sdk/lightning_cloud/openapi/models/models_id_body.py +123 -0
  42. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +183 -1
  43. lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +6 -6
  44. lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +27 -1
  45. lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
  46. lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +107 -3
  47. lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
  48. lightning_sdk/lightning_cloud/openapi/models/update.py +79 -1
  49. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +55 -3
  50. lightning_sdk/lightning_cloud/openapi/models/v1_aws_direct_v1.py +53 -1
  51. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +3 -0
  52. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +79 -1
  53. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_config.py +123 -0
  54. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +79 -1
  55. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +104 -0
  56. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_source_type.py +103 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_cloudflare_v1.py +66 -66
  58. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  59. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_tagging_options.py +27 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_upload.py +149 -0
  61. lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload.py +55 -3
  62. lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +79 -1
  64. lightning_sdk/lightning_cloud/openapi/models/v1_delete_deployment_alerting_policy_response.py +175 -0
  65. lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +53 -1
  66. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_event.py +487 -0
  67. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy.py +383 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_frequency.py +105 -0
  69. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_operation.py +105 -0
  70. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_severity.py +106 -0
  71. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_type.py +111 -0
  72. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_api.py +27 -1
  73. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +4 -4
  74. lightning_sdk/lightning_cloud/openapi/models/v1_endpoint.py +27 -1
  75. lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
  76. lightning_sdk/lightning_cloud/openapi/models/v1_ge_list_deployment_routing_telemetry_response.py +123 -0
  77. lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_instance_open_ports_response.py +123 -0
  78. lightning_sdk/lightning_cloud/openapi/models/v1_get_deployment_routing_telemetry_content_response.py +123 -0
  79. lightning_sdk/lightning_cloud/openapi/models/v1_get_job_stats_response.py +53 -1
  80. lightning_sdk/lightning_cloud/openapi/models/v1_get_organization_storage_metadata_response.py +331 -0
  81. lightning_sdk/lightning_cloud/openapi/models/v1_get_project_balance_response.py +1 -27
  82. lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +27 -1
  83. lightning_sdk/lightning_cloud/openapi/models/v1_job_type.py +1 -0
  84. lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_events_response.py +123 -0
  85. lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_policies_response.py +175 -0
  86. lightning_sdk/lightning_cloud/openapi/models/v1_list_product_licenses_response.py +123 -0
  87. lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +27 -1
  88. lightning_sdk/lightning_cloud/openapi/models/v1_membership.py +43 -17
  89. lightning_sdk/lightning_cloud/openapi/models/v1_modify_filesystem_volume_response.py +97 -0
  90. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +183 -1
  91. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +6 -6
  92. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_state.py +111 -0
  93. lightning_sdk/lightning_cloud/openapi/models/v1_presigned_url.py +53 -1
  94. lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +409 -0
  95. lightning_sdk/lightning_cloud/openapi/models/v1_product_license_check_response.py +123 -0
  96. lightning_sdk/lightning_cloud/openapi/models/v1_project.py +27 -1
  97. lightning_sdk/lightning_cloud/openapi/models/v1_project_membership.py +43 -17
  98. lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +107 -3
  99. lightning_sdk/lightning_cloud/openapi/models/v1_project_storage.py +53 -1
  100. lightning_sdk/lightning_cloud/openapi/models/v1_r2_data_connection.py +53 -1
  101. lightning_sdk/lightning_cloud/openapi/models/v1_routing_telemetry.py +253 -0
  102. lightning_sdk/lightning_cloud/openapi/models/v1_secret_type.py +1 -0
  103. lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_type.py +2 -0
  104. lightning_sdk/lightning_cloud/openapi/models/v1_sleep_server_response.py +97 -0
  105. lightning_sdk/lightning_cloud/openapi/models/v1_trigger_filesystem_upgrade_response.py +123 -0
  106. lightning_sdk/lightning_cloud/openapi/models/v1_upload_project_artifact_response.py +27 -1
  107. lightning_sdk/lightning_cloud/openapi/models/v1_usage_report.py +79 -1
  108. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +347 -113
  109. lightning_sdk/lightning_cloud/openapi/models/v1_user_requested_compute_config.py +27 -1
  110. lightning_sdk/lightning_cloud/rest_client.py +4 -0
  111. lightning_sdk/llm/llm.py +132 -40
  112. lightning_sdk/services/__init__.py +1 -1
  113. lightning_sdk/services/license.py +236 -0
  114. lightning_sdk/studio.py +62 -1
  115. lightning_sdk/teamspace.py +68 -0
  116. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/METADATA +1 -1
  117. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/RECORD +122 -86
  118. /lightning_sdk/services/{finetune/__init__.py → finetune_llm.py} +0 -0
  119. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/LICENSE +0 -0
  120. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/WHEEL +0 -0
  121. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/entry_points.txt +0 -0
  122. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,11 @@
1
- import concurrent.futures
2
1
  import os
3
2
  import socket
4
3
  import subprocess
5
- import time
6
4
  import webbrowser
7
5
  from datetime import datetime
8
- from enum import Enum
9
6
  from pathlib import Path
10
7
  from threading import Thread
11
- from typing import Dict, List, Optional, TypedDict, Union
12
- from urllib.parse import urlencode
8
+ from typing import Optional, Union
13
9
 
14
10
  import click
15
11
  from rich.console import Console
@@ -17,26 +13,19 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
17
13
  from rich.prompt import Confirm
18
14
 
19
15
  from lightning_sdk import Machine, Teamspace
20
- from lightning_sdk.api import UserApi
21
16
  from lightning_sdk.api.lit_container_api import LitContainerApi
22
17
  from lightning_sdk.api.utils import _get_registry_url
23
- from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
24
- from lightning_sdk.cli.upload import (
25
- _dump_current_upload_state,
26
- _resolve_previous_upload_state,
27
- _start_parallel_upload,
18
+ from lightning_sdk.cli.deploy._auth import (
19
+ _AuthMode,
20
+ _Onboarding,
21
+ authenticate,
22
+ poll_verified_status,
23
+ select_teamspace,
28
24
  )
29
- from lightning_sdk.lightning_cloud import env
30
- from lightning_sdk.lightning_cloud.login import Auth, AuthServer
31
- from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
32
- from lightning_sdk.lightning_cloud.rest_client import LightningClient
25
+ from lightning_sdk.cli.deploy.devbox import _handle_devbox
33
26
  from lightning_sdk.serve import _LitServeDeployer
34
- from lightning_sdk.studio import Studio
35
- from lightning_sdk.utils.resolve import _get_authed_user, _get_studio_url, _resolve_teamspace
36
27
 
37
28
  _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
38
- _POLL_TIMEOUT = 600
39
- LITSERVE_CODE = os.environ.get("LITSERVE_CODE", "j39bzk903h")
40
29
 
41
30
 
42
31
  class _ServeGroup(click.Group):
@@ -53,12 +42,12 @@ def deploy() -> None:
53
42
  """Deploy a LitServe model.
54
43
 
55
44
  Example:
56
- lightning deploy server.py # deploy to the cloud
45
+ lightning deploy server.py --cloud # deploy to the cloud
57
46
 
58
47
  Example:
59
- lightning deploy server.py --local # run locally
48
+ lightning deploy server.py # run locally
60
49
 
61
- You can deploy the API to the cloud by running `lightning deploy server.py`.
50
+ You can deploy the API to the cloud by running `lightning deploy server.py --cloud`.
62
51
  This will build a docker container for the server.py script and deploy it to the Lightning AI platform.
63
52
  """
64
53
 
@@ -73,11 +62,11 @@ def deploy() -> None:
73
62
  help="Generate a client for the model",
74
63
  )
75
64
  @click.option(
76
- "--local",
65
+ "--cloud",
77
66
  is_flag=True,
78
67
  default=False,
79
68
  flag_value=True,
80
- help="Run the model locally",
69
+ help="Run the model on cloud",
81
70
  )
82
71
  @click.option("--name", default=None, help="Name of the deployed API (e.g., 'classification-api', 'Llama-api')")
83
72
  @click.option(
@@ -133,7 +122,7 @@ def deploy() -> None:
133
122
  @click.option("--port", default=8000, help="The port to expose the API on.")
134
123
  @click.option("--min_replica", "--min-replica", default=0, help="Number of replicas to start with.")
135
124
  @click.option("--max_replica", "--max-replica", default=1, help="Number of replicas to scale up to.")
136
- @click.option("--replicas", "--replicas", default=1, help="Deployment will start with this many replicas.")
125
+ @click.option("--replicas", default=1, help="Deployment will start with this many replicas.")
137
126
  @click.option(
138
127
  "--no_credentials",
139
128
  "--no-credentials",
@@ -145,7 +134,7 @@ def deploy() -> None:
145
134
  def api(
146
135
  script_path: str,
147
136
  easy: bool,
148
- local: bool,
137
+ cloud: bool,
149
138
  name: Optional[str],
150
139
  non_interactive: bool,
151
140
  machine: Optional[str],
@@ -165,7 +154,7 @@ def api(
165
154
  return api_impl(
166
155
  script_path=script_path,
167
156
  easy=easy,
168
- local=local,
157
+ cloud=cloud,
169
158
  name=name,
170
159
  non_interactive=non_interactive,
171
160
  machine=machine,
@@ -186,7 +175,7 @@ def api(
186
175
  def api_impl(
187
176
  script_path: Union[str, Path],
188
177
  easy: bool = False,
189
- local: bool = False,
178
+ cloud: bool = False,
190
179
  name: Optional[str] = None,
191
180
  tag: Optional[str] = None,
192
181
  non_interactive: bool = False,
@@ -217,7 +206,7 @@ def api_impl(
217
206
  timestr = datetime.now().strftime("%b-%d-%H_%M")
218
207
  name = f"litserve-{timestr}".lower()
219
208
 
220
- if local:
209
+ if not cloud and not devbox:
221
210
  try:
222
211
  subprocess.run(
223
212
  ["python", str(script_path)],
@@ -230,7 +219,8 @@ def api_impl(
230
219
  raise RuntimeError(error_msg) from None
231
220
 
232
221
  if devbox:
233
- 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)
234
224
 
235
225
  machine = Machine.from_str(machine)
236
226
  return _handle_cloud(
@@ -253,165 +243,6 @@ def api_impl(
253
243
  )
254
244
 
255
245
 
256
- class _AuthServer(AuthServer):
257
- def get_auth_url(self, port: int) -> str:
258
- redirect_uri = f"http://localhost:{port}/login-complete"
259
- params = urlencode({"redirectTo": redirect_uri, "okbhrt": LITSERVE_CODE})
260
- return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
261
-
262
-
263
- class _Auth(Auth):
264
- def __init__(self, shall_confirm: bool = False) -> None:
265
- super().__init__()
266
- self._shall_confirm = shall_confirm
267
-
268
- def _run_server(self) -> None:
269
- if self._shall_confirm:
270
- proceed = Confirm.ask(
271
- "Authenticating with Lightning AI. This will open a browser window. Continue?", default=True
272
- )
273
- if not proceed:
274
- raise RuntimeError(
275
- "Login cancelled. Please login to Lightning AI to deploy the API."
276
- " Run `lightning login` to login."
277
- ) from None
278
- print("Opening browser for authentication...")
279
- print("Please come back to the terminal after logging in.")
280
- time.sleep(3)
281
- _AuthServer().login_with_browser(self)
282
-
283
-
284
- def authenticate(shall_confirm: bool = True) -> None:
285
- auth = _Auth(shall_confirm)
286
- auth.authenticate()
287
-
288
-
289
- def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
290
- if teamspace is None:
291
- user = _get_authed_user()
292
- menu = _TeamspacesMenu()
293
- possible_teamspaces = menu._get_possible_teamspaces(user)
294
- if len(possible_teamspaces) == 1:
295
- name = next(iter(possible_teamspaces.values()))["name"]
296
- return Teamspace(name=name, org=org, user=user)
297
-
298
- return menu._resolve_teamspace(teamspace)
299
-
300
- return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
301
-
302
-
303
- class _UserStatus(TypedDict):
304
- verified: bool
305
- onboarded: bool
306
-
307
-
308
- def poll_verified_status(timeout: int = _POLL_TIMEOUT) -> _UserStatus:
309
- """Polls the verified status of the user until it is True or a timeout occurs."""
310
- user_api = UserApi()
311
- user = _get_authed_user()
312
- start_time = datetime.now()
313
- result = {"onboarded": False, "verified": False}
314
- while True:
315
- user_resp = user_api.get_user(name=user.name)
316
- result["onboarded"] = user_resp.status.completed_project_onboarding
317
- result["verified"] = user_resp.status.verified
318
- if user_resp.status.verified:
319
- return result
320
- if (datetime.now() - start_time).total_seconds() > timeout:
321
- break
322
- time.sleep(5)
323
- return result
324
-
325
-
326
- class _OnboardingStatus(Enum):
327
- NOT_VERIFIED = "not_verified"
328
- ONBOARDING = "onboarding"
329
- ONBOARDED = "onboarded"
330
-
331
-
332
- class _Onboarding:
333
- def __init__(self, console: Console) -> None:
334
- self.console = console
335
- self.user = _get_authed_user()
336
- self.user_api = UserApi()
337
- self.client = LightningClient(max_tries=7)
338
-
339
- @property
340
- def verified(self) -> bool:
341
- return self.user_api.get_user(name=self.user.name).status.verified
342
-
343
- @property
344
- def is_onboarded(self) -> bool:
345
- return self.user_api.get_user(name=self.user.name).status.completed_project_onboarding
346
-
347
- @property
348
- def can_join_org(self) -> bool:
349
- return len(self.client.organizations_service_list_joinable_organizations().joinable_organizations) > 0
350
-
351
- @property
352
- def status(self) -> _OnboardingStatus:
353
- if not self.verified:
354
- return _OnboardingStatus.NOT_VERIFIED
355
- if self.is_onboarded:
356
- return _OnboardingStatus.ONBOARDED
357
- return _OnboardingStatus.ONBOARDING
358
-
359
- def _wait_user_onboarding(self, timeout: int = _POLL_TIMEOUT) -> None:
360
- """Wait for user onboarding if they can join the teamspace otherwise move to select a teamspace."""
361
- status = self.status
362
- if status == _OnboardingStatus.ONBOARDED:
363
- return
364
-
365
- self.console.print("Waiting for account setup. Visit lightning.ai")
366
- start_time = datetime.now()
367
- while self.status != _OnboardingStatus.ONBOARDED:
368
- time.sleep(5)
369
- if self.is_onboarded:
370
- return
371
- if (datetime.now() - start_time).total_seconds() > timeout:
372
- break
373
-
374
- raise RuntimeError("Timed out waiting for onboarding status")
375
-
376
- def get_cloudspace_id(self, teamspace: Teamspace) -> Optional[str]:
377
- cloudspaces: List[V1CloudSpace] = self.client.cloud_space_service_list_cloud_spaces(teamspace.id).cloudspaces
378
- cloudspaces = sorted(cloudspaces, key=lambda cloudspace: cloudspace.created_at, reverse=True)
379
- if len(cloudspaces) == 0:
380
- raise RuntimeError("Error creating deployment! Finish account setup at lightning.ai first.")
381
- # get the first cloudspace
382
- cloudspace = cloudspaces[0]
383
- if "scratch-studio" in cloudspace.name or "scratch-studio" in cloudspace.display_name:
384
- return cloudspace.id
385
- return None
386
-
387
- def select_teamspace(self, teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
388
- """Select a teamspace while onboarding.
389
-
390
- If user is being onboarded and can't join any org, the teamspace it will be resolved to the default
391
- personal teamspace.
392
- If user is being onboarded and can join an org then it will select default teamspace from the org.
393
- """
394
- if self.is_onboarded:
395
- return select_teamspace(teamspace, org, user)
396
-
397
- # Run only when user hasn't completed onboarding yet.
398
- menu = _TeamspacesMenu()
399
- self._wait_user_onboarding()
400
- # Onboarding has been completed - user already selected organization if they could
401
- possible_teamspaces = menu._get_possible_teamspaces(self.user)
402
- if len(possible_teamspaces) == 1:
403
- # User didn't select any org
404
- value = next(iter(possible_teamspaces.values()))
405
- return Teamspace(name=value["name"], org=value["org"], user=value["user"])
406
-
407
- for _, value in possible_teamspaces.items():
408
- # User select an org
409
- # Onboarding teamspace will be the default teamspace in the selected org
410
- if value["org"]:
411
- return Teamspace(name=value["name"], org=value["org"], user=value["user"])
412
- raise RuntimeError("Unable to select teamspace. Visit lightning.ai")
413
-
414
-
415
246
  def is_connected(host: str = "8.8.8.8", port: int = 53, timeout: int = 10) -> bool:
416
247
  try:
417
248
  socket.setdefaulttimeout(timeout)
@@ -453,96 +284,6 @@ def _upload_container(
453
284
  return True
454
285
 
455
286
 
456
- # TODO: Move the rest of the devbox logic here
457
- class _LitServeDevbox:
458
- """Build LitServe API in a Studio."""
459
-
460
- def resolve_previous_upload(self, studio: Studio, folder: str) -> Dict[str, str]:
461
- remote_path = "."
462
- pairs = {}
463
- for root, _, files in os.walk(folder):
464
- rel_root = os.path.relpath(root, folder)
465
- for f in files:
466
- pairs[os.path.join(root, f)] = os.path.join(remote_path, rel_root, f)
467
- return _resolve_previous_upload_state(studio, remote_path, pairs)
468
-
469
- def upload_folder(self, studio: Studio, folder: str, upload_state: Dict[str, str]) -> None:
470
- with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
471
- futures = _start_parallel_upload(executor, studio, upload_state)
472
- total_files = len(upload_state)
473
-
474
- with Progress(
475
- SpinnerColumn(),
476
- TextColumn("[progress.description]{task.description}"),
477
- TimeElapsedColumn(),
478
- console=Console(),
479
- transient=True,
480
- ) as progress:
481
- upload_task = progress.add_task(f"[cyan]Uploading {total_files} files to Studio...", total=total_files)
482
- for f in concurrent.futures.as_completed(futures):
483
- upload_state.pop(f.result())
484
- _dump_current_upload_state(studio, ".", upload_state)
485
- progress.update(upload_task, advance=1)
486
-
487
-
488
- def _handle_devbox(
489
- name: str,
490
- script_path: Path,
491
- console: Console,
492
- non_interactive: bool = False,
493
- devbox: Machine = "CPU",
494
- interruptible: bool = False,
495
- teamspace: Optional[str] = None,
496
- org: Optional[str] = None,
497
- user: Optional[str] = None,
498
- ) -> None:
499
- if script_path.suffix != ".py":
500
- console.print("❌ Error: Only Python files (.py) are supported for development servers", style="red")
501
- return
502
-
503
- resolved_teamspace = select_teamspace(teamspace, org, user)
504
- studio = Studio(name=name, teamspace=resolved_teamspace)
505
- lit_devbox = _LitServeDevbox()
506
-
507
- studio_url = _get_studio_url(studio, turn_on=True)
508
- pathlib_path = Path(script_path).resolve()
509
- ok = False
510
- studio_path = f"{studio.owner.name}/{studio.teamspace.name}/{studio.name}"
511
-
512
- console.print("\n=== Lightning Studio Setup ===")
513
- console.print(f"🔧 [bold]Setting up Studio:[/bold] {studio_path}")
514
- console.print(f"📁 [bold]Local project:[/bold] {pathlib_path.parent}")
515
-
516
- upload_state = lit_devbox.resolve_previous_upload(studio, pathlib_path.parent)
517
- if non_interactive:
518
- console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
519
- ok = webbrowser.open(studio_url)
520
- else:
521
- if Confirm.ask("Would you like to open your Studio in the browser?", default=True):
522
- console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
523
- ok = webbrowser.open(studio_url)
524
-
525
- if not ok:
526
- console.print(f"🔗 [bold]Access Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
527
-
528
- console.print("\n⚡ Initializing Studio (this typically takes 1-2 minutes)...")
529
- studio.start(machine=devbox, interruptible=interruptible)
530
- studio.install_plugin("custom-port")
531
- console.print("🔌 Configuring server port...")
532
- studio.run_plugin("custom-port", port=8000) # TODO: Remove hardcoded port and fetch from LitServe
533
-
534
- console.print("📤 Syncing project files to Studio...")
535
- lit_devbox.upload_folder(studio, pathlib_path.parent, upload_state)
536
-
537
- # Add completion message with next steps
538
- console.print("\n✅ Studio ready!")
539
- console.print("\n📋 [bold]Next steps:[/bold]")
540
- console.print(" [bold]1.[/bold] Server code will be available in the Studio")
541
- console.print(" [bold]2.[/bold] The Studio is now running with the specified configuration")
542
- console.print(" [bold]3.[/bold] Modify and run your server directly in the Studio")
543
- # TODO: Once server running is implemented
544
-
545
-
546
287
  def _handle_cloud(
547
288
  script_path: Union[str, Path],
548
289
  console: Console,
@@ -563,7 +304,7 @@ def _handle_cloud(
563
304
  ) -> None:
564
305
  if not is_connected():
565
306
  console.print("❌ Internet connection required to deploy to the cloud.", style="red")
566
- console.print("To run locally instead, use: `lightning serve [SCRIPT | server.py] --local`")
307
+ console.print("To run locally instead, use: `lightning serve [SCRIPT | server.py]`")
567
308
  return
568
309
 
569
310
  deployment_name = os.path.basename(repository)
@@ -605,7 +346,7 @@ def _handle_cloud(
605
346
  # Push the container to the registry
606
347
  console.print("\nPushing container to registry. It may take a while...", style="bold")
607
348
  # Authenticate with LitServe affiliate
608
- authenticate(shall_confirm=not non_interactive)
349
+ authenticate(_AuthMode.DEPLOY, shall_confirm=not non_interactive)
609
350
  user_status = poll_verified_status()
610
351
  cloudspace_id: Optional[str] = None
611
352
  from_onboarding = False
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  import re
3
4
  from pathlib import Path
@@ -6,6 +7,7 @@ from typing import Optional
6
7
  import click
7
8
  from rich.console import Console
8
9
 
10
+ from lightning_sdk.api.license_api import LicenseApi
9
11
  from lightning_sdk.api.lit_container_api import LitContainerApi
10
12
  from lightning_sdk.cli.exceptions import StudioCliError
11
13
  from lightning_sdk.cli.studios_menu import _StudiosMenu
@@ -46,13 +48,18 @@ def model(name: str, download_dir: str = ".") -> None:
46
48
  "--studio",
47
49
  default=None,
48
50
  help=(
49
- "The name of the studio to upload to. "
51
+ "The name of the studio to download from. "
50
52
  "Will show a menu with user's owned studios for selection if not specified. "
51
53
  "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
52
54
  "The teamspace and studio names can be regular expressions to match, "
53
55
  "a menu filtered studios will be shown for final selection."
54
56
  ),
55
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
+ )
56
63
  @click.option(
57
64
  "--local-path",
58
65
  "--local_path",
@@ -60,32 +67,41 @@ def model(name: str, download_dir: str = ".") -> None:
60
67
  type=click.Path(file_okay=False, dir_okay=True),
61
68
  help="The path to the directory you want to download the folder to.",
62
69
  )
63
- def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
64
- """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.
65
74
 
66
75
  Example:
67
76
  lightning download folder PATH
68
77
 
69
- PATH: The relative path within the Studio you want to download.
70
- 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.
71
80
  """
72
81
  local_path = Path(local_path)
73
82
  if not local_path.is_dir():
74
83
  raise NotADirectoryError(f"'{local_path}' is not a directory")
75
84
 
76
- 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)
77
93
 
78
94
  if not path:
79
- local_path /= resolved_studio.name
95
+ local_path /= resolved_downloader.name
80
96
  path = ""
81
97
 
82
98
  try:
83
99
  if not path:
84
100
  raise FileNotFoundError()
85
- 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))
86
102
  except Exception as e:
87
103
  raise StudioCliError(
88
- 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}. "
89
105
  "Please contact Lightning AI directly to resolve this issue."
90
106
  ) from e
91
107
 
@@ -103,6 +119,11 @@ def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".")
103
119
  "a menu filtered studios will be shown for final selection."
104
120
  ),
105
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
+ )
106
127
  @click.option(
107
128
  "--local-path",
108
129
  "--local_path",
@@ -110,31 +131,40 @@ def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".")
110
131
  type=click.Path(file_okay=False, dir_okay=True),
111
132
  help="The path to the directory you want to download the file to.",
112
133
  )
113
- def file(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
114
- """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.
115
136
 
116
137
  Example:
117
138
  lightning download file PATH
118
139
 
119
- 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.
120
141
  """
121
142
  local_path = Path(local_path)
122
143
  if not local_path.is_dir():
123
144
  raise NotADirectoryError(f"'{local_path}' is not a directory")
124
145
 
125
- 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")
126
156
 
127
157
  if not path:
128
- local_path /= resolved_studio.name
158
+ local_path /= resolved_downloader.name
129
159
  path = ""
130
160
 
131
161
  try:
132
162
  if not path:
133
163
  raise FileNotFoundError()
134
- 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)))
135
165
  except Exception as e:
136
166
  raise StudioCliError(
137
- 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}. "
138
168
  "Please contact Lightning AI directly to resolve this issue."
139
169
  ) from e
140
170
 
@@ -208,3 +238,26 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
208
238
 
209
239
  with skip_studio_init():
210
240
  return Studio(**selected_studio)
241
+
242
+
243
+ @download.command(name="licenses")
244
+ def licenses() -> None:
245
+ """Download licenses for all products/packages.
246
+
247
+ Example:
248
+ lightning download licenses
249
+
250
+ """
251
+ user = _get_authed_user()
252
+ api = LicenseApi()
253
+ licenses = api.list_user_licenses(user.id)
254
+
255
+ user_home = Path.home()
256
+ lit_dir = user_home / ".lightning"
257
+ lit_dir.mkdir(parents=True, exist_ok=True)
258
+ licenses_file = lit_dir / "licenses.json"
259
+
260
+ licenses_short = {ll.product_name: ll.license_key for ll in licenses if ll.is_valid}
261
+ with licenses_file.open("w") as fp:
262
+ json.dump(licenses_short, fp, indent=4)
263
+ Console().print(f"Licenses downloaded to {licenses_file}", style="green")
@@ -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
lightning_sdk/cli/open.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import webbrowser
2
+ from contextlib import suppress
2
3
  from pathlib import Path
3
4
  from typing import Optional
4
5
 
@@ -23,7 +24,17 @@ from lightning_sdk.utils.resolve import _get_studio_url
23
24
  "If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
24
25
  ),
25
26
  )
26
- def open(path: str = ".", teamspace: Optional[str] = None) -> None: # noqa: A001
27
+ @click.option(
28
+ "--cloud-account",
29
+ "--cloud_account",
30
+ default=None,
31
+ help=(
32
+ "The cloud account to create the studio on. "
33
+ "If not specified, will try to infer from the environment (e.g. when run from within a Studio.) "
34
+ "or fall back to the teamspace default."
35
+ ),
36
+ )
37
+ def open(path: str = ".", teamspace: Optional[str] = None, cloud_account: Optional[str] = None) -> None: # noqa: A001
27
38
  """Open a local file or folder in a Lightning Studio.
28
39
 
29
40
  Example:
@@ -41,7 +52,15 @@ def open(path: str = ".", teamspace: Optional[str] = None) -> None: # noqa: A00
41
52
  menu = _TeamspacesMenu()
42
53
  resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
43
54
 
44
- new_studio = Studio(name=pathlib_path.stem, teamspace=resolved_teamspace)
55
+ # default cloud account to current studios cloud account if run from studio
56
+ # else it will fall back to teamspace default in the backend
57
+ if cloud_account is None:
58
+ with suppress(ValueError):
59
+ s = Studio()
60
+ if s.teamspace.name == resolved_teamspace.name and s.teamspace.owner.name == resolved_teamspace.owner.name:
61
+ cloud_account = s.cloud_account
62
+
63
+ new_studio = Studio(name=pathlib_path.stem, teamspace=resolved_teamspace, cloud_account=cloud_account)
45
64
 
46
65
  console.print(
47
66
  f"[bold]Uploading {path} to {new_studio.owner.name}/{new_studio.teamspace.name}/{new_studio.name}[/bold]"
@@ -4,8 +4,10 @@ import click
4
4
 
5
5
  from lightning_sdk import Machine, Studio
6
6
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
7
+ from lightning_sdk.studio import Provider
7
8
 
8
9
  _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
10
+ _PROVIDER_VALUES = tuple([provider.value for provider in Provider])
9
11
 
10
12
 
11
13
  @click.group("start")
@@ -33,7 +35,14 @@ def start() -> None:
33
35
  type=click.Choice(_MACHINE_VALUES),
34
36
  help="The machine type to start the studio on.",
35
37
  )
36
- def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU") -> None:
38
+ @click.option(
39
+ "--provider",
40
+ default=None,
41
+ show_default=True,
42
+ type=click.Choice(_PROVIDER_VALUES),
43
+ help="The provider to start the studio on.",
44
+ )
45
+ def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU", provider: Optional[str] = None) -> None:
37
46
  """Start a studio on a given machine.
38
47
 
39
48
  Example:
@@ -50,9 +59,9 @@ def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU") ->
50
59
  owner, teamspace = None, None
51
60
 
52
61
  try:
53
- studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False)
62
+ studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False, provider=provider)
54
63
  except (RuntimeError, ValueError, ApiException):
55
- studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
64
+ studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False, provider=provider)
56
65
 
57
66
  try:
58
67
  resolved_machine = getattr(Machine, machine.upper(), Machine(machine, machine))