lightning-sdk 2025.7.10__py3-none-any.whl → 2025.7.22__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 (93) hide show
  1. lightning_sdk/__init__.py +3 -2
  2. lightning_sdk/api/cloud_account_api.py +154 -0
  3. lightning_sdk/api/deployment_api.py +11 -0
  4. lightning_sdk/api/job_api.py +9 -0
  5. lightning_sdk/api/llm_api.py +11 -6
  6. lightning_sdk/api/mmt_api.py +9 -0
  7. lightning_sdk/api/pipeline_api.py +4 -3
  8. lightning_sdk/api/studio_api.py +19 -5
  9. lightning_sdk/cli/clusters_menu.py +3 -3
  10. lightning_sdk/cli/create.py +22 -10
  11. lightning_sdk/cli/deploy/_auth.py +19 -3
  12. lightning_sdk/cli/deploy/serve.py +18 -4
  13. lightning_sdk/cli/entrypoint.py +1 -1
  14. lightning_sdk/cli/start.py +37 -7
  15. lightning_sdk/deployment/deployment.py +8 -0
  16. lightning_sdk/job/base.py +37 -5
  17. lightning_sdk/job/job.py +28 -4
  18. lightning_sdk/job/v1.py +10 -1
  19. lightning_sdk/job/v2.py +15 -1
  20. lightning_sdk/lightning_cloud/openapi/__init__.py +15 -1
  21. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +335 -0
  22. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +214 -0
  23. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +5 -1
  24. lightning_sdk/lightning_cloud/openapi/api/user_service_api.py +11 -11
  25. lightning_sdk/lightning_cloud/openapi/models/__init__.py +15 -1
  26. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +29 -3
  27. lightning_sdk/lightning_cloud/openapi/models/blogposts_id_body.py +53 -1
  28. lightning_sdk/lightning_cloud/openapi/models/{v1_list_new_features_for_user_response.py → conversations_id_body1.py} +23 -23
  29. lightning_sdk/lightning_cloud/openapi/models/messages_id_body.py +123 -0
  30. lightning_sdk/lightning_cloud/openapi/models/project_id_schedules_body.py +29 -3
  31. lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +1 -27
  32. lightning_sdk/lightning_cloud/openapi/models/protobuf_null_value.py +102 -0
  33. lightning_sdk/lightning_cloud/openapi/models/schedules_id_body.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +1 -27
  35. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +3 -55
  36. lightning_sdk/lightning_cloud/openapi/models/user_id_upgradetrigger_body.py +175 -0
  37. lightning_sdk/lightning_cloud/openapi/models/v1_ai_pod_v1.py +123 -0
  38. lightning_sdk/lightning_cloud/openapi/models/v1_artifact.py +27 -1
  39. lightning_sdk/lightning_cloud/openapi/models/v1_assistant_session_daily_aggregated.py +357 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_blog_post.py +53 -1
  41. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
  42. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  43. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +1 -0
  44. lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload.py +3 -55
  45. lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
  46. lightning_sdk/lightning_cloud/openapi/models/v1_create_billing_upgrade_trigger_record_response.py +97 -0
  47. lightning_sdk/lightning_cloud/openapi/models/v1_create_blog_post_request.py +53 -1
  48. lightning_sdk/lightning_cloud/openapi/models/v1_create_checkout_session_request.py +27 -1
  49. lightning_sdk/lightning_cloud/openapi/models/v1_create_subscription_checkout_session_request.py +29 -3
  50. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -1
  51. lightning_sdk/lightning_cloud/openapi/models/v1_function_tool.py +175 -0
  52. lightning_sdk/lightning_cloud/openapi/models/v1_get_artifacts_page_response.py +29 -3
  53. lightning_sdk/lightning_cloud/openapi/models/v1_get_assistant_session_daily_aggregated_response.py +201 -0
  54. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +79 -1
  55. lightning_sdk/lightning_cloud/openapi/models/v1_lightningapp_instance_artifact.py +27 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_like_status.py +104 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_list_notification_dialogs_response.py +149 -0
  58. lightning_sdk/lightning_cloud/openapi/models/v1_list_published_managed_endpoint_models_response.py +123 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_message.py +53 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_presigned_url.py +1 -53
  61. lightning_sdk/lightning_cloud/openapi/models/v1_project.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_schedule.py +27 -1
  64. lightning_sdk/lightning_cloud/openapi/models/v1_tool.py +149 -0
  65. lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_like_response.py +149 -0
  66. lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_message_like_response.py +149 -0
  67. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +105 -261
  68. lightning_sdk/lightning_cloud/openapi/models/v1_volume.py +27 -1
  69. lightning_sdk/llm/llm.py +32 -5
  70. lightning_sdk/llm/public_assistants.json +3 -1
  71. lightning_sdk/machine.py +24 -1
  72. lightning_sdk/mmt/base.py +20 -2
  73. lightning_sdk/mmt/mmt.py +25 -3
  74. lightning_sdk/mmt/v1.py +7 -1
  75. lightning_sdk/mmt/v2.py +21 -2
  76. lightning_sdk/organization.py +4 -0
  77. lightning_sdk/pipeline/pipeline.py +16 -5
  78. lightning_sdk/pipeline/printer.py +5 -3
  79. lightning_sdk/pipeline/schedule.py +844 -1
  80. lightning_sdk/pipeline/steps.py +19 -4
  81. lightning_sdk/sandbox.py +4 -1
  82. lightning_sdk/serve.py +2 -0
  83. lightning_sdk/studio.py +79 -39
  84. lightning_sdk/teamspace.py +14 -8
  85. lightning_sdk/utils/resolve.py +29 -2
  86. {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/METADATA +1 -1
  87. {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/RECORD +92 -78
  88. lightning_sdk/api/cluster_api.py +0 -119
  89. /lightning_sdk/cli/{inspect.py → inspection.py} +0 -0
  90. {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/LICENSE +0 -0
  91. {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/WHEEL +0 -0
  92. {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/entry_points.txt +0 -0
  93. {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py CHANGED
@@ -4,7 +4,7 @@ from lightning_sdk.constants import __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__ # noq
4
4
  from lightning_sdk.deployment import Deployment
5
5
  from lightning_sdk.helpers import _check_version_and_prompt_upgrade, _set_tqdm_envvars_noninteractive
6
6
  from lightning_sdk.job import Job
7
- from lightning_sdk.machine import Machine
7
+ from lightning_sdk.machine import CloudProvider, Machine
8
8
  from lightning_sdk.mmt import MMT
9
9
  from lightning_sdk.organization import Organization
10
10
  from lightning_sdk.plugin import JobsPlugin, MultiMachineTrainingPlugin, Plugin, SlurmJobsPlugin
@@ -16,6 +16,7 @@ from lightning_sdk.user import User
16
16
  __all__ = [
17
17
  "AIHub",
18
18
  "Agent",
19
+ "CloudProvider",
19
20
  "Deployment",
20
21
  "Job",
21
22
  "JobsPlugin",
@@ -31,6 +32,6 @@ __all__ = [
31
32
  "User",
32
33
  ]
33
34
 
34
- __version__ = "2025.07.10"
35
+ __version__ = "2025.07.22"
35
36
  _check_version_and_prompt_upgrade(__version__)
36
37
  _set_tqdm_envvars_noninteractive()
@@ -0,0 +1,154 @@
1
+ from typing import TYPE_CHECKING, Dict, List, Optional, Union
2
+
3
+ from lightning_sdk.lightning_cloud.openapi import (
4
+ Externalv1Cluster,
5
+ V1CloudProvider,
6
+ V1ClusterType,
7
+ V1ExternalCluster,
8
+ V1ListClusterAcceleratorsResponse,
9
+ )
10
+ from lightning_sdk.lightning_cloud.rest_client import LightningClient
11
+
12
+ if TYPE_CHECKING:
13
+ from lightning_sdk.machine import CloudProvider
14
+
15
+
16
+ class CloudAccountApi:
17
+ """Internal API client for API requests to cluster endpoints."""
18
+
19
+ def __init__(self) -> None:
20
+ self._client = LightningClient(max_tries=7)
21
+
22
+ def get_cloud_account(self, cloud_account_id: str, teamspace_id: str, org_id: str) -> Externalv1Cluster:
23
+ """Gets the cluster from given params cluster_id, project_id and owner.
24
+
25
+ Args:
26
+ cloud_account_id: the cloud account to get
27
+ teamspace_id: the teamspace the cloud_account is supposed to be associated with
28
+ org_id: The owning org of this teamspace
29
+
30
+ """
31
+ res = self._client.cluster_service_get_cluster(id=cloud_account_id, org_id=org_id, project_id=teamspace_id)
32
+ if not res:
33
+ raise ValueError(f"CloudAccount {cloud_account_id} does not exist")
34
+ return res
35
+
36
+ def list_cloud_accounts(self, teamspace_id: str) -> List[V1ExternalCluster]:
37
+ """Lists the cloud accounts for a given teamspace.
38
+
39
+ Args:
40
+ teamspace_id: The teamspace to list cloud accounts for
41
+
42
+ Returns:
43
+ A list of cloud accounts
44
+ """
45
+ res = self._client.cluster_service_list_project_clusters(
46
+ project_id=teamspace_id,
47
+ )
48
+ return res.clusters
49
+
50
+ def get_cloud_account_non_org(self, teamspace_id: str, cloud_account_id: str) -> Optional[V1ExternalCluster]:
51
+ for cluster in self.list_cloud_accounts(teamspace_id=teamspace_id):
52
+ if cluster.id == cloud_account_id:
53
+ return cluster
54
+
55
+ return None
56
+
57
+ def list_cloud_account_accelerators(self, cloud_account_id: str, org_id: str) -> V1ListClusterAcceleratorsResponse:
58
+ """Lists the accelerators for a given cloud account.
59
+
60
+ Args:
61
+ cloud_account_id: cluster ID to list accelerators for
62
+ org_id: The owning org of this project
63
+ """
64
+ res = self._client.cluster_service_list_cluster_accelerators(
65
+ id=cloud_account_id,
66
+ org_id=org_id,
67
+ )
68
+ if not res:
69
+ raise ValueError(f"CloudAccount {cloud_account_id} does not exist")
70
+ return res
71
+
72
+ def list_global_cloud_accounts(self, teamspace_id: str) -> List[V1ExternalCluster]:
73
+ """Lists the accelerators for a given teamspace.
74
+
75
+ Args:
76
+ teamspace_id: id of the teamspace to get the associated cloud_accounts for
77
+ """
78
+ cloud_accounts = self.list_cloud_accounts(teamspace_id=teamspace_id)
79
+ if not cloud_accounts:
80
+ raise ValueError(f"Teamspace {teamspace_id} does not exist")
81
+ filtered_cloud_accounts = filter(lambda x: x.spec.cluster_type == V1ClusterType.GLOBAL, cloud_accounts)
82
+ return list(filtered_cloud_accounts)
83
+
84
+ def get_cloud_account_provider_mapping(self, teamspace_id: str) -> Dict["CloudProvider", str]:
85
+ """Gets the cloud account <-> provider mapping."""
86
+ res = self.list_global_cloud_accounts(teamspace_id=teamspace_id)
87
+ return {self._get_cloud_account_provider(cloud_account): cloud_account.id for cloud_account in res}
88
+
89
+ @staticmethod
90
+ def _get_cloud_account_provider(cloud_account: Optional[V1ExternalCluster]) -> "CloudProvider":
91
+ """Determines the cloud provider based on the cloud_account configuration.
92
+
93
+ Args:
94
+ cloud_account: An optional Externalv1Cluster object containing cluster specifications
95
+
96
+ Returns:
97
+ CloudProvider: The determined cloud provider, defaults to AWS if no match is found
98
+ """
99
+ from lightning_sdk.machine import CloudProvider
100
+
101
+ if not cloud_account:
102
+ return CloudProvider.AWS
103
+
104
+ if cloud_account.spec and cloud_account.spec.driver:
105
+ if cloud_account.spec.driver == V1CloudProvider.LIGHTNING:
106
+ return CloudProvider.LIGHTNING
107
+
108
+ if cloud_account.spec.driver == V1CloudProvider.DGX:
109
+ return CloudProvider.DGX
110
+
111
+ if cloud_account.spec:
112
+ if cloud_account.spec.aws_v1:
113
+ return CloudProvider.AWS
114
+ if cloud_account.spec.google_cloud_v1:
115
+ return CloudProvider.GCP
116
+ if cloud_account.spec.lambda_labs_v1:
117
+ return CloudProvider.LAMBDA_LABS
118
+ if cloud_account.spec.vultr_v1:
119
+ return CloudProvider.VULTR
120
+ if cloud_account.spec.voltage_park_v1:
121
+ return CloudProvider.VOLTAGE_PARK
122
+ if cloud_account.spec.nebius_v1:
123
+ return CloudProvider.NEBIUS
124
+
125
+ return CloudProvider.AWS
126
+
127
+ def resolve_cloud_account(
128
+ self,
129
+ teamspace_id: str,
130
+ cloud_account: Optional[str],
131
+ cloud_provider: Optional[Union["CloudProvider", str]],
132
+ default_cloud_account: Optional[str],
133
+ ) -> Optional[str]:
134
+ if cloud_account:
135
+ if cloud_provider:
136
+ cloud_account_resp = self.get_cloud_account_non_org(teamspace_id, cloud_account)
137
+ cloud_provider_resp = self._get_cloud_account_provider(cloud_account_resp)
138
+ if cloud_provider_resp != cloud_provider:
139
+ raise RuntimeError(
140
+ f"Specified both cloud_provider ({cloud_provider}) and "
141
+ "cloud_account ({cloud_account} has cloud provider {cloud_provider_resp}) which don't match!"
142
+ )
143
+
144
+ return cloud_account
145
+
146
+ if cloud_provider:
147
+ cloud_account_mapping = self.get_cloud_account_provider_mapping(teamspace_id=teamspace_id)
148
+ if cloud_provider and cloud_provider in cloud_account_mapping:
149
+ return cloud_account_mapping[cloud_provider]
150
+
151
+ if default_cloud_account:
152
+ return default_cloud_account
153
+
154
+ return None
@@ -264,6 +264,7 @@ class DeploymentApi:
264
264
  custom_domain: Optional[str] = None,
265
265
  quantity: Optional[int] = None,
266
266
  include_credentials: Optional[bool] = None,
267
+ max_runtime: Optional[int] = None,
267
268
  ) -> V1Deployment:
268
269
  # Update the deployment in place
269
270
 
@@ -291,6 +292,9 @@ class DeploymentApi:
291
292
  requires_release |= apply_change(deployment.spec, "spot", spot)
292
293
  requires_release |= apply_change(deployment.spec, "quantity", quantity)
293
294
  requires_release |= apply_change(deployment.spec, "include_credentials", include_credentials)
295
+ requires_release |= apply_change(
296
+ deployment.spec, "requested_run_duration_seconds", str(max_runtime) if max_runtime is not None else None
297
+ )
294
298
 
295
299
  if requires_release:
296
300
  if deployment.strategy is None:
@@ -569,6 +573,7 @@ def to_spec(
569
573
  quantity: Optional[int] = None,
570
574
  include_credentials: Optional[bool] = None,
571
575
  cloudspace_id: Optional[None] = None,
576
+ max_runtime: Optional[int] = None,
572
577
  ) -> V1JobSpec:
573
578
  if cloud_account is None:
574
579
  raise ValueError("The cloud account should be defined.")
@@ -585,6 +590,11 @@ def to_spec(
585
590
  if command is None and cloudspace_id is not None:
586
591
  raise ValueError("The command should be defined.")
587
592
 
593
+ # need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
594
+ optional_spec_kwargs = {}
595
+ if max_runtime:
596
+ optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
597
+
588
598
  return V1JobSpec(
589
599
  cluster_id=cloud_account,
590
600
  command=command,
@@ -597,6 +607,7 @@ def to_spec(
597
607
  quantity=quantity,
598
608
  include_credentials=include_credentials,
599
609
  cloudspace_id=cloudspace_id,
610
+ **optional_spec_kwargs,
600
611
  )
601
612
 
602
613
 
@@ -215,6 +215,7 @@ class JobApiV2:
215
215
  path_mappings: Optional[Dict[str, str]],
216
216
  artifacts_local: Optional[str], # deprecated in favor of path_mappings
217
217
  artifacts_remote: Optional[str], # deprecated in favor of path_mappings
218
+ max_runtime: Optional[str] = None,
218
219
  ) -> V1Job:
219
220
  body = self._create_job_body(
220
221
  name=name,
@@ -231,6 +232,7 @@ class JobApiV2:
231
232
  path_mappings=path_mappings,
232
233
  artifacts_local=artifacts_local,
233
234
  artifacts_remote=artifacts_remote,
235
+ max_runtime=max_runtime,
234
236
  )
235
237
 
236
238
  job: V1Job = self._client.jobs_service_create_job(project_id=teamspace_id, body=body)
@@ -252,6 +254,7 @@ class JobApiV2:
252
254
  path_mappings: Optional[Dict[str, str]],
253
255
  artifacts_local: Optional[str], # deprecated in favor of path_mappings
254
256
  artifacts_remote: Optional[str], # deprecated in favor of path_mappings)
257
+ max_runtime: Optional[int] = None,
255
258
  ) -> ProjectIdJobsBody:
256
259
  env_vars = []
257
260
  if env is not None:
@@ -268,6 +271,11 @@ class JobApiV2:
268
271
  artifacts_remote=artifacts_remote,
269
272
  )
270
273
 
274
+ # need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
275
+ optional_spec_kwargs = {}
276
+ if max_runtime:
277
+ optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
278
+
271
279
  spec = V1JobSpec(
272
280
  cloudspace_id=studio_id or "",
273
281
  cluster_id=cloud_account or "",
@@ -281,6 +289,7 @@ class JobApiV2:
281
289
  image_cluster_credentials=cloud_account_auth,
282
290
  image_secret_ref=image_credentials or "",
283
291
  path_mappings=path_mappings_list,
292
+ **optional_spec_kwargs,
284
293
  )
285
294
  return ProjectIdJobsBody(name=name, spec=spec)
286
295
 
@@ -7,11 +7,12 @@ import threading
7
7
  import warnings
8
8
  from typing import Any, AsyncGenerator, Dict, Generator, List, Optional, Union
9
9
 
10
- from pip._vendor.urllib3 import HTTPResponse
11
-
12
- from lightning_sdk.lightning_cloud.openapi.models.v1_conversation_response_chunk import V1ConversationResponseChunk
13
- from lightning_sdk.lightning_cloud.openapi.models.v1_response_choice import V1ResponseChoice
14
- from lightning_sdk.lightning_cloud.openapi.models.v1_response_choice_delta import V1ResponseChoiceDelta
10
+ from lightning_sdk.lightning_cloud.openapi.models import (
11
+ StreamResultOfV1ConversationResponseChunk,
12
+ V1ConversationResponseChunk,
13
+ V1ResponseChoice,
14
+ V1ResponseChoiceDelta,
15
+ )
15
16
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
16
17
 
17
18
 
@@ -54,7 +55,9 @@ class LLMApi:
54
55
  warnings.warn("Error decoding JSON:", decoded_line)
55
56
  return None
56
57
 
57
- def _stream_chat_response(self, result: HTTPResponse) -> Generator[V1ConversationResponseChunk, None, None]:
58
+ def _stream_chat_response(
59
+ self, result: StreamResultOfV1ConversationResponseChunk
60
+ ) -> Generator[V1ConversationResponseChunk, None, None]:
58
61
  for line in result.stream():
59
62
  decoded_lines = line.decode("utf-8").strip()
60
63
  for decoded_line in decoded_lines.splitlines():
@@ -80,6 +83,7 @@ class LLMApi:
80
83
  name: Optional[str] = None,
81
84
  metadata: Optional[Dict[str, str]] = None,
82
85
  stream: bool = False,
86
+ tools: Optional[List[Dict[str, Any]]] = None,
83
87
  **kwargs: Any,
84
88
  ) -> Union[V1ConversationResponseChunk, Generator[V1ConversationResponseChunk, None, None]]:
85
89
  is_internal_conversation = os.getenv("LIGHTNING_INTERNAL_CONVERSATION", "false").lower() == "true"
@@ -105,6 +109,7 @@ class LLMApi:
105
109
  "ephemeral": ephemeral,
106
110
  "parent_conversation_id": kwargs.get("parent_conversation_id", ""),
107
111
  "parent_message_id": kwargs.get("parent_message_id", ""),
112
+ "tools": tools,
108
113
  }
109
114
  if images:
110
115
  for image in images:
@@ -87,6 +87,7 @@ class MMTApiV2:
87
87
  path_mappings: Optional[Dict[str, str]],
88
88
  artifacts_local: Optional[str], # deprecated in favor of path_mappings
89
89
  artifacts_remote: Optional[str], # deprecated in favor of path_mappings
90
+ max_runtime: Optional[int],
90
91
  ) -> V1MultiMachineJob:
91
92
  body = self._create_mmt_body(
92
93
  name=name,
@@ -104,6 +105,7 @@ class MMTApiV2:
104
105
  path_mappings=path_mappings,
105
106
  artifacts_local=artifacts_local, # deprecated in favor of path_mappings
106
107
  artifacts_remote=artifacts_remote, # deprecated in favor of path_mappings
108
+ max_runtime=max_runtime,
107
109
  )
108
110
 
109
111
  job: V1MultiMachineJob = self._client.jobs_service_create_multi_machine_job(project_id=teamspace_id, body=body)
@@ -126,6 +128,7 @@ class MMTApiV2:
126
128
  path_mappings: Optional[Dict[str, str]],
127
129
  artifacts_local: Optional[str], # deprecated in favor of path_mappings
128
130
  artifacts_remote: Optional[str], # deprecated in favor of path_mappings
131
+ max_runtime: Optional[int] = None,
129
132
  ) -> ProjectIdMultimachinejobsBody:
130
133
  env_vars = []
131
134
  if env is not None:
@@ -142,6 +145,11 @@ class MMTApiV2:
142
145
  artifacts_remote=artifacts_remote,
143
146
  )
