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
@@ -25,11 +25,11 @@ from lightning_sdk.lightning_cloud.openapi.models import (
25
25
  from lightning_sdk.machine import Machine
26
26
  from lightning_sdk.mmt.v2 import MMTApiV2
27
27
  from lightning_sdk.pipeline.utils import DEFAULT, _get_studio, _to_wait_for, _validate_cloud_account
28
- from lightning_sdk.studio import Studio
28
+ from lightning_sdk.studio import CloudAccountApi, Studio
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from lightning_sdk.organization import Organization
32
- from lightning_sdk.teamspace import Teamspace
32
+ from lightning_sdk.teamspace import CloudProvider, Teamspace
33
33
  from lightning_sdk.user import User
34
34
 
35
35
 
@@ -57,6 +57,7 @@ class DeploymentStep:
57
57
  custom_domain: Optional[str] = None,
58
58
  quantity: Optional[int] = None,
59
59
  include_credentials: Optional[bool] = None,
60
+ max_runtime: Optional[int] = None,
60
61
  wait_for: Optional[Union[str, List[str]]] = DEFAULT,
61
62
  ) -> None:
62
63
  self.name = name
@@ -96,6 +97,7 @@ class DeploymentStep:
96
97
  self.custom_domain = custom_domain
97
98
  self.quantity = quantity
98
99
  self.include_credentials = include_credentials or True
100
+ self.max_runtime = max_runtime
99
101
  self.wait_for = wait_for
100
102
 
101
103
  def to_proto(
@@ -124,6 +126,7 @@ class DeploymentStep:
124
126
  quantity=self.quantity,
125
127
  cloudspace_id=self.studio._studio.id if self.studio else None,
126
128
  include_credentials=self.include_credentials,
129
+ max_runtime=self.max_runtime,
127
130
  ),
128
131
  strategy=to_strategy(self.release_strategy),
129
132
  ),
@@ -144,12 +147,14 @@ class JobStep:
144
147
  org: Union[str, "Organization", None] = None,
145
148
  user: Union[str, "User", None] = None,
146
149
  cloud_account: Optional[str] = None,
150
+ cloud_provider: Optional[Union["CloudProvider", str]] = None,
147
151
  env: Optional[Dict[str, str]] = None,
148
152
  interruptible: bool = False,
149
153
  image_credentials: Optional[str] = None,
150
154
  cloud_account_auth: bool = False,
151
155
  entrypoint: str = "sh -c",
152
156
  path_mappings: Optional[Dict[str, str]] = None,
157
+ max_runtime: Optional[int] = None,
153
158
  wait_for: Union[str, List[str], None] = DEFAULT,
154
159
  ) -> None:
155
160
  self.name = name
@@ -168,12 +173,14 @@ class JobStep:
168
173
  self.org = org
169
174
  self.user = user
170
175
  self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
176
+ self.cloud_provider = cloud_provider
171
177
  self.env = env
172
178
  self.interruptible = interruptible
173
179
  self.image_credentials = image_credentials
174
180
  self.cloud_account_auth = cloud_account_auth
175
181
  self.entrypoint = entrypoint
176
182
  self.path_mappings = path_mappings
183
+ self.max_runtime = max_runtime
177
184
  self.wait_for = wait_for
178
185
 
179
186
  def to_proto(
@@ -186,12 +193,16 @@ class JobStep:
186
193
  elif studio.cloud_account != self.cloud_account:
187
194
  raise ValueError("The provided cloud account doesn't match the studio")
188
195
 
189
- _validate_cloud_account(cloud_account, self.cloud_account, shared_filesystem)
196
+ resolved_cloud_account = CloudAccountApi().resolve_cloud_account(
197
+ teamspace.id, self.cloud_account, self.cloud_provider, teamspace.default_cloud_account
198
+ )
199
+
200
+ _validate_cloud_account(cloud_account, resolved_cloud_account, shared_filesystem)
190
201
 
191
202
  body = JobApiV2._create_job_body(
192
203
  name=self.name,
193
204
  command=self.command,
194
- cloud_account=self.cloud_account or cloud_account,
205
+ cloud_account=resolved_cloud_account or cloud_account,
195
206
  studio_id=studio._studio.id if isinstance(studio, Studio) else None,
196
207
  image=self.image,
197
208
  machine=self.machine,
@@ -203,6 +214,7 @@ class JobStep:
203
214
  path_mappings=self.path_mappings,
204
215
  artifacts_local=None,
205
216
  artifacts_remote=None,
217
+ max_runtime=self.max_runtime,
206
218
  )
207
219
 
208
220
  return V1PipelineStep(
@@ -234,6 +246,7 @@ class MMTStep:
234
246
  cloud_account_auth: bool = False,
235
247
  entrypoint: str = "sh -c",
236
248
  path_mappings: Optional[Dict[str, str]] = None,
249
+ max_runtime: Optional[int] = None,
237
250
  wait_for: Optional[Union[str, List[str]]] = DEFAULT,
238
251
  ) -> None:
239
252
  self.machine = machine or Machine.CPU
@@ -258,6 +271,7 @@ class MMTStep:
258
271
  self.cloud_account_auth = cloud_account_auth
259
272
  self.entrypoint = entrypoint
260
273
  self.path_mappings = path_mappings
274
+ self.max_runtime = max_runtime
261
275
  self.wait_for = wait_for
262
276
 
263
277
  def to_proto(
@@ -288,6 +302,7 @@ class MMTStep:
288
302
  path_mappings=self.path_mappings,
289
303
  artifacts_local=None, # deprecated in favor of path_mappings
290
304
  artifacts_remote=None, # deprecated in favor of path_mappings
305
+ max_runtime=self.max_runtime,
291
306
  )
292
307
 
293
308
  return V1PipelineStep(
lightning_sdk/sandbox.py CHANGED
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from datetime import datetime
5
5
  from typing import Optional, Type, Union
6
6
 
7
- from lightning_sdk.machine import Machine
7
+ from lightning_sdk.machine import CloudProvider, Machine
8
8
  from lightning_sdk.organization import Organization
9
9
  from lightning_sdk.status import Status
10
10
  from lightning_sdk.studio import Studio
@@ -31,6 +31,7 @@ class _Sandbox:
31
31
  org: The organization to use for the sandbox.
32
32
  user: The user to use for the sandbox.
33
33
  cloud_account: The cloud account to use for the sandbox.
34
+ cloud_provider: Selects the cloud account based on the available cloud accounts and the specified provider.
34
35
  disable_secrets: If true, user secrets such as LIGHTNING_API_KEY are not stored in the sandbox.
35
36
 
36
37
  Example:
@@ -48,6 +49,7 @@ class _Sandbox:
48
49
  org: Optional[Union[str, Organization]] = None,
49
50
  user: Optional[Union[str, User]] = None,
50
51
  cloud_account: Optional[str] = None,
52
+ cloud_provider: Optional[Union[CloudProvider, str]] = None,
51
53
  disable_secrets: bool = True,
52
54
  ) -> None:
53
55
  if name is None:
@@ -62,6 +64,7 @@ class _Sandbox:
62
64
  org=org,
63
65
  user=user,
64
66
  cloud_account=cloud_account,
67
+ cloud_provider=cloud_provider,
65
68
  disable_secrets=disable_secrets,
66
69
  )
67
70
 
lightning_sdk/serve.py CHANGED
@@ -101,6 +101,8 @@ Update [underline]{os.path.abspath("Dockerfile")}[/underline] to add any additio
101
101
 
102
102
  [bold]To push the container to a registry:[/bold]
103
103
  > [underline]docker push {tag}[/underline]
104
+
105
+ Check out [blue][link=https://lightning.ai/docs/litserve/features]the docs[/link][/blue] for more details.
104
106
  """
105
107
  console.print(success_msg)
106
108
  return os.path.abspath("Dockerfile")
lightning_sdk/studio.py CHANGED
@@ -1,23 +1,26 @@
1
1
  import glob
2
2
  import os
3
3
  import warnings
4
- from enum import Enum
5
4
  from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Tuple, Union
6
5
 
7
6
  from tqdm.auto import tqdm
8
7
 
9
- from lightning_sdk.api.cluster_api import ClusterApi
8
+ from lightning_sdk.api.cloud_account_api import CloudAccountApi
10
9
  from lightning_sdk.api.studio_api import StudioApi
11
- from lightning_sdk.api.utils import _machine_to_compute_name
12
10
  from lightning_sdk.constants import _LIGHTNING_DEBUG
13
- from lightning_sdk.lightning_cloud.openapi import V1CloudSpaceSourceType
14
- from lightning_sdk.machine import Machine
11
+ from lightning_sdk.machine import CloudProvider, Machine
15
12
  from lightning_sdk.organization import Organization
16
13
  from lightning_sdk.owner import Owner
17
14
  from lightning_sdk.status import Status
18
15
  from lightning_sdk.teamspace import Teamspace
19
16
  from lightning_sdk.user import User
20
- from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_teamspace, _setup_logger
17
+ from lightning_sdk.utils.resolve import (
18
+ _get_org_id,
19
+ _resolve_deprecated_cluster,
20
+ _resolve_deprecated_provider,
21
+ _resolve_teamspace,
22
+ _setup_logger,
23
+ )
21
24
 
22
25
  if TYPE_CHECKING:
23
26
  from lightning_sdk.job import Job
@@ -27,19 +30,6 @@ if TYPE_CHECKING:
27
30
  _logger = _setup_logger(__name__)
28
31
 
29
32
 
30
- class Provider(Enum):
31
- # Machine providers based on v1CloudProvider
32
- AWS = "AWS"
33
- GCP = "GCP"
34
- VULTR = "VULTR"
35
- LAMBDA_LABS = "LAMBDA_LABS"
36
- DGX = "DGX"
37
- VOLTAGE_PARK = "VOLTAGE_PARK"
38
- NEBIUS = "NEBIUS"
39
- CLOUDFLARE = "CLOUDFLARE"
40
- LIGHTNING = "LIGHTNING"
41
-
42
-
43
33
  class Studio:
44
34
  """A single Lightning AI Studio.
45
35
 
@@ -53,6 +43,9 @@ class Studio:
53
43
  user: the name of the user owning the :param`teamspace` in case it is owned directly by a user instead of an org
54
44
  cloud_account: the name of the cloud account, the studio should be created on.
55
45
  Doesn't matter when the studio already exists.
46
+ cloud_account_provider: The provider to select the cloud-account from.
47
+ If set, must be in agreement with the provider from the cloud_account (if specified).
48
+ If not specified, falls backto the teamspace default cloud account.
56
49
  create_ok: whether the studio will be created if it does not yet exist. Defaults to True
57
50
  provider: the provider of the machine, the studio should be created on.
58
51
 
@@ -72,34 +65,37 @@ class Studio:
72
65
  org: Optional[Union[str, Organization]] = None,
73
66
  user: Optional[Union[str, User]] = None,
74
67
  cloud_account: Optional[str] = None,
68
+ cloud_provider: Optional[Union[CloudProvider, str]] = None,
75
69
  create_ok: bool = True,
76
70
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
77
- provider: Optional[str] = None,
78
- source: Optional[V1CloudSpaceSourceType] = None,
71
+ source: Optional[str] = None,
79
72
  disable_secrets: bool = False,
73
+ provider: Optional[Union[CloudProvider, str]] = None, # deprecated in favor of cloud_provider
80
74
  ) -> None:
81
75
  self._studio_api = StudioApi()
82
- self._cluster_api = ClusterApi()
76
+ self._cloud_account_api = CloudAccountApi()
77
+
78
+ _teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
79
+ if _teamspace is None:
80
+ raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
83
81
 
84
- self._teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
82
+ self._teamspace = _teamspace
85
83
  self._cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
84
+
86
85
  self._setup_done = False
87
86
  self._disable_secrets = disable_secrets
88
87
 
89
88
  self._plugins = {}
90
89
 
91
- if self._teamspace is None:
92
- raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
90
+ cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
91
+ cloud_provider = _resolve_deprecated_provider(cloud_provider, provider)
93
92
 
94
- if provider is not None:
95
- if isinstance(provider, str) and provider in Provider.__members__:
96
- provider = Provider(provider)
97
- else:
98
- raise ValueError(f"Invalid provider: {provider}. Must be one of {Provider.__members__.keys()}.")
99
- self._cloud_account = self._cluster_api.get_cluster_provider_mapping(
100
- self._teamspace.id,
101
- self._teamspace.owner.id,
102
- )[provider.value]
93
+ self._cloud_account = self._cloud_account_api.resolve_cloud_account(
94
+ self._teamspace.id,
95
+ cloud_account=cloud_account,
96
+ cloud_provider=cloud_provider,
97
+ default_cloud_account=self._teamspace.default_cloud_account,
98
+ )
103
99
 
104
100
  if name is None:
105
101
  studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
@@ -121,6 +117,8 @@ class Studio:
121
117
  else:
122
118
  raise ValueError(f"Studio {name} does not exist.") from e
123
119
 
120
+ self._cloud_account = self._studio.cluster_id
121
+
124
122
  if (
125
123
  not self._skip_init
126
124
  and _internal_status_to_external_status(
@@ -178,7 +176,12 @@ class Studio:
178
176
  """Returns the current machine type the Studio is running on."""
179
177
  if self.status != Status.Running:
180
178
  return None
181
- return self._studio_api.get_machine(self._studio.id, self._teamspace.id)
179
+ return self._studio_api.get_machine(
180
+ self._studio.id,
181
+ self._teamspace.id,
182
+ self.cloud_account,
183
+ _get_org_id(self._teamspace),
184
+ )
182
185
 
183
186
  @property
184
187
  def interruptible(self) -> bool:
@@ -198,8 +201,23 @@ class Studio:
198
201
  def cloud_account(self) -> str:
199
202
  return self._studio.cluster_id
200
203
 
201
- def start(self, machine: Union[Machine, str] = Machine.CPU, interruptible: Optional[bool] = None) -> None:
202
- """Starts a Studio on the specified machine type (default: CPU-4)."""
204
+ def start(
205
+ self,
206
+ machine: Union[Machine, str] = Machine.CPU,
207
+ interruptible: Optional[bool] = None,
208
+ max_runtime: Optional[int] = None,
209
+ ) -> None:
210
+ """Starts a Studio on the specified machine type (default: CPU-4).
211
+
212
+ Args:
213
+ machine: the machine type to start the studio on. Defaults to CPU-4
214
+ interruptible: whether to use interruptible machines
215
+ max_runtime: the duration (in seconds) for which to allocate the machine.
216
+ Irrelevant for most machines, required for some of the top-end machines on GCP.
217
+ If in doubt, set it. Won't have an effect on machines not requiring it.
218
+ Defaults to 3h
219
+
220
+ """
203
221
  status = self.status
204
222
 
205
223
  if interruptible is None:
@@ -210,10 +228,12 @@ class Studio:
210
228
  interruptible = self.teamspace.start_studios_on_interruptible
211
229
 
212
230
  if status == Status.Running:
213
- curr_machine = _machine_to_compute_name(self.machine) if self.machine is not None else None
214
- if curr_machine != _machine_to_compute_name(machine):
231
+ new_machine = machine
232
+ if not isinstance(machine, Machine):
233
+ new_machine = Machine.from_str(machine)
234
+ if new_machine != self.machine:
215
235
  raise RuntimeError(
216
- f"Requested to start studio on {machine}, but studio is already running on {self.machine}."
236
+ f"Requested to start studio on {new_machine}, but studio is already running on {self.machine}."
217
237
  " Consider switching instead!"
218
238
  )
219
239
  _logger.info(f"Studio {self.name} is already running")
@@ -221,7 +241,9 @@ class Studio:
221
241
 
222
242
  if status != Status.Stopped:
223
243
  raise RuntimeError(f"Cannot start a studio that is not stopped. Studio {self.name} is {status}.")
224
- self._studio_api.start_studio(self._studio.id, self._teamspace.id, machine, interruptible=interruptible)
244
+ self._studio_api.start_studio(
245
+ self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
246
+ )
225
247
 
226
248
  self._setup()
227
249
 
@@ -236,9 +258,34 @@ class Studio:
236
258
  """Deletes the current Studio."""
237
259
  self._studio_api.delete_studio(self._studio.id, self._teamspace.id)
238
260
 
239
- def duplicate(self) -> "Studio":
240
- """Duplicates the existing Studio to the same teamspace."""
241
- kwargs = self._studio_api.duplicate_studio(self._studio.id, self._teamspace.id, self._teamspace.id)
261
+ def duplicate(self, target_teamspace: Optional[Union["Teamspace", str]] = None) -> "Studio":
262
+ """Duplicates the existing Studio.
263
+
264
+ Args:
265
+ target_teamspace: the teamspace to duplicate the studio to.
266
+ Must have the same owner as the source teamspace.
267
+ If not provided, defaults to current teamspace.
268
+ """
269
+ if target_teamspace is None:
270
+ target_teamspace_id = self._teamspace.id
271
+ else:
272
+ target_teamspace = _resolve_teamspace(
273
+ target_teamspace,
274
+ org=self._teamspace.owner if isinstance(self._teamspace.owner, Organization) else None,
275
+ user=self._teamspace.owner if isinstance(self._teamspace.owner, User) else None,
276
+ )
277
+
278
+ if target_teamspace is None:
279
+ raise ValueError(
280
+ f"Could not resolve target teamspace {target_teamspace} "
281
+ f"with owner {self.teamspace.owner} for duplication!"
282
+ )
283
+
284
+ target_teamspace_id = target_teamspace.id
285
+
286
+ kwargs = self._studio_api.duplicate_studio(
287
+ studio_id=self._studio.id, teamspace_id=self._teamspace.id, target_teamspace_id=target_teamspace_id
288
+ )
242
289
  return Studio(**kwargs)
243
290
 
244
291
  def switch_machine(self, machine: Union[Machine, str], interruptible: bool = False) -> None:
@@ -107,8 +107,8 @@ class Teamspace:
107
107
  from lightning_sdk.studio import Studio
108
108
 
109
109
  studios = []
110
- clusters = self._teamspace_api.list_cloud_accounts(teamspace_id=self.id)
111
- for cl in clusters:
110
+ cloud_accounts = self._teamspace_api.list_cloud_accounts(teamspace_id=self.id)
111
+ for cl in cloud_accounts:
112
112
  _studios = self._teamspace_api.list_studios(teamspace_id=self.id, cloud_account=cl.cluster_id)
113
113
  for s in _studios:
114
114
  studios.append(Studio(name=s.name, teamspace=self, cluster=cl.cluster_name, create_ok=False))
@@ -116,8 +116,11 @@ class Teamspace:
116
116
  return studios
117
117
 
118
118
  @property
119
- def default_cloud_account(self) -> str:
120
- return self._teamspace.project_settings.preferred_cluster
119
+ def default_cloud_account(self) -> Optional[str]:
120
+ owner_preferred_cluster = (
121
+ getattr(self.owner, "default_cloud_account", None) if isinstance(self.owner, Organization) else None
122
+ )
123
+ return self._teamspace.project_settings.preferred_cluster or owner_preferred_cluster
121
124
 
122
125
  @property
123
126
  def start_studios_on_interruptible(self) -> bool:
@@ -126,8 +129,8 @@ class Teamspace:
126
129
  @property
127
130
  def cloud_accounts(self) -> List[str]:
128
131
  """All cloud accounts associated with that teamspace."""
129
- clusters = self._teamspace_api.list_cloud_accounts(teamspace_id=self.id)
130
- return [cl.cluster_name for cl in clusters]
132
+ cloud_accounts = self._teamspace_api.list_cloud_accounts(teamspace_id=self.id)
133
+ return [cl.cluster_name for cl in cloud_accounts]
131
134
 
132
135
  @property
133
136
  def cloud_account_objs(self) -> List[V1ProjectClusterBinding]:
@@ -196,11 +199,17 @@ class Teamspace:
196
199
  if cloud_account is None:
197
200
  cloud_account = os.getenv("LIGHTNING_CLUSTER_ID") or self.default_cloud_account
198
201
 
199
- cluster_machines = self._teamspace_api.list_machines(self.id, cloud_account=cloud_account)
202
+ if cloud_account is None:
203
+ raise RuntimeError("Could not resolve cloud account")
204
+
205
+ cloud_machines = self._teamspace_api.list_machines(self.id, cloud_account=cloud_account)
200
206
  return [
201
207
  Machine(
202
- cluster_machine.instance_id,
203
- cluster_machine.instance_id,
208
+ name=cluster_machine.instance_id,
209
+ slug=cluster_machine.slug_multi_cloud,
210
+ instance_type=cluster_machine.instance_id,
211
+ family=cluster_machine.family,
212
+ accelerator_count=cluster_machine.resources.gpu or cluster_machine.resources.cpu,
204
213
  cost=cluster_machine.cost,
205
214
  interruptible_cost=cluster_machine.spot_price,
206
215
  wait_time=float(cluster_machine.available_in_seconds) if cluster_machine.available_in_seconds else None,
@@ -208,7 +217,7 @@ class Teamspace:
208
217
  if cluster_machine.available_in_seconds_spot
209
218
  else None,
210
219
  )
211
- for cluster_machine in cluster_machines
220
+ for cluster_machine in cloud_machines
212
221
  ]
213
222
 
214
223
  def __eq__(self, other: "Teamspace") -> bool:
@@ -6,7 +6,8 @@ from typing import TYPE_CHECKING, Generator, List, Optional, Tuple, Union
6
6
 
7
7
  from lightning_sdk.api import TeamspaceApi, UserApi
8
8
  from lightning_sdk.api.utils import _get_cloud_url
9
- from lightning_sdk.machine import Machine
9
+ from lightning_sdk.lightning_cloud.openapi.rest import ApiException
10
+ from lightning_sdk.machine import CloudProvider, Machine
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from lightning_sdk.organization import Organization
@@ -48,6 +49,26 @@ def _resolve_deprecated_cloud_compute(machine: Machine, cloud_compute: Optional[
48
49
  return machine
49
50
 
50
51
 
52
+ def _resolve_deprecated_provider(
53
+ cloud_provider: Optional[Union[CloudProvider, str]], provider: Optional[Union[CloudProvider, str]]
54
+ ) -> Optional[Union[CloudProvider, str]]:
55
+ if provider is not None:
56
+ if cloud_provider is not None:
57
+ raise ValueError(
58
+ "Cannot use both 'provider' and 'cloud_provider' at the same time."
59
+ "Please don't set the 'provider' as it will be deprecated!"
60
+ )
61
+
62
+ warnings.warn(
63
+ "The 'provider' argument will be deprecated in the future! "
64
+ "Please consider using the 'cloud_provider' argument instead!",
65
+ DeprecationWarning,
66
+ )
67
+ return provider
68
+
69
+ return cloud_provider
70
+
71
+
51
72
  def _resolve_deprecated_cluster(cloud_account: Optional[str], cluster: Optional[str]) -> Optional[str]:
52
73
  if cluster is not None:
53
74
  if cloud_account is not None:
@@ -85,7 +106,13 @@ def _resolve_org(org: Optional[Union[str, "Organization"]]) -> Optional["Organiz
85
106
 
86
107
  from lightning_sdk.organization import Organization
87
108
 
88
- return Organization(name=org)
109
+ try:
110
+ return Organization(name=org)
111
+ # Handle case where user name is mistakenly used as organization name
112
+ except ApiException as ae:
113
+ if ae.status == 404:
114
+ raise ValueError(f"Organization '{org}' does not exist or you are not a member of it.") from ae
115
+ raise RuntimeError(f"Failed to resolve organization '{org}': {ae}") from ae
89
116
 
90
117
 
91
118
  def _resolve_user_name(name: Optional[str]) -> Optional[str]:
@@ -213,3 +240,11 @@ def _get_studio_url(studio: "Studio", turn_on: bool = False) -> str:
213
240
  if turn_on:
214
241
  return f"{base_url}?turnOn=true"
215
242
  return base_url
243
+
244
+
245
+ def _get_org_id(teamspace: "Teamspace") -> str:
246
+ from lightning_sdk.organization import Organization
247
+
248
+ if isinstance(teamspace.owner, Organization):
249
+ return teamspace.owner.id
250
+ return ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 2025.7.17
3
+ Version: 2025.7.30rc0
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License
@@ -25,6 +25,8 @@ License: MIT License
25
25
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
26
  SOFTWARE.
27
27
 
28
+ Project-URL: Homepage, https://lightning.ai
29
+ Project-URL: Documentation, https://lightning.ai/docs/overview/sdk-reference
28
30
  Keywords: deep learning,machine learning,pytorch,AI
29
31
  Classifier: Programming Language :: Python :: 3
30
32
  Classifier: License :: OSI Approved :: MIT License
@@ -79,11 +81,11 @@ s.start()
79
81
  # prints Machine.CPU-4
80
82
  print(s.machine)
81
83
 
82
- # or start directly on this machine with s.start(Machine.A10G)
84
+ # or start directly on this machine with s.start(Machine.L4)
83
85
  print("switching Studio machine...")
84
- s.switch_machine(Machine.A10G)
86
+ s.switch_machine(Machine.L4)
85
87
 
86
- # prints Machine.A10G
88
+ # prints Machine.L4
87
89
  print(s.machine)
88
90
 
89
91
  # prints Status.Running
@@ -96,7 +98,7 @@ s.install_plugin("jobs")
96
98
  s.install_plugin("multi-machine-training")
97
99
 
98
100
  # run the resulting plugins to start 1 job and 1 multi-machine training
99
- s.installed_plugins["jobs"].run("python my_dummy_file", name="my_first_job", machine=Machine.A10G)
101
+ s.installed_plugins["jobs"].run("python my_dummy_file", name="my_first_job", machine=Machine.L4)
100
102
  s.installed_plugins["multi-machine-training"].run("python my_dummy_file", name="my_first_mmt", machine=Machine.T4, num_instances=42)
101
103
 
102
104
  print("Stopping Studio")