lightning-sdk 2025.8.19.post0__py3-none-any.whl → 2025.8.26__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 +1 -1
- lightning_sdk/api/llm_api.py +6 -2
- lightning_sdk/api/studio_api.py +168 -2
- lightning_sdk/api/teamspace_api.py +60 -30
- lightning_sdk/api/user_api.py +49 -1
- lightning_sdk/api/utils.py +1 -1
- lightning_sdk/cli/config/set.py +6 -18
- lightning_sdk/cli/legacy/create.py +12 -14
- lightning_sdk/cli/legacy/delete.py +3 -3
- lightning_sdk/cli/legacy/deploy/_auth.py +4 -4
- lightning_sdk/cli/legacy/download.py +7 -7
- lightning_sdk/cli/legacy/job_and_mmt_action.py +4 -4
- lightning_sdk/cli/legacy/list.py +9 -9
- lightning_sdk/cli/legacy/open.py +3 -3
- lightning_sdk/cli/legacy/start.py +1 -0
- lightning_sdk/cli/legacy/switch.py +1 -0
- lightning_sdk/cli/legacy/upload.py +3 -3
- lightning_sdk/cli/studio/create.py +14 -23
- lightning_sdk/cli/studio/delete.py +28 -27
- lightning_sdk/cli/studio/list.py +5 -6
- lightning_sdk/cli/studio/ssh.py +19 -22
- lightning_sdk/cli/studio/start.py +23 -23
- lightning_sdk/cli/studio/stop.py +22 -26
- lightning_sdk/cli/studio/switch.py +20 -23
- lightning_sdk/cli/utils/resolve.py +1 -1
- lightning_sdk/cli/utils/save_to_config.py +27 -0
- lightning_sdk/cli/utils/studio_selection.py +106 -0
- lightning_sdk/cli/utils/teamspace_selection.py +125 -0
- lightning_sdk/lightning_cloud/openapi/__init__.py +3 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +170 -0
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +3 -0
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +15 -15
- lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
- lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
- lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
- lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
- lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
- lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +281 -21
- lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_quote_annual_upsell_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -27
- lightning_sdk/llm/llm.py +2 -2
- lightning_sdk/llm/public_assistants.py +4 -0
- lightning_sdk/studio.py +92 -28
- lightning_sdk/teamspace.py +25 -2
- lightning_sdk/user.py +19 -1
- lightning_sdk/utils/config.py +6 -0
- lightning_sdk/utils/names.py +1179 -0
- lightning_sdk/utils/progress.py +284 -0
- lightning_sdk/utils/resolve.py +6 -6
- {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/RECORD +61 -53
- {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/top_level.txt +0 -0
lightning_sdk/studio.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import glob
|
|
2
2
|
import os
|
|
3
|
+
import threading
|
|
3
4
|
import warnings
|
|
4
5
|
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Tuple, Union
|
|
5
6
|
|
|
@@ -14,6 +15,7 @@ from lightning_sdk.owner import Owner
|
|
|
14
15
|
from lightning_sdk.status import Status
|
|
15
16
|
from lightning_sdk.teamspace import Teamspace
|
|
16
17
|
from lightning_sdk.user import User
|
|
18
|
+
from lightning_sdk.utils.names import random_unique_name
|
|
17
19
|
from lightning_sdk.utils.resolve import (
|
|
18
20
|
_get_org_id,
|
|
19
21
|
_resolve_deprecated_cluster,
|
|
@@ -56,8 +58,11 @@ class Studio:
|
|
|
56
58
|
"""
|
|
57
59
|
|
|
58
60
|
# skips init of studio, only set when using this as a shell for names, ids etc.
|
|
59
|
-
_skip_init =
|
|
60
|
-
_skip_setup =
|
|
61
|
+
_skip_init = threading.local()
|
|
62
|
+
_skip_setup = threading.local()
|
|
63
|
+
|
|
64
|
+
# whether to show progress bars during operations
|
|
65
|
+
show_progress = False
|
|
61
66
|
|
|
62
67
|
def __init__(
|
|
63
68
|
self,
|
|
@@ -76,64 +81,77 @@ class Studio:
|
|
|
76
81
|
self._studio_api = StudioApi()
|
|
77
82
|
self._cloud_account_api = CloudAccountApi()
|
|
78
83
|
|
|
79
|
-
_teamspace =
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
self._teamspace = None
|
|
85
|
+
|
|
86
|
+
# don't resolve anything if we're skipping init
|
|
87
|
+
if not getattr(self._skip_init, "value", False):
|
|
88
|
+
_teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
|
|
89
|
+
if _teamspace is None:
|
|
90
|
+
raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
self._teamspace = _teamspace
|
|
84
93
|
|
|
85
|
-
self._setup_done = self._skip_setup
|
|
94
|
+
self._setup_done = getattr(self._skip_setup, "value", False)
|
|
86
95
|
self._disable_secrets = disable_secrets
|
|
87
96
|
|
|
88
97
|
self._plugins = {}
|
|
98
|
+
self._studio = None
|
|
89
99
|
|
|
90
100
|
cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
|
|
91
101
|
cloud_provider = _resolve_deprecated_provider(cloud_provider, provider)
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
# if we're skipping init, we don't need to resolve the cloud account as then we're not creating a studio
|
|
104
|
+
if self._teamspace is not None:
|
|
105
|
+
_cloud_account = self._cloud_account_api.resolve_cloud_account(
|
|
106
|
+
self._teamspace.id,
|
|
107
|
+
cloud_account=cloud_account,
|
|
108
|
+
cloud_provider=cloud_provider,
|
|
109
|
+
default_cloud_account=self._teamspace.default_cloud_account,
|
|
110
|
+
)
|
|
99
111
|
|
|
100
112
|
# Resolve studio name if not provided: explicit → env (LIGHTNING_CLOUD_SPACE_ID) → config defaults
|
|
101
|
-
if name is None:
|
|
113
|
+
if name is None and not getattr(self._skip_init, "value", False):
|
|
102
114
|
studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
|
|
103
115
|
if studio_id is not None:
|
|
104
116
|
# We're inside a studio, get it by ID
|
|
105
117
|
self._studio = self._studio_api.get_studio_by_id(studio_id=studio_id, teamspace_id=self._teamspace.id)
|
|
118
|
+
name = self._studio.name
|
|
106
119
|
else:
|
|
107
120
|
# Try config defaults
|
|
108
121
|
from lightning_sdk.utils.config import Config, DefaultConfigKeys
|
|
109
122
|
|
|
110
123
|
config = Config()
|
|
111
124
|
name = config.get_value(DefaultConfigKeys.studio)
|
|
112
|
-
if name is None:
|
|
125
|
+
if name is None and not create_ok:
|
|
113
126
|
raise ValueError(
|
|
114
127
|
"Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!"
|
|
115
128
|
)
|
|
116
129
|
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
if self._studio is None and not getattr(self._skip_init, "value", False):
|
|
131
|
+
# If we have a name (explicit or from config), get studio by name
|
|
119
132
|
try:
|
|
133
|
+
if name is None:
|
|
134
|
+
# if we don't have a name, raise an error to get
|
|
135
|
+
# to the exception path and optionally create a studio
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!"
|
|
138
|
+
)
|
|
120
139
|
self._studio = self._studio_api.get_studio(name, self._teamspace.id)
|
|
121
140
|
except ValueError as e:
|
|
122
141
|
if create_ok:
|
|
142
|
+
name = name or random_unique_name()
|
|
123
143
|
self._studio = self._studio_api.create_studio(
|
|
124
144
|
name,
|
|
125
145
|
self._teamspace.id,
|
|
126
|
-
cloud_account=
|
|
146
|
+
cloud_account=_cloud_account,
|
|
127
147
|
source=source,
|
|
128
148
|
disable_secrets=self._disable_secrets,
|
|
129
149
|
)
|
|
130
150
|
else:
|
|
131
|
-
raise
|
|
132
|
-
|
|
133
|
-
self._cloud_account = self._studio.cluster_id
|
|
151
|
+
raise e
|
|
134
152
|
|
|
135
153
|
if (
|
|
136
|
-
not self._skip_init
|
|
154
|
+
not getattr(self._skip_init, "value", False)
|
|
137
155
|
and _internal_status_to_external_status(
|
|
138
156
|
self._studio_api._get_studio_instance_status_from_object(self._studio)
|
|
139
157
|
)
|
|
@@ -254,9 +272,26 @@ class Studio:
|
|
|
254
272
|
|
|
255
273
|
if status != Status.Stopped:
|
|
256
274
|
raise RuntimeError(f"Cannot start a studio that is not stopped. Studio {self.name} is {status}.")
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
275
|
+
|
|
276
|
+
# Show progress bar during startup
|
|
277
|
+
if self.show_progress:
|
|
278
|
+
from lightning_sdk.utils.progress import StudioProgressTracker
|
|
279
|
+
|
|
280
|
+
with StudioProgressTracker("start", show_progress=True) as progress:
|
|
281
|
+
# Start the studio without blocking
|
|
282
|
+
self._studio_api.start_studio_async(
|
|
283
|
+
self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Track progress through completion
|
|
287
|
+
progress.track_startup_phases(
|
|
288
|
+
lambda: self._studio_api.get_studio_status(self._studio.id, self._teamspace.id)
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
# Use the blocking version if no progress is needed
|
|
292
|
+
self._studio_api.start_studio(
|
|
293
|
+
self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
|
|
294
|
+
)
|
|
260
295
|
|
|
261
296
|
self._setup()
|
|
262
297
|
|
|
@@ -324,9 +359,22 @@ class Studio:
|
|
|
324
359
|
raise RuntimeError(
|
|
325
360
|
f"Cannot switch machine on a studio that is not running. Studio {self.name} is {status}."
|
|
326
361
|
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
362
|
+
|
|
363
|
+
if self.show_progress:
|
|
364
|
+
from lightning_sdk.utils.progress import StudioProgressTracker
|
|
365
|
+
|
|
366
|
+
with StudioProgressTracker("switch", show_progress=True) as progress:
|
|
367
|
+
# Update progress before starting the switch
|
|
368
|
+
progress.update_progress(5, "Initiating machine switch...")
|
|
369
|
+
|
|
370
|
+
# Start the switch operation with progress tracking
|
|
371
|
+
self._studio_api.switch_studio_machine_with_progress(
|
|
372
|
+
self._studio.id, self._teamspace.id, machine, interruptible=interruptible, progress=progress
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
self._studio_api.switch_studio_machine(
|
|
376
|
+
self._studio.id, self._teamspace.id, machine, interruptible=interruptible
|
|
377
|
+
)
|
|
330
378
|
|
|
331
379
|
def run_and_detach(self, *commands: str, timeout: float = 10, check_interval: float = 1) -> str:
|
|
332
380
|
"""Runs given commands on the Studio and returns immediately.
|
|
@@ -575,6 +623,22 @@ class Studio:
|
|
|
575
623
|
warnings.warn("auto_shutdown_time is deprecated. Use auto_sleep_time instead", DeprecationWarning)
|
|
576
624
|
self.auto_sleep_time = value
|
|
577
625
|
|
|
626
|
+
@property
|
|
627
|
+
def env(self) -> Dict[str, str]:
|
|
628
|
+
self._update_studio_reference()
|
|
629
|
+
return self._studio_api.get_env(self._studio)
|
|
630
|
+
|
|
631
|
+
def set_env(self, new_env: Dict[str, str], partial: bool = True) -> None:
|
|
632
|
+
"""Set the environment variables for the Studio.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
new_env: The new environment variables to set.
|
|
636
|
+
partial: Whether to only set the environment variables that are provided.
|
|
637
|
+
If False, existing environment variables that are not in new_env will be removed.
|
|
638
|
+
If True, existing environment variables that are not in new_env will be kept.
|
|
639
|
+
"""
|
|
640
|
+
self._studio_api.set_env(self._studio, self._teamspace.id, new_env, partial=partial)
|
|
641
|
+
|
|
578
642
|
@property
|
|
579
643
|
def available_plugins(self) -> Mapping[str, str]:
|
|
580
644
|
"""All available plugins to install in the current Studio."""
|
lightning_sdk/teamspace.py
CHANGED
|
@@ -2,7 +2,7 @@ import glob
|
|
|
2
2
|
import os
|
|
3
3
|
import warnings
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
5
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
|
6
6
|
|
|
7
7
|
from tqdm.auto import tqdm
|
|
8
8
|
|
|
@@ -21,6 +21,7 @@ from lightning_sdk.utils.resolve import (
|
|
|
21
21
|
_resolve_org,
|
|
22
22
|
_resolve_teamspace_name,
|
|
23
23
|
_resolve_user,
|
|
24
|
+
skip_studio_init,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
@@ -125,7 +126,11 @@ class Teamspace:
|
|
|
125
126
|
for cl in cloud_accounts:
|
|
126
127
|
_studios = self._teamspace_api.list_studios(teamspace_id=self.id, cloud_account=cl.cluster_id)
|
|
127
128
|
for s in _studios:
|
|
128
|
-
|
|
129
|
+
with skip_studio_init():
|
|
130
|
+
studio = Studio(name=s.name, teamspace=self, cluster=cl.cluster_name, create_ok=False)
|
|
131
|
+
studio._studio = s
|
|
132
|
+
studio._teamspace = self
|
|
133
|
+
studios.append(studio)
|
|
129
134
|
|
|
130
135
|
return studios
|
|
131
136
|
|
|
@@ -209,6 +214,24 @@ class Teamspace:
|
|
|
209
214
|
|
|
210
215
|
return tuple(mmts)
|
|
211
216
|
|
|
217
|
+
@property
|
|
218
|
+
def secrets(self) -> Dict[str, str]:
|
|
219
|
+
"""All (encrypted) secrets for the teamspace.
|
|
220
|
+
|
|
221
|
+
Note:
|
|
222
|
+
Once created, the secret values are encrypted and cannot be viewed here anymore.
|
|
223
|
+
"""
|
|
224
|
+
return self._teamspace_api.get_secrets(self.id)
|
|
225
|
+
|
|
226
|
+
def set_secret(self, key: str, value: str) -> None:
|
|
227
|
+
"""Set the (encrypted) secrets for the teamspace."""
|
|
228
|
+
if not self._teamspace_api.verify_secret_name(key):
|
|
229
|
+
raise ValueError(
|
|
230
|
+
"Secret keys must only contain alphanumeric characters and underscores and not begin with a number."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
self._teamspace_api.set_secret(self.id, key, value)
|
|
234
|
+
|
|
212
235
|
def list_machines(self, cloud_account: Optional[str] = None) -> List[Machine]:
|
|
213
236
|
if cloud_account is None:
|
|
214
237
|
cloud_account = os.getenv("LIGHTNING_CLUSTER_ID") or self.default_cloud_account
|
lightning_sdk/user.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Dict, Optional
|
|
2
2
|
|
|
3
3
|
from lightning_sdk.api import UserApi
|
|
4
4
|
from lightning_sdk.owner import Owner
|
|
@@ -37,6 +37,24 @@ class User(Owner):
|
|
|
37
37
|
"""The user's ID."""
|
|
38
38
|
return self._user.id
|
|
39
39
|
|
|
40
|
+
@property
|
|
41
|
+
def secrets(self) -> Dict[str, str]:
|
|
42
|
+
"""All (encrypted) secrets for the user.
|
|
43
|
+
|
|
44
|
+
Note:
|
|
45
|
+
Once created, the secret values are encrypted and cannot be viewed here anymore.
|
|
46
|
+
"""
|
|
47
|
+
return self._user_api.get_secrets()
|
|
48
|
+
|
|
49
|
+
def set_secret(self, key: str, value: str) -> None:
|
|
50
|
+
"""Set a (encrypted) secret for the user."""
|
|
51
|
+
if not self._user_api.verify_secret_name(key):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Secret keys must only contain alphanumeric characters and underscores and not begin with a number."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self._user_api.set_secret(key, value)
|
|
57
|
+
|
|
40
58
|
def __repr__(self) -> str:
|
|
41
59
|
"""Returns reader friendly representation."""
|
|
42
60
|
return f"User(name={self.name})"
|
lightning_sdk/utils/config.py
CHANGED
|
@@ -144,6 +144,12 @@ class Config:
|
|
|
144
144
|
sort_keys=True,
|
|
145
145
|
)
|
|
146
146
|
|
|
147
|
+
def get(self, key: str) -> Optional[str]:
|
|
148
|
+
return self.get_value(key)
|
|
149
|
+
|
|
150
|
+
def set(self, key: str, value: str) -> None:
|
|
151
|
+
self._set_nested([key], value)
|
|
152
|
+
|
|
147
153
|
|
|
148
154
|
def _unflatten_dict(flat_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
149
155
|
unflattened_dict = {}
|