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.
Files changed (61) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/llm_api.py +6 -2
  3. lightning_sdk/api/studio_api.py +168 -2
  4. lightning_sdk/api/teamspace_api.py +60 -30
  5. lightning_sdk/api/user_api.py +49 -1
  6. lightning_sdk/api/utils.py +1 -1
  7. lightning_sdk/cli/config/set.py +6 -18
  8. lightning_sdk/cli/legacy/create.py +12 -14
  9. lightning_sdk/cli/legacy/delete.py +3 -3
  10. lightning_sdk/cli/legacy/deploy/_auth.py +4 -4
  11. lightning_sdk/cli/legacy/download.py +7 -7
  12. lightning_sdk/cli/legacy/job_and_mmt_action.py +4 -4
  13. lightning_sdk/cli/legacy/list.py +9 -9
  14. lightning_sdk/cli/legacy/open.py +3 -3
  15. lightning_sdk/cli/legacy/start.py +1 -0
  16. lightning_sdk/cli/legacy/switch.py +1 -0
  17. lightning_sdk/cli/legacy/upload.py +3 -3
  18. lightning_sdk/cli/studio/create.py +14 -23
  19. lightning_sdk/cli/studio/delete.py +28 -27
  20. lightning_sdk/cli/studio/list.py +5 -6
  21. lightning_sdk/cli/studio/ssh.py +19 -22
  22. lightning_sdk/cli/studio/start.py +23 -23
  23. lightning_sdk/cli/studio/stop.py +22 -26
  24. lightning_sdk/cli/studio/switch.py +20 -23
  25. lightning_sdk/cli/utils/resolve.py +1 -1
  26. lightning_sdk/cli/utils/save_to_config.py +27 -0
  27. lightning_sdk/cli/utils/studio_selection.py +106 -0
  28. lightning_sdk/cli/utils/teamspace_selection.py +125 -0
  29. lightning_sdk/lightning_cloud/openapi/__init__.py +3 -0
  30. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +170 -0
  31. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
  32. lightning_sdk/lightning_cloud/openapi/models/__init__.py +3 -0
  33. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +15 -15
  34. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
  36. lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
  37. lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
  38. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
  39. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
  40. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
  41. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +281 -21
  42. lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
  43. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
  44. lightning_sdk/lightning_cloud/openapi/models/v1_quote_annual_upsell_response.py +201 -0
  45. lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
  46. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -27
  47. lightning_sdk/llm/llm.py +2 -2
  48. lightning_sdk/llm/public_assistants.py +4 -0
  49. lightning_sdk/studio.py +92 -28
  50. lightning_sdk/teamspace.py +25 -2
  51. lightning_sdk/user.py +19 -1
  52. lightning_sdk/utils/config.py +6 -0
  53. lightning_sdk/utils/names.py +1179 -0
  54. lightning_sdk/utils/progress.py +284 -0
  55. lightning_sdk/utils/resolve.py +6 -6
  56. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/METADATA +1 -1
  57. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/RECORD +61 -53
  58. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/LICENSE +0 -0
  59. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/WHEEL +0 -0
  60. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/entry_points.txt +0 -0
  61. {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 = False
60
- _skip_setup = False
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 = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
80
- if _teamspace is None:
81
- raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
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
- self._teamspace = _teamspace
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
- 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
+ # 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
- # If we have a name (explicit or from config), get studio by name
118
- if name is not None:
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=self._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 ValueError(f"Studio {name} does not exist.") from e
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
- self._studio_api.start_studio(
258
- self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
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
- self._studio_api.switch_studio_machine(
328
- self._studio.id, self._teamspace.id, machine, interruptible=interruptible
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."""
@@ -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
- studios.append(Studio(name=s.name, teamspace=self, cluster=cl.cluster_name, create_ok=False))
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})"
@@ -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 = {}