144
147
 
148
+ # need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
149
+ optional_spec_kwargs = {}
150
+ if max_runtime:
151
+ optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
152
+
145
153
  spec = V1JobSpec(
146
154
  cloudspace_id=studio_id or "",
147
155
  cluster_id=cloud_account or "",
@@ -155,6 +163,7 @@ class MMTApiV2:
155
163
  image_cluster_credentials=cloud_account_auth,
156
164
  image_secret_ref=image_credentials or "",
157
165
  path_mappings=path_mappings_list,
166
+ **optional_spec_kwargs,
158
167
  )
159
168
  return ProjectIdMultimachinejobsBody(
160
169
  name=name, spec=spec, cluster_id=cloud_account or "", machines=num_machines
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, List, Optional, Union
2
2
 
3
- from lightning_sdk.api.cluster_api import ClusterApi
3
+ from lightning_sdk.api.cloud_account_api import CloudAccountApi
4
4
  from lightning_sdk.lightning_cloud.openapi.models import (
5
5
  ProjectIdPipelinesBody,
6
6
  ProjectIdSchedulesBody,
@@ -23,7 +23,7 @@ class PipelineApi:
23
23
 
24
24
  def __init__(self) -> None:
25
25
  self._client = LightningClient(max_tries=0, retry=False)
26
- self._cluster_api = ClusterApi()
26
+ self._cloud_account_api = CloudAccountApi()
27
27
 
28
28
  def get_pipeline_by_id(self, project_id: str, pipeline_id_or_name: str) -> Optional[V1Pipeline]:
29
29
  if pipeline_id_or_name.startswith("pip_"):
@@ -75,6 +75,7 @@ class PipelineApi:
75
75
  resource_id=pipeline.id,
76
76
  parent_resource_id=parent_pipeline_id or "",
77
77
  resource_type=V1ScheduleResourceType.PIPELINE,
78
+ timezone=schedule.timezone,
78
79
  )
79
80
 
80
81
  self._client.schedules_service_create_schedule(body, teamspace.id)
@@ -97,7 +98,7 @@ class PipelineApi:
97
98
 
98
99
  from lightning_sdk.pipeline.utils import _get_cloud_account
99
100
 
100
- clusters = self._cluster_api.list_clusters(project_id=teamspace.id)
101
+ clusters = self._cloud_account_api.list_cloud_accounts(teamspace_id=teamspace.id)
101
102
 
102
103
  selected_cluster = None
103
104
  selected_cluster_id = _get_cloud_account(steps)
@@ -121,7 +121,7 @@ class StudioApi:
121
121
  name: str,
122
122
  teamspace_id: str,
123
123
  cloud_account: Optional[str] = None,
124
- source: Optional[V1CloudSpaceSourceType] = None,
124
+ source: Optional[Union[V1CloudSpaceSourceType, str]] = None,
125
125
  disable_secrets: bool = False,
126
126
  sandbox: bool = False,
127
127
  cloud_space_environment_template_id: Optional[str] = None,
@@ -168,12 +168,26 @@ class StudioApi:
168
168
  return startup_status and startup_status.top_up_restore_finished
169
169
 
170
170
  def start_studio(
171
- self, studio_id: str, teamspace_id: str, machine: Union[Machine, str], interruptible: False
171
+ self,
172
+ studio_id: str,
173
+ teamspace_id: str,
174
+ machine: Union[Machine, str],
175
+ interruptible: bool = False,
176
+ max_runtime: Optional[int] = None,
172
177
  ) -> None:
173
178
  """Start an existing Studio."""
179
+ # need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
180
+ optional_kwargs_compute_body = {}
181
+
182
+ if max_runtime is not None:
183
+ optional_kwargs_compute_body["requested_run_duration_seconds"] = str(max_runtime)
174
184
  self._client.cloud_space_service_start_cloud_space_instance(
175
185
  IdStartBody(
176
- compute_config=V1UserRequestedComputeConfig(name=_machine_to_compute_name(machine), spot=interruptible)
186
+ compute_config=V1UserRequestedComputeConfig(
187
+ name=_machine_to_compute_name(machine),
188
+ spot=interruptible,
189
+ **optional_kwargs_compute_body,
190
+ )
177
191
  ),
178
192
  teamspace_id,
179
193
  studio_id,
@@ -398,7 +412,7 @@ class StudioApi:
398
412
  body=body,
399
413
  )
400
414
 
401
- def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str, str]:
415
+ def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str, Any]:
402
416
  """Duplicates the given Studio from a given Teamspace into a given target Teamspace."""
