lightning-sdk 2025.7.17__py3-none-any.whl → 2025.7.30rc0__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 (99) hide show
  1. lightning_sdk/__init__.py +3 -2
  2. lightning_sdk/api/cloud_account_api.py +204 -0
  3. lightning_sdk/api/deployment_api.py +11 -0
  4. lightning_sdk/api/job_api.py +82 -10
  5. lightning_sdk/api/llm_api.py +1 -1
  6. lightning_sdk/api/mmt_api.py +44 -5
  7. lightning_sdk/api/pipeline_api.py +4 -3
  8. lightning_sdk/api/studio_api.py +51 -8
  9. lightning_sdk/api/utils.py +6 -2
  10. lightning_sdk/cli/clusters_menu.py +3 -3
  11. lightning_sdk/cli/create.py +25 -11
  12. lightning_sdk/cli/deploy/_auth.py +19 -3
  13. lightning_sdk/cli/deploy/serve.py +21 -5
  14. lightning_sdk/cli/download.py +25 -1
  15. lightning_sdk/cli/entrypoint.py +4 -2
  16. lightning_sdk/cli/list.py +5 -1
  17. lightning_sdk/cli/run.py +3 -1
  18. lightning_sdk/cli/start.py +40 -8
  19. lightning_sdk/cli/switch.py +3 -1
  20. lightning_sdk/deployment/deployment.py +8 -0
  21. lightning_sdk/job/base.py +27 -3
  22. lightning_sdk/job/job.py +28 -4
  23. lightning_sdk/job/v1.py +10 -1
  24. lightning_sdk/job/v2.py +22 -2
  25. lightning_sdk/job/work.py +5 -1
  26. lightning_sdk/lightning_cloud/openapi/__init__.py +14 -1
  27. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +428 -0
  28. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +153 -48
  29. lightning_sdk/lightning_cloud/openapi/api/cloudy_service_api.py +295 -0
  30. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +93 -0
  31. lightning_sdk/lightning_cloud/openapi/models/__init__.py +14 -1
  32. lightning_sdk/lightning_cloud/openapi/models/agentmanagedendpoints_id_body.py +27 -1
  33. lightning_sdk/lightning_cloud/openapi/models/blogposts_id_body.py +53 -1
  34. lightning_sdk/lightning_cloud/openapi/models/conversations_id_body1.py +123 -0
  35. lightning_sdk/lightning_cloud/openapi/models/messages_id_body.py +123 -0
  36. lightning_sdk/lightning_cloud/openapi/models/metricsstream_id_body.py +27 -1
  37. lightning_sdk/lightning_cloud/openapi/models/project_id_schedules_body.py +81 -3
  38. lightning_sdk/lightning_cloud/openapi/models/schedules_id_body.py +79 -1
  39. lightning_sdk/lightning_cloud/openapi/models/user_id_upgradetrigger_body.py +201 -0
  40. lightning_sdk/lightning_cloud/openapi/models/user_user_id_body.py +201 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_billing_subscription.py +27 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_blog_post.py +53 -1
  43. lightning_sdk/lightning_cloud/openapi/models/v1_cloudy_settings.py +227 -0
  44. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  45. lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
  46. lightning_sdk/lightning_cloud/openapi/models/v1_conversation_response_chunk.py +27 -1
  47. lightning_sdk/lightning_cloud/openapi/models/v1_create_billing_upgrade_trigger_record_response.py +97 -0
  48. lightning_sdk/lightning_cloud/openapi/models/v1_create_blog_post_request.py +53 -1
  49. lightning_sdk/lightning_cloud/openapi/models/v1_create_checkout_session_request.py +27 -1
  50. lightning_sdk/lightning_cloud/openapi/models/v1_create_subscription_checkout_session_request.py +55 -3
  51. lightning_sdk/lightning_cloud/openapi/models/v1_function_call.py +149 -0
  52. lightning_sdk/lightning_cloud/openapi/models/{v1_get_clickhouse_assistant_session_daily_aggregated_response.py → v1_get_assistant_session_daily_aggregated_response.py} +22 -22
  53. lightning_sdk/lightning_cloud/openapi/models/v1_get_cluster_health_response.py +149 -0
  54. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
  55. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +105 -1
  57. lightning_sdk/lightning_cloud/openapi/models/v1_like_status.py +104 -0
  58. lightning_sdk/lightning_cloud/openapi/models/v1_list_published_managed_endpoints_response.py +123 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_managed_endpoint.py +27 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +95 -17
  61. lightning_sdk/lightning_cloud/openapi/models/v1_message.py +27 -1
  62. lightning_sdk/lightning_cloud/openapi/models/v1_quote_subscription_response.py +27 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_resource_visibility.py +27 -1
  64. lightning_sdk/lightning_cloud/openapi/models/v1_response_choice.py +29 -3
  65. lightning_sdk/lightning_cloud/openapi/models/v1_schedule.py +79 -1
  66. lightning_sdk/lightning_cloud/openapi/models/v1_service_health.py +27 -1
  67. lightning_sdk/lightning_cloud/openapi/models/v1_slurm_v1.py +79 -1
  68. lightning_sdk/lightning_cloud/openapi/models/v1_slurm_v1_status.py +79 -1
  69. lightning_sdk/lightning_cloud/openapi/models/v1_tool_call.py +175 -0
  70. lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_like_response.py +149 -0
  71. lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_message_like_response.py +149 -0
  72. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -313
  73. lightning_sdk/lightning_cloud/openapi/models/v1_volume_state.py +1 -0
  74. lightning_sdk/llm/llm.py +69 -11
  75. lightning_sdk/llm/public_assistants.json +32 -8
  76. lightning_sdk/machine.py +151 -43
  77. lightning_sdk/mmt/base.py +20 -2
  78. lightning_sdk/mmt/mmt.py +25 -3
  79. lightning_sdk/mmt/v1.py +7 -1
  80. lightning_sdk/mmt/v2.py +27 -3
  81. lightning_sdk/models.py +1 -1
  82. lightning_sdk/organization.py +4 -0
  83. lightning_sdk/pipeline/pipeline.py +16 -5
  84. lightning_sdk/pipeline/printer.py +5 -3
  85. lightning_sdk/pipeline/schedule.py +844 -1
  86. lightning_sdk/pipeline/steps.py +19 -4
  87. lightning_sdk/sandbox.py +4 -1
  88. lightning_sdk/serve.py +2 -0
  89. lightning_sdk/studio.py +91 -44
  90. lightning_sdk/teamspace.py +19 -10
  91. lightning_sdk/utils/resolve.py +37 -2
  92. {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/METADATA +7 -5
  93. {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/RECORD +98 -85
  94. lightning_sdk/api/cluster_api.py +0 -119
  95. /lightning_sdk/cli/{inspect.py → inspection.py} +0 -0
  96. {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/LICENSE +0 -0
  97. {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/WHEEL +0 -0
  98. {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/entry_points.txt +0 -0
  99. {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ import tempfile
4
4
  import time
5
5
  import zipfile
6
6
  from threading import Event, Thread
7
- from typing import Any, Dict, Generator, Mapping, Optional, Tuple, Union
7
+ from typing import Any, Dict, Generator, List, Mapping, Optional, Tuple, Union
8
8
 
9
9
  import backoff
10
10
  import requests
@@ -37,6 +37,7 @@ from lightning_sdk.lightning_cloud.openapi import (
37
37
  V1CloudSpaceSeedFile,
38
38
  V1CloudSpaceSourceType,
39
39
  V1CloudSpaceState,
40
+ V1ClusterAccelerator,
40
41
  V1EndpointType,
41
42
  V1GetCloudSpaceInstanceStatusResponse,
42
43
  V1GetLongRunningCommandInCloudSpaceResponse,
@@ -121,7 +122,7 @@ class StudioApi:
121
122
  name: str,
122
123
  teamspace_id: str,
123
124
  cloud_account: Optional[str] = None,
124
- source: Optional[V1CloudSpaceSourceType] = None,
125
+ source: Optional[Union[V1CloudSpaceSourceType, str]] = None,
125
126
  disable_secrets: bool = False,
126
127
  sandbox: bool = False,
127
128
  cloud_space_environment_template_id: Optional[str] = None,
@@ -168,12 +169,26 @@ class StudioApi:
168
169
  return startup_status and startup_status.top_up_restore_finished
169
170
 
170
171
  def start_studio(
171
- self, studio_id: str, teamspace_id: str, machine: Union[Machine, str], interruptible: False
172
+ self,
173
+ studio_id: str,
174
+ teamspace_id: str,
175
+ machine: Union[Machine, str],
176
+ interruptible: bool = False,
177
+ max_runtime: Optional[int] = None,
172
178
  ) -> None:
173
179
  """Start an existing Studio."""
180
+ # need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
181
+ optional_kwargs_compute_body = {}
182
+
183
+ if max_runtime is not None:
184
+ optional_kwargs_compute_body["requested_run_duration_seconds"] = str(max_runtime)
174
185
  self._client.cloud_space_service_start_cloud_space_instance(
175
186
  IdStartBody(
176
- compute_config=V1UserRequestedComputeConfig(name=_machine_to_compute_name(machine), spot=interruptible)
187
+ compute_config=V1UserRequestedComputeConfig(
188
+ name=_machine_to_compute_name(machine),
189
+ spot=interruptible,
190
+ **optional_kwargs_compute_body,
191
+ )
177
192
  ),
178
193
  teamspace_id,
179
194
  studio_id,
@@ -253,12 +268,24 @@ class StudioApi:
253
268
  break
254
269
  time.sleep(1)
255
270
 
256
- def get_machine(self, studio_id: str, teamspace_id: str) -> Machine:
271
+ def get_machine(self, studio_id: str, teamspace_id: str, cloud_account_id: str, org_id: str) -> Machine:
257
272
  """Get the current machine type the given Studio is running on."""
258
273
  response: V1CloudSpaceInstanceConfig = self._client.cloud_space_service_get_cloud_space_instance_config(
259
274
  project_id=teamspace_id, id=studio_id
260
275
  )
261
- return Machine(response.compute_config.name, response.compute_config.name)
276
+ accelerators = self._get_machines_for_cloud_account(
277
+ teamspace_id=teamspace_id, cloud_account_id=cloud_account_id, org_id=org_id
278
+ )
279
+
280
+ for accelerator in accelerators:
281
+ if response.compute_config.name in (
282
+ accelerator.slug,
283
+ accelerator.slug_multi_cloud,
284
+ accelerator.instance_id,
285
+ ):
286
+ return Machine.from_str(accelerator.slug_multi_cloud)
287
+
288
+ return Machine.from_str(response.compute_config.name)
262
289
 
263
290
  def get_interruptible(self, studio_id: str, teamspace_id: str) -> bool:
264
291
  """Get whether the Studio is running on a interruptible instance."""
@@ -268,6 +295,22 @@ class StudioApi:
268
295
 
269
296
  return response.compute_config.spot
270
297
 
298
+ def _get_machines_for_cloud_account(
299
+ self, teamspace_id: str, cloud_account_id: str, org_id: str
300
+ ) -> List[V1ClusterAccelerator]:
301
+ from lightning_sdk.api.cloud_account_api import CloudAccountApi
302
+
303
+ cloud_account_api = CloudAccountApi()
304
+ accelerators = cloud_account_api.list_cloud_account_accelerators(
305
+ teamspace_id=teamspace_id,
306
+ cloud_account_id=cloud_account_id,
307
+ org_id=org_id,
308
+ )
309
+ if not accelerators:
310
+ return []
311
+
312
+ return list(filter(lambda acc: acc.enabled, accelerators.accelerator))
313
+
271
314
  def _get_detached_command_status(
272
315
  self, studio_id: str, teamspace_id: str, session_id: str
273
316
  ) -> V1GetLongRunningCommandInCloudSpaceResponse:
@@ -398,7 +441,7 @@ class StudioApi:
398
441
  body=body,
399
442
  )
400
443
 
401
- def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str, str]:
444
+ def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str, Any]:
402
445
  """Duplicates the given Studio from a given Teamspace into a given target Teamspace."""
403
446
  target_teamspace = self._client.projects_service_get_project(target_teamspace_id)
404
447
  init_kwargs = {}
@@ -421,7 +464,7 @@ class StudioApi:
421
464
  init_kwargs["name"] = new_cloudspace.name
422
465
  init_kwargs["teamspace"] = target_teamspace.name
423
466
 
424
- self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False)
467
+ self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False, None)
425
468
  return init_kwargs
426
469
 
427
470
  def delete_studio(self, studio_id: str, teamspace_id: str) -> None:
@@ -331,7 +331,9 @@ class _DummyResponse:
331
331
 
332
332
  def _machine_to_compute_name(machine: Union[Machine, str]) -> str:
333
333
  if isinstance(machine, Machine):
334
- return machine.instance_type
334
+ if machine.instance_type is not None:
335
+ return machine.instance_type
336
+ return machine.slug
335
337
  return machine
336
338
 
337
339
 
@@ -352,7 +354,9 @@ def _get_registry_url() -> str:
352
354
 
353
355
 
354
356
  def _sanitize_studio_remote_path(path: str, studio_id: str) -> str:
355
- return f"/cloudspaces/{studio_id}/code/content/{path.replace('/teamspace/studios/this_studio/', '')}"
357
+ path = path.replace("/teamspace/studios/this_studio/", "")
358
+ root = f"/cloudspaces/{studio_id}/code/content/"
359
+ return os.path.join(root, path)
356
360
 
357
361
 
358
362
  def _resolve_teamspace_remote_path(path: str) -> str:
@@ -5,7 +5,7 @@ from rich.console import Console
5
5
  from simple_term_menu import TerminalMenu
6
6
 
7
7
  from lightning_sdk import Teamspace
8
- from lightning_sdk.api.cluster_api import ClusterApi
8
+ from lightning_sdk.api.cloud_account_api import CloudAccountApi
9
9
  from lightning_sdk.lightning_cloud.openapi import Externalv1Cluster, V1ProjectClusterBinding
10
10
 
11
11
 
@@ -29,9 +29,9 @@ class _ClustersMenu:
29
29
  selected_cluster_id = self._get_cluster_from_interactive_menu(
30
30
  possible_clusters=teamspace.cloud_account_objs
31
31
  )
32
- cluster_api = ClusterApi()
32
+ cloud_account_api = CloudAccountApi()
33
33
 
34
- return cluster_api.get_cluster(
34
+ return cloud_account_api.get_cloud_account(
35
35
  cluster_id=selected_cluster_id, org_id=teamspace.owner.id, project_id=teamspace.id
36
36
  )
37
37
  except KeyboardInterrupt:
@@ -7,12 +7,15 @@ import click
7
7
  from rich.console import Console
8
8
 
9
9
  from lightning_sdk import Machine, Studio
10
- from lightning_sdk.api.cluster_api import ClusterApi
10
+ from lightning_sdk.api.cloud_account_api import CloudAccountApi
11
11
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
12
- from lightning_sdk.studio import Provider
12
+ from lightning_sdk.machine import CloudProvider
13
+ from lightning_sdk.utils.resolve import _resolve_deprecated_provider
13
14
 
14
- _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
15
- _PROVIDER_VALUES = tuple([provider.value for provider in Provider])
15
+ _MACHINE_VALUES = tuple(
16
+ [machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
17
+ )
18
+ _PROVIDER_VALUES = tuple([provider.value for provider in CloudProvider])
16
19
 
17
20
 
18
21
  @click.group("create")
@@ -48,16 +51,26 @@ def create() -> None:
48
51
  ),
49
52
  )
50
53
  @click.option(
51
- "--provider",
54
+ "--cloud-provider",
52
55
  default=None,
53
56
  type=click.Choice(_PROVIDER_VALUES),
54
57
  help="The provider to create the studio on. If --cloud-account is specified, this option is prioritized.",
55
58
  )
59
+ @click.option(
60
+ "--provider",
61
+ default=None,
62
+ type=click.Choice(_PROVIDER_VALUES),
63
+ help=(
64
+ "Deprecated. Use --cloud-provider instead. The provider to create the studio on. "
65
+ "If --cloud-account is specified, this option is prioritized."
66
+ ),
67
+ )
56
68
  def studio(
57
69
  name: str,
58
70
  teamspace: Optional[str] = None,
59
71
  start: Optional[str] = None,
60
72
  cloud_account: Optional[str] = None,
73
+ cloud_provider: Optional[str] = None,
61
74
  provider: Optional[str] = None,
62
75
  ) -> None:
63
76
  """Create a new studio on the Lightning AI platform.
@@ -70,12 +83,13 @@ def studio(
70
83
  menu = _TeamspacesMenu()
71
84
  teamspace_resolved = menu._resolve_teamspace(teamspace)
72
85
 
73
- if provider is not None:
74
- cluster_api = ClusterApi()
75
- cloud_account = cluster_api.get_cluster_provider_mapping(
76
- teamspace_resolved.id,
77
- teamspace_resolved.owner.id,
78
- )[provider]
86
+ cloud_provider = str(_resolve_deprecated_provider(cloud_provider, provider))
87
+
88
+ if cloud_provider is not None:
89
+ cloud_account_api = CloudAccountApi()
90
+ cloud_account = cloud_account_api.resolve_cloud_account(
91
+ teamspace_resolved.id, cloud_account, cloud_provider, teamspace_resolved.default_cloud_account
92
+ )
79
93
 
80
94
  # default cloud account to current studios cloud account if run from studio
81
95
  # else it will fall back to teamspace default in the backend
@@ -2,7 +2,8 @@ import os
2
2
  import time
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
- from typing import List, Optional, TypedDict
5
+ from typing import Any, List, Optional, TypedDict
6
+ from urllib.parse import urlencode
6
7
 
7
8
  from rich.console import Console
8
9
  from rich.prompt import Confirm
@@ -10,6 +11,7 @@ from rich.prompt import Confirm
10
11
  from lightning_sdk import Teamspace
11
12
  from lightning_sdk.api import UserApi
12
13
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
14
+ from lightning_sdk.lightning_cloud import env
13
15
  from lightning_sdk.lightning_cloud.login import Auth, AuthServer
14
16
  from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
15
17
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
@@ -24,6 +26,17 @@ class _AuthMode(Enum):
24
26
  DEPLOY = "deploy"
25
27
 
26
28
 
29
+ class _AuthServer(AuthServer):
30
+ def __init__(self, mode: _AuthMode, *args: Any, **kwargs: Any) -> None:
31
+ self._mode = mode
32
+ super().__init__(*args, **kwargs)
33
+
34
+ def get_auth_url(self, port: int) -> str:
35
+ redirect_uri = f"http://localhost:{port}/login-complete"
36
+ params = urlencode({"redirectTo": redirect_uri, "mode": self._mode.value, "okbhrt": LITSERVE_CODE})
37
+ return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
38
+
39
+
27
40
  class _AuthLitServe(Auth):
28
41
  def __init__(self, mode: _AuthMode, shall_confirm: bool = False) -> None:
29
42
  super().__init__()
@@ -33,7 +46,10 @@ class _AuthLitServe(Auth):
33
46
  def _run_server(self) -> None:
34
47
  if self._shall_confirm:
35
48
  proceed = Confirm.ask(
36
- "Authenticating with Lightning AI. This will open a browser window. Continue?", default=True
49
+ "[bold yellow]LitServe needs to authenticate with Lightning AI to deploy your server.[/bold yellow]\n"
50
+ "This will open a browser window for login.\n"
51
+ "Do you want to continue?",
52
+ default=True,
37
53
  )
38
54
  if not proceed:
39
55
  raise RuntimeError(
@@ -42,7 +58,7 @@ class _AuthLitServe(Auth):
42
58
  print("Opening browser for authentication...")
43
59
  print("Please come back to the terminal after logging in.")
44
60
  time.sleep(3)
45
- AuthServer({"mode": self._mode, "okbhrt": LITSERVE_CODE}).login_with_browser(self)
61
+ _AuthServer(self._mode).login_with_browser(self)
46
62
 
47
63
 
48
64
  def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
@@ -25,7 +25,9 @@ from lightning_sdk.cli.deploy._auth import (
25
25
  from lightning_sdk.cli.deploy.devbox import _handle_devbox
26
26
  from lightning_sdk.serve import _LitServeDeployer
27
27
 
28
- _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
28
+ _MACHINE_VALUES = tuple(
29
+ [machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
30
+ )
29
31
 
30
32
 
31
33
  class _ServeGroup(click.Group):
@@ -317,12 +319,20 @@ def _handle_cloud(
317
319
  ls_deployer = _LitServeDeployer(name=deployment_name, teamspace=None)
318
320
  path = ls_deployer.dockerize_api(script_path, port=port, gpu=not machine.is_cpu(), tag=tag, print_success=False)
319
321
 
320
- console.print(f"\nPlease review the Dockerfile at [u]{path}[/u] and make sure it is correct.", style="bold")
321
- correct_dockerfile = True if non_interactive else Confirm.ask("Is the Dockerfile correct?", default=True)
322
+ console.print(f"\n[bold]LitServe generated a Dockerfile at:[/bold]\n[u]{path}[/u]\n")
323
+ console.print("Please check that it matches your server setup.")
324
+ correct_dockerfile = (
325
+ True
326
+ if non_interactive
327
+ else Confirm.ask("Have you reviewed the Dockerfile and confirmed it's correct?", default=True)
328
+ )
322
329
  if not correct_dockerfile:
323
- console.print("Please fix the Dockerfile and try again.", style="red")
330
+ console.print("[red]Dockerfile review canceled. Please update the Dockerfile and try again.[/red]")
324
331
  return
325
332
 
333
+ console.print(
334
+ "Building your container image now.\n[cyan bold]Make sure Docker is installed and running.[/cyan bold]\n"
335
+ )
326
336
  with Progress(
327
337
  SpinnerColumn(),
328
338
  TextColumn("[progress.description]{task.description}"),
@@ -340,7 +350,12 @@ def _handle_cloud(
340
350
  progress.remove_task(build_task)
341
351
 
342
352
  except Exception as e:
343
- console.print(f"❌ Deployment failed: {e}", style="red")
353
+ console.print(
354
+ "❌ Failed to build a container for your server.\n"
355
+ "Make sure Docker is installed and running, then try again.",
356
+ f"\n\nError details: {e}",
357
+ style="red",
358
+ )
344
359
  return
345
360
 
346
361
  # Push the container to the registry
@@ -373,6 +388,7 @@ def _handle_cloud(
373
388
  f"{resolved_teamspace.owner.name}/{resolved_teamspace.name}/{container_basename}"
374
389
  )
375
390
 
391
+ cloud_account = cloud_account or resolved_teamspace.default_cloud_account
376
392
  if from_onboarding:
377
393
  thread = Thread(
378
394
  target=ls_deployer.run_on_cloud,
@@ -17,6 +17,27 @@ from lightning_sdk.studio import Studio
17
17
  from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
18
18
 
19
19
 
20
+ def _expand_remote_path(path: str) -> str:
21
+ """Expand and normalize remote CLI paths.
22
+
23
+ - Strips leading `~/` or `~`
24
+ - Expands `~` to the user's home but returns relative to it
25
+ - Returns an empty string if path is empty or `~`
26
+ """
27
+ if not path:
28
+ return ""
29
+
30
+ local_home = os.path.expanduser("~")
31
+
32
+ # Expand to absolute path and remove the local home prefix if present
33
+ path = os.path.expanduser(path)
34
+ if path.startswith(local_home):
35
+ path = path[len(local_home) :]
36
+
37
+ # Remove any leading "/" or "~" remnants
38
+ return path.lstrip("/~")
39
+
40
+
20
41
  @click.group(name="download")
21
42
  def download() -> None:
22
43
  """Download resources from Lightning AI."""
@@ -86,17 +107,20 @@ def folder(
86
107
  raise ValueError("Either --studio or --teamspace must be provided, not both")
87
108
 
88
109
  if studio:
110
+ path = _expand_remote_path(path)
89
111
  resolved_downloader = _resolve_studio(studio)
90
112
  elif teamspace:
91
113
  menu = _TeamspacesMenu()
92
114
  resolved_downloader = menu._resolve_teamspace(teamspace)
115
+ else:
116
+ raise ValueError("Either --studio or --teamspace must be provided")
93
117
 
94
118
  if not path:
95
119
  local_path /= resolved_downloader.name
96
120
  path = ""
97
121
 
98
122
  try:
99
- if not path:
123
+ if not path and teamspace:
100
124
  raise FileNotFoundError()
101
125
  resolved_downloader.download_folder(remote_path=path, target_path=str(local_path))
102
126
  except Exception as e:
@@ -21,7 +21,7 @@ from lightning_sdk.cli.deploy.serve import deploy
21
21
  from lightning_sdk.cli.docker_cli import dockerize
22
22
  from lightning_sdk.cli.download import download
23
23
  from lightning_sdk.cli.generate import generate
24
- from lightning_sdk.cli.inspect import inspect
24
+ from lightning_sdk.cli.inspection import inspect
25
25
  from lightning_sdk.cli.list import list_cli
26
26
  from lightning_sdk.cli.open import open
27
27
  from lightning_sdk.cli.run import run
@@ -50,7 +50,9 @@ def _notify_exception(exception_type: Type[BaseException], value: BaseException,
50
50
  renderables.append(Text("\n\nFull traceback:\n", style="bold yellow"))
51
51
  renderables.append(Syntax(tb_text, "python", theme="monokai", line_numbers=False, word_wrap=True))
52
52
  else:
53
- renderables.append(Text("\n\nTo see the full traceback, set the LIGHTNING_DEBUG environment variable to 1."))
53
+ renderables.append(Text("\n\n🐞 To view the full traceback, set: LIGHTNING_DEBUG=1"))
54
+
55
+ renderables.append(Text("\n📘 Need help? Run: lightning <command> --help", style="cyan"))
54
56
 
55
57
  console.print(Panel(Group(*renderables), title="⚡ Lightning CLI Error", border_style="red"))
56
58
 
lightning_sdk/cli/list.py CHANGED
@@ -274,7 +274,11 @@ def machines() -> None:
274
274
  table.add_column("Name")
275
275
 
276
276
  # Get all machine types from the enum
277
- machine_types = [name for name in dir(Machine) if isinstance(getattr(Machine, name), Machine)]
277
+ machine_types = [
278
+ name
279
+ for name in dir(Machine)
280
+ if isinstance(getattr(Machine, name), Machine) and getattr(Machine, name)._include_in_cli
281
+ ]
278
282
 
279
283
  # Add rows to table
280
284
  for name in sorted(machine_types):
lightning_sdk/cli/run.py CHANGED
@@ -8,7 +8,9 @@ from lightning_sdk.machine import Machine
8
8
  from lightning_sdk.mmt import MMT
9
9
  from lightning_sdk.teamspace import Teamspace
10
10
 
11
- _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
11
+ _MACHINE_VALUES = tuple(
12
+ [machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
13
+ )
12
14
 
13
15
 
14
16
  @click.group(name="run")
@@ -4,10 +4,12 @@ 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
+ from lightning_sdk.machine import CloudProvider
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
+ _MACHINE_VALUES = tuple(
10
+ [machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
11
+ )
12
+ _PROVIDER_VALUES = tuple([provider.value for provider in CloudProvider])
11
13
 
12
14
 
13
15
  @click.group("start")
@@ -35,14 +37,28 @@ def start() -> None:
35
37
  type=click.Choice(_MACHINE_VALUES),
36
38
  help="The machine type to start the studio on.",
37
39
  )
40
+ @click.option(
41
+ "--cloud-provider",
42
+ default=None,
43
+ type=click.Choice(_PROVIDER_VALUES),
44
+ help=("The provider to create the studio on. If --cloud-account is specified, this option is prioritized."),
45
+ )
38
46
  @click.option(
39
47
  "--provider",
40
48
  default=None,
41
- show_default=True,
42
49
  type=click.Choice(_PROVIDER_VALUES),
43
- help="The provider to start the studio on.",
50
+ help=(
51
+ "Deprecated. Use --cloud-provider instead. The provider to create the studio on. "
52
+ "If --cloud-account is specified, this option is prioritized."
53
+ ),
44
54
  )
45
- def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU", provider: Optional[str] = None) -> None:
55
+ def studio(
56
+ name: str,
57
+ teamspace: Optional[str] = None,
58
+ machine: str = "CPU",
59
+ cloud_provider: Optional[str] = None,
60
+ provider: Optional[str] = None,
61
+ ) -> None:
46
62
  """Start a studio on a given machine.
47
63
 
48
64
  Example:
@@ -59,10 +75,26 @@ def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU", pro
59
75
  owner, teamspace = None, None
60
76
 
61
77
  try:
62
- studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False, provider=provider)
78
+ studio = Studio(
79
+ name=name,
80
+ teamspace=teamspace,
81
+ org=owner,
82
+ user=None,
83
+ create_ok=False,
84
+ cloud_provider=cloud_provider,
85
+ provider=provider,
86
+ )
63
87
  except (RuntimeError, ValueError, ApiException) as first_error:
64
88
  try:
65
- studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False, provider=provider)
89
+ studio = Studio(
90
+ name=name,
91
+ teamspace=teamspace,
92
+ org=None,
93
+ user=owner,
94
+ create_ok=False,
95
+ cloud_provider=cloud_provider,
96
+ provider=provider,
97
+ )
66
98
  except (RuntimeError, ValueError, ApiException) as second_error:
67
99
  raise first_error from second_error
68
100
 
@@ -5,7 +5,9 @@ import click
5
5
  from lightning_sdk import Machine, Studio
6
6
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
7
7
 
8
- _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
8
+ _MACHINE_VALUES = tuple(
9
+ [machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
10
+ )
9
11
 
10
12
 
11
13
  @click.group("switch")
@@ -130,6 +130,7 @@ class Deployment:
130
130
  include_credentials: Optional[bool] = None,
131
131
  from_onboarding: Optional[bool] = None,
132
132
  from_litserve: Optional[bool] = None,
133
+ max_runtime: Optional[int] = None,
133
134
  ) -> None:
134
135
  """The Lightning AI Deployment.
135
136
 
@@ -157,6 +158,10 @@ class Deployment:
157
158
  quantity: The number of machines per replica to deploy.
158
159
  include_credentials: Whether to include the environment variables for the SDK to authenticate
159
160
  from_onboarding: Whether the deployment is from onboarding.
161
+ max_runtime: the duration (in seconds) for which to allocate the machine.
162
+ Irrelevant for most machines, required for some of the top-end machines on GCP.
163
+ If in doubt, set it. Won't have an effect on machines not requiring it.
164
+ Defaults to 3h
160
165
 
161
166
  Note:
162
167
  Since a teamspace can either be owned by an org or by a user directly,
@@ -227,6 +232,7 @@ class Deployment:
227
232
  quantity=quantity,
228
233
  include_credentials=include_credentials if include_credentials is not None else True,
229
234
  cloudspace_id=cloudspace_id,
235
+ max_runtime=max_runtime,
230
236
  ),
231
237
  strategy=to_strategy(release_strategy),
232
238
  ),
@@ -262,6 +268,7 @@ class Deployment:
262
268
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
263
269
  quantity: Optional[int] = None,
264
270
  include_credentials: Optional[bool] = None,
271
+ max_runtime: Optional[int] = None,
265
272
  ) -> None:
266
273
  cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
267
274
 
@@ -288,6 +295,7 @@ class Deployment:
288
295
  release_strategy=release_strategy,
289
296
  quantity=quantity,
290
297
  include_credentials=include_credentials,
298
+ max_runtime=max_runtime,
291
299
  )
292
300
 
293
301
  def stop(self) -> None: