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.
- lightning_sdk/__init__.py +3 -2
- lightning_sdk/api/cloud_account_api.py +204 -0
- lightning_sdk/api/deployment_api.py +11 -0
- lightning_sdk/api/job_api.py +82 -10
- lightning_sdk/api/llm_api.py +1 -1
- lightning_sdk/api/mmt_api.py +44 -5
- lightning_sdk/api/pipeline_api.py +4 -3
- lightning_sdk/api/studio_api.py +51 -8
- lightning_sdk/api/utils.py +6 -2
- lightning_sdk/cli/clusters_menu.py +3 -3
- lightning_sdk/cli/create.py +25 -11
- lightning_sdk/cli/deploy/_auth.py +19 -3
- lightning_sdk/cli/deploy/serve.py +21 -5
- lightning_sdk/cli/download.py +25 -1
- lightning_sdk/cli/entrypoint.py +4 -2
- lightning_sdk/cli/list.py +5 -1
- lightning_sdk/cli/run.py +3 -1
- lightning_sdk/cli/start.py +40 -8
- lightning_sdk/cli/switch.py +3 -1
- lightning_sdk/deployment/deployment.py +8 -0
- lightning_sdk/job/base.py +27 -3
- lightning_sdk/job/job.py +28 -4
- lightning_sdk/job/v1.py +10 -1
- lightning_sdk/job/v2.py +22 -2
- lightning_sdk/job/work.py +5 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +14 -1
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +428 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +153 -48
- lightning_sdk/lightning_cloud/openapi/api/cloudy_service_api.py +295 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +93 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +14 -1
- lightning_sdk/lightning_cloud/openapi/models/agentmanagedendpoints_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/blogposts_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/conversations_id_body1.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/messages_id_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/metricsstream_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_schedules_body.py +81 -3
- lightning_sdk/lightning_cloud/openapi/models/schedules_id_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/user_id_upgradetrigger_body.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/user_user_id_body.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_billing_subscription.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_blog_post.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloudy_settings.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation_response_chunk.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_billing_upgrade_trigger_record_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_blog_post_request.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_checkout_session_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_subscription_checkout_session_request.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_function_call.py +149 -0
- 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
- lightning_sdk/lightning_cloud/openapi/models/v1_get_cluster_health_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_like_status.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_published_managed_endpoints_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_endpoint.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +95 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_message.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_quote_subscription_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_resource_visibility.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_response_choice.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_schedule.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_service_health.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_slurm_v1.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_slurm_v1_status.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_tool_call.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_like_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_message_like_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -313
- lightning_sdk/lightning_cloud/openapi/models/v1_volume_state.py +1 -0
- lightning_sdk/llm/llm.py +69 -11
- lightning_sdk/llm/public_assistants.json +32 -8
- lightning_sdk/machine.py +151 -43
- lightning_sdk/mmt/base.py +20 -2
- lightning_sdk/mmt/mmt.py +25 -3
- lightning_sdk/mmt/v1.py +7 -1
- lightning_sdk/mmt/v2.py +27 -3
- lightning_sdk/models.py +1 -1
- lightning_sdk/organization.py +4 -0
- lightning_sdk/pipeline/pipeline.py +16 -5
- lightning_sdk/pipeline/printer.py +5 -3
- lightning_sdk/pipeline/schedule.py +844 -1
- lightning_sdk/pipeline/steps.py +19 -4
- lightning_sdk/sandbox.py +4 -1
- lightning_sdk/serve.py +2 -0
- lightning_sdk/studio.py +91 -44
- lightning_sdk/teamspace.py +19 -10
- lightning_sdk/utils/resolve.py +37 -2
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/METADATA +7 -5
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/RECORD +98 -85
- lightning_sdk/api/cluster_api.py +0 -119
- /lightning_sdk/cli/{inspect.py → inspection.py} +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/top_level.txt +0 -0
lightning_sdk/pipeline/steps.py
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
92
|
-
|
|
90
|
+
cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
|
|
91
|
+
cloud_provider = _resolve_deprecated_provider(cloud_provider, provider)
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
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(
|
|
202
|
-
|
|
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
|
-
|
|
214
|
-
if
|
|
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 {
|
|
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(
|
|
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
|
|
241
|
-
|
|
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:
|
lightning_sdk/teamspace.py
CHANGED
|
@@ -107,8 +107,8 @@ class Teamspace:
|
|
|
107
107
|
from lightning_sdk.studio import Studio
|
|
108
108
|
|
|
109
109
|
studios = []
|
|
110
|
-
|
|
111
|
-
for cl in
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
return [cl.cluster_name for cl in
|
|
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
|
-
|
|
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.
|
|
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
|
|
220
|
+
for cluster_machine in cloud_machines
|
|
212
221
|
]
|
|
213
222
|
|
|
214
223
|
def __eq__(self, other: "Teamspace") -> bool:
|
lightning_sdk/utils/resolve.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
84
|
+
# or start directly on this machine with s.start(Machine.L4)
|
|
83
85
|
print("switching Studio machine...")
|
|
84
|
-
s.switch_machine(Machine.
|
|
86
|
+
s.switch_machine(Machine.L4)
|
|
85
87
|
|
|
86
|
-
# prints Machine.
|
|
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.
|
|
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")
|