403
417
  target_teamspace = self._client.projects_service_get_project(target_teamspace_id)
404
418
  init_kwargs = {}
@@ -421,7 +435,7 @@ class StudioApi:
421
435
  init_kwargs["name"] = new_cloudspace.name
422
436
  init_kwargs["teamspace"] = target_teamspace.name
423
437
 
424
- self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False)
438
+ self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False, None)
425
439
  return init_kwargs
426
440
 
427
441
  def delete_studio(self, studio_id: str, teamspace_id: str) -> None:
@@ -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,13 @@ 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
15
  _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])
16
+ _PROVIDER_VALUES = tuple([provider.value for provider in CloudProvider])
16
17
 
17
18
 
18
19
  @click.group("create")
@@ -48,16 +49,26 @@ def create() -> None:
48
49
  ),
49
50
  )
50
51
  @click.option(
51
- "--provider",
52
+ "--cloud-provider",
52
53
  default=None,
53
54
  type=click.Choice(_PROVIDER_VALUES),
54
55
  help="The provider to create the studio on. If --cloud-account is specified, this option is prioritized.",
55
56
  )
57
+ @click.option(
58
+ "--provider",
59
+ default=None,
60
+ type=click.Choice(_PROVIDER_VALUES),
61
+ help=(
62
+ "Deprecated. Use --cloud-provider instead. The provider to create the studio on. "
63
+ "If --cloud-account is specified, this option is prioritized."
64
+ ),
65
+ )
56
66
  def studio(
57
67
  name: str,
58
68
  teamspace: Optional[str] = None,
59
69
  start: Optional[str] = None,
60
70
  cloud_account: Optional[str] = None,
71
+ cloud_provider: Optional[str] = None,
61
72
  provider: Optional[str] = None,
62
73
  ) -> None:
