lightning-sdk 2025.8.21__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 (53) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/studio_api.py +69 -2
  3. lightning_sdk/api/teamspace_api.py +60 -30
  4. lightning_sdk/api/user_api.py +49 -1
  5. lightning_sdk/api/utils.py +1 -1
  6. lightning_sdk/cli/config/set.py +6 -18
  7. lightning_sdk/cli/legacy/create.py +3 -3
  8. lightning_sdk/cli/legacy/delete.py +3 -3
  9. lightning_sdk/cli/legacy/deploy/_auth.py +4 -4
  10. lightning_sdk/cli/legacy/download.py +7 -7
  11. lightning_sdk/cli/legacy/job_and_mmt_action.py +4 -4
  12. lightning_sdk/cli/legacy/list.py +9 -9
  13. lightning_sdk/cli/legacy/open.py +3 -3
  14. lightning_sdk/cli/legacy/upload.py +3 -3
  15. lightning_sdk/cli/studio/create.py +14 -23
  16. lightning_sdk/cli/studio/delete.py +28 -27
  17. lightning_sdk/cli/studio/list.py +5 -6
  18. lightning_sdk/cli/studio/ssh.py +19 -22
  19. lightning_sdk/cli/studio/start.py +22 -23
  20. lightning_sdk/cli/studio/stop.py +22 -26
  21. lightning_sdk/cli/studio/switch.py +19 -23
  22. lightning_sdk/cli/utils/resolve.py +1 -1
  23. lightning_sdk/cli/utils/save_to_config.py +27 -0
  24. lightning_sdk/cli/utils/studio_selection.py +106 -0
  25. lightning_sdk/cli/utils/teamspace_selection.py +125 -0
  26. lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
  27. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
  28. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
  29. lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
  30. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
  31. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
  32. lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
  33. lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
  34. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
  35. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
  36. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
  37. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +145 -41
  38. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
  39. lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
  40. lightning_sdk/llm/public_assistants.py +4 -0
  41. lightning_sdk/studio.py +53 -22
  42. lightning_sdk/teamspace.py +25 -2
  43. lightning_sdk/user.py +19 -1
  44. lightning_sdk/utils/config.py +6 -0
  45. lightning_sdk/utils/names.py +1179 -0
  46. lightning_sdk/utils/progress.py +2 -2
  47. lightning_sdk/utils/resolve.py +6 -6
  48. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/METADATA +1 -1
  49. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/RECORD +53 -47
  50. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/LICENSE +0 -0
  51. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/WHEEL +0 -0
  52. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/entry_points.txt +0 -0
  53. {lightning_sdk-2025.8.21.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,8 @@ 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()
61
63
 
62
64
  # whether to show progress bars during operations
63
65
  show_progress = False
@@ -79,64 +81,77 @@ class Studio:
79
81
  self._studio_api = StudioApi()
80
82
  self._cloud_account_api = CloudAccountApi()
81
83
 
82
- _teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
83
- if _teamspace is None:
84
- raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
84
+ self._teamspace = None
85
85
 
86
- self._teamspace = _teamspace
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")
87
91
 
88
- self._setup_done = self._skip_setup
92
+ self._teamspace = _teamspace
93
+
94
+ self._setup_done = getattr(self._skip_setup, "value", False)
89
95
  self._disable_secrets = disable_secrets
90
96
 
91
97
  self._plugins = {}
98
+ self._studio = None
92
99
 
93
100
  cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
94
101
  cloud_provider = _resolve_deprecated_provider(cloud_provider, provider)
95
102
 
96
- self._cloud_account = self._cloud_account_api.resolve_cloud_account(
97
- self._teamspace.id,
98
- cloud_account=cloud_account,
99
- cloud_provider=cloud_provider,
100
- default_cloud_account=self._teamspace.default_cloud_account,
101
- )
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
+ )
102
111
 
103
112
  # Resolve studio name if not provided: explicit → env (LIGHTNING_CLOUD_SPACE_ID) → config defaults
104
- if name is None:
113
+ if name is None and not getattr(self._skip_init, "value", False):
105
114
  studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
106
115
  if studio_id is not None:
107
116
  # We're inside a studio, get it by ID
108
117
  self._studio = self._studio_api.get_studio_by_id(studio_id=studio_id, teamspace_id=self._teamspace.id)
118
+ name = self._studio.name
109
119
  else:
110
120
  # Try config defaults
111
121
  from lightning_sdk.utils.config import Config, DefaultConfigKeys
112
122
 
113
123
  config = Config()
114
124
  name = config.get_value(DefaultConfigKeys.studio)
115
- if name is None:
125
+ if name is None and not create_ok:
116
126
  raise ValueError(
117
127
  "Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!"
118
128
  )
119
129
 
120
- # If we have a name (explicit or from config), get studio by name
121
- 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
122
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
+ )
123
139
  self._studio = self._studio_api.get_studio(name, self._teamspace.id)
124
140
  except ValueError as e:
125
141
  if create_ok:
142
+ name = name or random_unique_name()
126
143
  self._studio = self._studio_api.create_studio(
127
144
  name,
128
145
  self._teamspace.id,
129
- cloud_account=self._cloud_account,
146
+ cloud_account=_cloud_account,
130
147
  source=source,
131
148
  disable_secrets=self._disable_secrets,
132
149
  )
133
150
  else:
134
- raise ValueError(f"Studio {name} does not exist.") from e
135
-
136
- self._cloud_account = self._studio.cluster_id
151
+ raise e
137
152
 
138
153
  if (
139
- not self._skip_init
154
+ not getattr(self._skip_init, "value", False)
140
155
  and _internal_status_to_external_status(
141
156
  self._studio_api._get_studio_instance_status_from_object(self._studio)
142
157
  )
@@ -608,6 +623,22 @@ class Studio:
608
623
  warnings.warn("auto_shutdown_time is deprecated. Use auto_sleep_time instead", DeprecationWarning)
609
624
  self.auto_sleep_time = value
610
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
+
611
642
  @property
612
643
  def available_plugins(self) -> Mapping[str, str]:
613
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 = {}