lightning-sdk 2025.8.21__py3-none-any.whl → 2025.8.28__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 (54) 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 +16 -24
  16. lightning_sdk/cli/studio/delete.py +28 -27
  17. lightning_sdk/cli/studio/list.py +29 -15
  18. lightning_sdk/cli/studio/ssh.py +19 -22
  19. lightning_sdk/cli/studio/start.py +25 -25
  20. lightning_sdk/cli/studio/stop.py +25 -28
  21. lightning_sdk/cli/studio/switch.py +21 -24
  22. lightning_sdk/cli/utils/resolve.py +1 -1
  23. lightning_sdk/cli/utils/richt_print.py +24 -0
  24. lightning_sdk/cli/utils/save_to_config.py +27 -0
  25. lightning_sdk/cli/utils/studio_selection.py +106 -0
  26. lightning_sdk/cli/utils/teamspace_selection.py +125 -0
  27. lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
  28. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
  29. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
  30. lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
  31. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
  32. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
  33. lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
  34. lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
  35. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
  36. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
  37. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
  38. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +145 -41
  39. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
  41. lightning_sdk/llm/public_assistants.py +4 -0
  42. lightning_sdk/studio.py +54 -22
  43. lightning_sdk/teamspace.py +25 -2
  44. lightning_sdk/user.py +19 -1
  45. lightning_sdk/utils/config.py +6 -0
  46. lightning_sdk/utils/names.py +1179 -0
  47. lightning_sdk/utils/progress.py +2 -2
  48. lightning_sdk/utils/resolve.py +17 -6
  49. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/METADATA +1 -1
  50. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/RECORD +54 -48
  51. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/LICENSE +0 -0
  52. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/WHEEL +0 -0
  53. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/entry_points.txt +0 -0
  54. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.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,78 @@ 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._prevent_refetch = False
85
+ self._teamspace = None
85
86
 
86
- self._teamspace = _teamspace
87
+ # don't resolve anything if we're skipping init
88
+ if not getattr(self._skip_init, "value", False):
89
+ _teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
90
+ if _teamspace is None:
91
+ raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
87
92
 
88
- self._setup_done = self._skip_setup
93
+ self._teamspace = _teamspace
94
+
95
+ self._setup_done = getattr(self._skip_setup, "value", False)
89
96
  self._disable_secrets = disable_secrets
90
97
 
91
98
  self._plugins = {}
99
+ self._studio = None
92
100
 
93
101
  cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
94
102
  cloud_provider = _resolve_deprecated_provider(cloud_provider, provider)
95
103
 
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
- )
104
+ # if we're skipping init, we don't need to resolve the cloud account as then we're not creating a studio
105
+ if self._teamspace is not None:
106
+ _cloud_account = self._cloud_account_api.resolve_cloud_account(
107
+ self._teamspace.id,
108
+ cloud_account=cloud_account,
109
+ cloud_provider=cloud_provider,
110
+ default_cloud_account=self._teamspace.default_cloud_account,
111
+ )
102
112
 
103
113
  # Resolve studio name if not provided: explicit → env (LIGHTNING_CLOUD_SPACE_ID) → config defaults
104
- if name is None:
114
+ if name is None and not getattr(self._skip_init, "value", False):
105
115
  studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
106
116
  if studio_id is not None:
107
117
  # We're inside a studio, get it by ID
108
118
  self._studio = self._studio_api.get_studio_by_id(studio_id=studio_id, teamspace_id=self._teamspace.id)
119
+ name = self._studio.name
109
120
  else:
110
121
  # Try config defaults
111
122
  from lightning_sdk.utils.config import Config, DefaultConfigKeys
112
123
 
113
124
  config = Config()
114
125
  name = config.get_value(DefaultConfigKeys.studio)
115
- if name is None:
126
+ if name is None and not create_ok:
116
127
  raise ValueError(
117
128
  "Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!"
118
129
  )
119
130
 
120
- # If we have a name (explicit or from config), get studio by name
121
- if name is not None:
131
+ if self._studio is None and not getattr(self._skip_init, "value", False):
132
+ # If we have a name (explicit or from config), get studio by name
122
133
  try:
134
+ if name is None:
135
+ # if we don't have a name, raise an error to get
136
+ # to the exception path and optionally create a studio
137
+ raise ValueError(
138
+ "Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!"
139
+ )
123
140
  self._studio = self._studio_api.get_studio(name, self._teamspace.id)
124
141
  except ValueError as e:
125
142
  if create_ok:
143
+ name = name or random_unique_name()
126
144
  self._studio = self._studio_api.create_studio(
127
145
  name,
128
146
  self._teamspace.id,
129
- cloud_account=self._cloud_account,
147
+ cloud_account=_cloud_account,
130
148
  source=source,
131
149
  disable_secrets=self._disable_secrets,
132
150
  )
133
151
  else:
134
- raise ValueError(f"Studio {name} does not exist.") from e
135
-
136
- self._cloud_account = self._studio.cluster_id
152
+ raise e
137
153
 
138
154
  if (
139
- not self._skip_init
155
+ not getattr(self._skip_init, "value", False)
140
156
  and _internal_status_to_external_status(
141
157
  self._studio_api._get_studio_instance_status_from_object(self._studio)
142
158
  )
@@ -608,6 +624,22 @@ class Studio:
608
624
  warnings.warn("auto_shutdown_time is deprecated. Use auto_sleep_time instead", DeprecationWarning)
609
625
  self.auto_sleep_time = value
610
626
 
627
+ @property
628
+ def env(self) -> Dict[str, str]:
629
+ self._update_studio_reference()
630
+ return self._studio_api.get_env(self._studio)
631
+
632
+ def set_env(self, new_env: Dict[str, str], partial: bool = True) -> None:
633
+ """Set the environment variables for the Studio.
634
+
635
+ Args:
636
+ new_env: The new environment variables to set.
637
+ partial: Whether to only set the environment variables that are provided.
638
+ If False, existing environment variables that are not in new_env will be removed.
639
+ If True, existing environment variables that are not in new_env will be kept.
640
+ """
641
+ self._studio_api.set_env(self._studio, self._teamspace.id, new_env, partial=partial)
642
+
611
643
  @property
612
644
  def available_plugins(self) -> Mapping[str, str]:
613
645
  """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 = {}