63
74
  """Create a new studio on the Lightning AI platform.
@@ -70,12 +81,13 @@ def studio(
70
81
  menu = _TeamspacesMenu()
71
82
  teamspace_resolved = menu._resolve_teamspace(teamspace)
72
83
 
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]
84
+ cloud_provider = str(_resolve_deprecated_provider(cloud_provider, provider))
85
+
86
+ if cloud_provider is not None:
87
+ cloud_account_api = CloudAccountApi()
88
+ cloud_account = cloud_account_api.resolve_cloud_account(
89
+ teamspace_resolved.id, cloud_account, cloud_provider, teamspace_resolved.default_cloud_account
90
+ )
79
91
 
80
92
  # default cloud account to current studios cloud account if run from studio
81
93
  # 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:
@@ -317,12 +317,20 @@ def _handle_cloud(
317
317
  ls_deployer = _LitServeDeployer(name=deployment_name, teamspace=None)
318
318
  path = ls_deployer.dockerize_api(script_path, port=port, gpu=not machine.is_cpu(), tag=tag, print_success=False)
319
319
 
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)
320
+ console.print(f"\n[bold]LitServe generated a Dockerfile at:[/bold]\n[u]{path}[/u]\n")
321
+ console.print("Please check that it matches your server setup.")
322
+ correct_dockerfile = (
323
+ True
324
+ if non_interactive
325
+ else Confirm.ask("Have you reviewed the Dockerfile and confirmed it's correct?", default=True)
326
+ )
322
327
  if not correct_dockerfile:
323
- console.print("Please fix the Dockerfile and try again.", style="red")
328
+ console.print("[red]Dockerfile review canceled. Please update the Dockerfile and try again.[/red]")
324
329
  return
325
330
 
331
+ console.print(
332
+ "Building your container image now.\n[cyan bold]Make sure Docker is installed and running.[/cyan bold]\n"
333
+ )
326
334
  with Progress(
327
335
  SpinnerColumn(),
328
336
  TextColumn("[progress.description]{task.description}"),
@@ -340,7 +348,12 @@ def _handle_cloud(
340
348
  progress.remove_task(build_task)
341
349
 
342
350
  except Exception as e:
343
- console.print(f"❌ Deployment failed: {e}", style="red")
351
+ console.print(
352
+ "❌ Failed to build a container for your server.\n"
353
+ "Make sure Docker is installed and running, then try again.",
354
+ f"\n\nError details: {e}",
355
+ style="red",
356
+ )
344
357
  return
345
358
 
346
359
  # Push the container to the registry
@@ -373,6 +386,7 @@ def _handle_cloud(
373
386
  f"{resolved_teamspace.owner.name}/{resolved_teamspace.name}/{container_basename}"
374
387
  )
375
388
 
389
+ cloud_account = cloud_account or resolved_teamspace.default_cloud_account
376
390
  if from_onboarding:
377
391
  thread = Thread(
378
392
  target=ls_deployer.run_on_cloud,
@@ -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