lightning-sdk 0.2.22__py3-none-any.whl → 0.2.23__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 (57) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/license_api.py +2 -2
  3. lightning_sdk/api/llm_api.py +2 -0
  4. lightning_sdk/api/pipeline_api.py +1 -0
  5. lightning_sdk/api/studio_api.py +2 -0
  6. lightning_sdk/cli/entrypoint.py +15 -13
  7. lightning_sdk/cli/start.py +5 -2
  8. lightning_sdk/lightning_cloud/openapi/__init__.py +8 -0
  9. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +206 -0
  10. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +97 -0
  11. lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +118 -1
  12. lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
  13. lightning_sdk/lightning_cloud/openapi/models/__init__.py +8 -0
  14. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +29 -3
  15. lightning_sdk/lightning_cloud/openapi/models/cloudspace_id_visibility_body.py +123 -0
  16. lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +27 -1
  17. lightning_sdk/lightning_cloud/openapi/models/metricsstream_create_body.py +27 -1
  18. lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +27 -1
  19. lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +27 -1
  20. lightning_sdk/lightning_cloud/openapi/models/v1_check_cluster_name_availability_request.py +123 -0
  21. lightning_sdk/lightning_cloud/openapi/models/v1_check_cluster_name_availability_response.py +123 -0
  22. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
  23. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +53 -1
  24. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_session.py +29 -3
  25. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
  26. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -1
  27. lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +27 -1
  28. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster.py +253 -0
  29. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +827 -0
  30. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
  31. lightning_sdk/lightning_cloud/openapi/models/v1_instance_overprovisioning_spec.py +29 -1
  32. lightning_sdk/lightning_cloud/openapi/models/v1_lightning_run.py +53 -1
  33. lightning_sdk/lightning_cloud/openapi/models/v1_list_clusters_response.py +6 -6
  34. lightning_sdk/lightning_cloud/openapi/models/v1_list_project_clusters_response.py +6 -6
  35. lightning_sdk/lightning_cloud/openapi/models/v1_lite_published_cloud_space_response.py +513 -0
  36. lightning_sdk/lightning_cloud/openapi/models/v1_metrics_stream.py +27 -1
  37. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +27 -1
  38. lightning_sdk/lightning_cloud/openapi/models/v1_shared_filesystem.py +131 -1
  39. lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_visibility_response.py +97 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -79
  41. lightning_sdk/lightning_cloud/openapi/models/v1_volume.py +78 -104
  42. lightning_sdk/lightning_cloud/openapi/models/v1_volume_state.py +104 -0
  43. lightning_sdk/pipeline/__init__.py +11 -2
  44. lightning_sdk/pipeline/pipeline.py +38 -13
  45. lightning_sdk/pipeline/printer.py +29 -10
  46. lightning_sdk/pipeline/schedule.py +2 -1
  47. lightning_sdk/pipeline/{types.py → steps.py} +74 -56
  48. lightning_sdk/pipeline/utils.py +39 -2
  49. lightning_sdk/sandbox.py +157 -0
  50. lightning_sdk/services/license.py +12 -6
  51. lightning_sdk/studio.py +7 -1
  52. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/METADATA +1 -1
  53. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/RECORD +57 -48
  54. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/LICENSE +0 -0
  55. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/WHEEL +0 -0
  56. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/entry_points.txt +0 -0
  57. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Dict, List, Optional, Union
1
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
2
2
 
3
3
  from lightning_sdk.api.deployment_api import (
4
4
  AutoScaleConfig,
@@ -20,52 +20,61 @@ from lightning_sdk.lightning_cloud.openapi.models import (
20
20
  V1CreateDeploymentRequest,
21
21
  V1PipelineStep,
22
22
  V1PipelineStepType,
23
+ V1SharedFilesystem,
23
24
  )
25
+ from lightning_sdk.machine import Machine
24
26
  from lightning_sdk.mmt.v2 import MMTApiV2
27
+ from lightning_sdk.pipeline.utils import DEFAULT, _get_studio, _to_wait_for, _validate_cloud_account
25
28
  from lightning_sdk.studio import Studio
26
29
 
27
30
  if TYPE_CHECKING:
28
- from lightning_sdk.machine import Machine
29
31
  from lightning_sdk.organization import Organization
30
32
  from lightning_sdk.teamspace import Teamspace
31
33
  from lightning_sdk.user import User
32
34
 
33
35
 
34
- from lightning_sdk.pipeline.utils import DEFAULT
35
-
36
-
37
- class Deployment:
36
+ class DeploymentStep:
38
37
  # Note: This class is only temporary while pipeline is wip
39
38
 
40
39
  def __init__(
41
40
  self,
42
41
  name: Optional[str] = None,
42
+ studio: Optional[Union[str, Studio]] = None,
43
43
  machine: Optional["Machine"] = None,
44
44
  image: Optional[str] = None,
45
- autoscale: Optional["AutoScaleConfig"] = None,
46
- ports: Optional[List[float]] = None,
47
- release_strategy: Optional["ReleaseStrategy"] = None,
45
+ autoscale: Optional[AutoScaleConfig] = None,
46
+ ports: Optional[Union[float, List[float]]] = None,
47
+ release_strategy: Optional[ReleaseStrategy] = None,
48
48
  entrypoint: Optional[str] = None,
49
49
  command: Optional[str] = None,
50
- env: Union[List[Union["Secret", "Env"]], Dict[str, str], None] = None,
50
+ commands: Optional[List[str]] = None,
51
+ env: Union[List[Union[Secret, Env]], Dict[str, str], None] = None,
51
52
  spot: Optional[bool] = None,
52
53
  replicas: Optional[int] = None,
53
- health_check: Optional[Union["HttpHealthCheck", "ExecHealthCheck"]] = None,
54
- auth: Optional[Union["BasicAuth", "TokenAuth"]] = None,
54
+ health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None,
55
+ auth: Optional[Union[BasicAuth, TokenAuth]] = None,
55
56
  cloud_account: Optional[str] = None,
56
57
  custom_domain: Optional[str] = None,
57
58
  quantity: Optional[int] = None,
59
+ include_credentials: Optional[bool] = None,
58
60
  wait_for: Optional[Union[str, List[str]]] = DEFAULT,
59
61
  ) -> None:
60
62
  self.name = name
61
- self.machine = machine
63
+ self.studio = _get_studio(studio)
64
+ if cloud_account and studio and cloud_account != studio.cloud_account != cloud_account:
65
+ raise ValueError(
66
+ f"The provided cloud account `{cloud_account}` doesn't match"
67
+ f" the Studio cloud account {self.studio.cloud_account}"
68
+ )
69
+
70
+ self.machine = machine or Machine.CPU
62
71
  self.image = image
63
72
  self.autoscale = autoscale or AutoScaleConfig(
64
73
  min_replicas=0,
65
74
  max_replicas=1,
66
75
  target_metrics=[
67
76
  AutoScalingMetric(
68
- name="CPU" if machine.is_cpu() else "GPU",
77
+ name="CPU" if self.machine.is_cpu() else "GPU",
69
78
  target=80,
70
79
  )
71
80
  ],
@@ -74,22 +83,26 @@ class Deployment:
74
83
  self.release_strategy = release_strategy
75
84
  self.entrypoint = entrypoint
76
85
  self.command = command
86
+ self.commands = commands
77
87
  self.env = env
78
88
  self.spot = spot
79
89
  self.replicas = replicas or 1
80
90
  self.health_check = health_check
81
91
  self.auth = auth
82
- self.cloud_account = cloud_account or ""
92
+ self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
83
93
  self.custom_domain = custom_domain
84
94
  self.quantity = quantity
95
+ self.include_credentials = include_credentials or True
85
96
  self.wait_for = wait_for
86
97
 
87
- def to_proto(self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: bool) -> V1PipelineStep:
98
+ def to_proto(
99
+ self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
100
+ ) -> V1PipelineStep:
88
101
  _validate_cloud_account(cloud_account, self.cloud_account, shared_filesystem)
89
102
  return V1PipelineStep(
90
103
  name=self.name,
91
104
  type=V1PipelineStepType.DEPLOYMENT,
92
- wait_for=to_wait_for(self.wait_for),
105
+ wait_for=_to_wait_for(self.wait_for),
93
106
  deployment=V1CreateDeploymentRequest(
94
107
  autoscaling=to_autoscaling(self.autoscale, self.replicas),
95
108
  endpoint=to_endpoint(self.ports, self.auth, self.custom_domain),
@@ -106,18 +119,20 @@ class Deployment:
106
119
  machine=self.machine,
107
120
  health_check=self.health_check,
108
121
  quantity=self.quantity,
122
+ cloudspace_id=self.studio._studio.id if self.studio else None,
123
+ include_credentials=self.include_credentials,
109
124
  ),
110
125
  strategy=to_strategy(self.release_strategy),
111
126
  ),
112
127
  )
113
128
 
114
129
 
115
- class Job:
130
+ class JobStep:
116
131
  # Note: This class is only temporary while pipeline is wip
117
132
 
118
133
  def __init__(
119
134
  self,
120
- machine: Union["Machine", str],
135
+ machine: Optional[Union["Machine", str]] = None,
121
136
  name: Optional[str] = None,
122
137
  command: Optional[str] = None,
123
138
  studio: Union["Studio", str, None] = None,
@@ -135,14 +150,21 @@ class Job:
135
150
  wait_for: Union[str, List[str], None] = DEFAULT,
136
151
  ) -> None:
137
152
  self.name = name
138
- self.machine = machine
153
+ self.machine = machine or Machine.CPU
139
154
  self.command = command
140
- self.studio = studio
155
+ self.studio = _get_studio(studio)
156
+
157
+ if cloud_account and self.studio and cloud_account != self.studio.cloud_account != cloud_account:
158
+ raise ValueError(
159
+ f"The provided cloud account `{cloud_account}` doesn't match"
160
+ f" the Studio cloud account {self.studio.cloud_account}"
161
+ )
162
+
141
163
  self.image = image
142
164
  self.teamspace = teamspace
143
165
  self.org = org
144
166
  self.user = user
145
- self.cloud_account = cloud_account
167
+ self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
146
168
  self.env = env
147
169
  self.interruptible = interruptible
148
170
  self.image_credentials = image_credentials
@@ -151,7 +173,9 @@ class Job:
151
173
  self.path_mappings = path_mappings
152
174
  self.wait_for = wait_for
153
175
 
154
- def to_proto(self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: bool) -> V1PipelineStep:
176
+ def to_proto(
177
+ self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
178
+ ) -> V1PipelineStep:
155
179
  studio = _get_studio(self.studio)
156
180
  if isinstance(studio, Studio):
157
181
  if self.cloud_account is None:
@@ -181,12 +205,12 @@ class Job:
181
205
  return V1PipelineStep(
182
206
  name=self.name,
183
207
  type=V1PipelineStepType.JOB,
184
- wait_for=to_wait_for(self.wait_for),
208
+ wait_for=_to_wait_for(self.wait_for),
185
209
  job=body,
186
210
  )
187
211
 
188
212
 
189
- class MMT:
213
+ class MMTStep:
190
214
  # Note: This class is only temporary while pipeline is wip
191
215
 
192
216
  def __init__(
@@ -209,16 +233,22 @@ class MMT:
209
233
  path_mappings: Optional[Dict[str, str]] = None,
210
234
  wait_for: Optional[Union[str, List[str]]] = DEFAULT,
211
235
  ) -> None:
212
- self.machine = machine
236
+ self.machine = machine or Machine.CPU
213
237
  self.num_machines = num_machines
214
238
  self.name = name
215
239
  self.command = command
216
- self.studio = studio
240
+ self.studio = _get_studio(studio)
241
+
242
+ if cloud_account and self.studio and cloud_account != self.studio.cloud_account != cloud_account:
243
+ raise ValueError(
244
+ f"The provided cloud account `{cloud_account}` doesn't match"
245
+ f" the Studio cloud account {self.studio.cloud_account}"
246
+ )
217
247
  self.image = image
218
248
  self.teamspace = teamspace
219
249
  self.org = org
220
250
  self.user = user
221
- self.cloud_account = cloud_account
251
+ self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
222
252
  self.env = env
223
253
  self.interruptible = interruptible
224
254
  self.image_credentials = image_credentials
@@ -227,7 +257,9 @@ class MMT:
227
257
  self.path_mappings = path_mappings
228
258
  self.wait_for = wait_for
229
259
 
230
- def to_proto(self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: bool) -> V1PipelineStep:
260
+ def to_proto(
261
+ self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
262
+ ) -> V1PipelineStep:
231
263
  studio = _get_studio(self.studio)
232
264
  if isinstance(studio, Studio):
233
265
  if self.cloud_account is None:
@@ -258,37 +290,23 @@ class MMT:
258
290
  return V1PipelineStep(
259
291
  name=self.name,
260
292
  type=V1PipelineStepType.MMT,
261
- wait_for=to_wait_for(self.wait_for),
293
+ wait_for=_to_wait_for(self.wait_for),
262
294
  mmt=body,
263
295
  )
264
296
 
265
297
 
266
- def to_wait_for(wait_for: Optional[Union[str, List[str]]]) -> Optional[List[str]]:
267
- if wait_for == DEFAULT:
268
- return wait_for
269
-
270
- if wait_for is None:
271
- return []
272
-
273
- return wait_for if isinstance(wait_for, list) else [wait_for]
274
-
275
-
276
- def _validate_cloud_account(pipeline_cloud_account: str, step_cloud_account: str, shared_filesystem: bool) -> None:
277
- if not shared_filesystem:
278
- return
279
-
280
- if pipeline_cloud_account != "" and step_cloud_account != "" and pipeline_cloud_account != step_cloud_account:
281
- raise ValueError(
282
- "With shared filesystem enabled, all the pipeline steps wait_for to be on the same cluster."
283
- f" Found {pipeline_cloud_account} and {step_cloud_account}"
284
- )
285
-
298
+ class DeploymentReleaseStep(DeploymentStep):
299
+ def __init__(self, *args: Any, deployment_name: Optional[str] = None, **kwargs: Any) -> None:
300
+ if not deployment_name:
301
+ raise ValueError("The deployment name is required")
302
+ self._deployment_name = deployment_name
303
+ super().__init__(*args, **kwargs)
286
304
 
287
- def _get_studio(studio: Union["Studio", str, None]) -> Union[Studio, None]:
288
- if studio is None:
289
- return None
305
+ def to_proto(self, *args: Any, **kwargs: Any) -> V1PipelineStep:
306
+ proto: V1PipelineStep = super().to_proto(*args, **kwargs)
307
+ proto.deployment.name = self._deployment_name
308
+ proto.deployment.pipeline_reuse_deployment_between_runs = True
309
+ return proto
290
310
 
291
- if isinstance(studio, Studio):
292
- return studio
293
311
 
294
- return Studio(studio)
312
+ __all__ = ["JobStep", "MMTStep", "DeploymentStep", "DeploymentReleaseStep"]
@@ -1,6 +1,7 @@
1
- from typing import List
1
+ from typing import List, Literal, Optional, Union
2
2
 
3
- from lightning_sdk.lightning_cloud.openapi.models import V1PipelineStep
3
+ from lightning_sdk.lightning_cloud.openapi.models import V1PipelineStep, V1SharedFilesystem
4
+ from lightning_sdk.studio import Studio
4
5
 
5
6
  DEFAULT = "DEFAULT"
6
7
 
@@ -52,3 +53,39 @@ def prepare_steps(steps: List["V1PipelineStep"]) -> List["V1PipelineStep"]:
52
53
  raise ValueError("You can only reference prior steps")
53
54
 
54
55
  return steps
56
+
57
+
58
+ def _get_studio(studio: Union["Studio", str, None]) -> Union[Studio, None]:
59
+ if studio is None:
60
+ return None
61
+
62
+ if isinstance(studio, Studio):
63
+ return studio
64
+
65
+ return Studio(studio)
66
+
67
+
68
+ def _validate_cloud_account(
69
+ pipeline_cloud_account: str, step_cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
70
+ ) -> None:
71
+ shared_filesystem_enable = (
72
+ shared_filesystem.enabled if isinstance(shared_filesystem, V1SharedFilesystem) else shared_filesystem
73
+ )
74
+ if not shared_filesystem_enable:
75
+ return
76
+
77
+ if pipeline_cloud_account != "" and step_cloud_account != "" and pipeline_cloud_account != step_cloud_account:
78
+ raise ValueError(
79
+ "With shared filesystem enabled, all the pipeline steps requires to be on the same cluster."
80
+ f" Found {pipeline_cloud_account} and {step_cloud_account}"
81
+ )
82
+
83
+
84
+ def _to_wait_for(wait_for: Optional[Union[str, List[str]]]) -> Optional[Union[List[str], Literal["DEFAULT"]]]:
85
+ if wait_for == DEFAULT:
86
+ return wait_for
87
+
88
+ if wait_for is None:
89
+ return []
90
+
91
+ return wait_for if isinstance(wait_for, list) else [wait_for]
@@ -0,0 +1,157 @@
1
+ import ast
2
+ import types
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Optional, Type, Union
6
+
7
+ from lightning_sdk.machine import Machine
8
+ from lightning_sdk.organization import Organization
9
+ from lightning_sdk.status import Status
10
+ from lightning_sdk.studio import Studio
11
+ from lightning_sdk.teamspace import Teamspace
12
+ from lightning_sdk.user import User
13
+
14
+
15
+ @dataclass
16
+ class Output:
17
+ text: str
18
+ exit_code: int
19
+
20
+
21
+ class _Sandbox:
22
+ """Sandbox runs AI generated code safely and discards the machine after use.
23
+
24
+ Users can run any arbitrary code in a sandbox with sudo permissions.
25
+
26
+ Args:
27
+ name: The name of the sandbox.
28
+ machine: The machine to use for the sandbox.
29
+ interruptible: Whether the sandbox is interruptible.
30
+ teamspace: The teamspace to use for the sandbox.
31
+ org: The organization to use for the sandbox.
32
+ user: The user to use for the sandbox.
33
+ cloud_account: The cloud account to use for the sandbox.
34
+ disable_secrets: If true, user secrets such as LIGHTNING_API_KEY are not stored in the sandbox.
35
+
36
+ Example:
37
+ with Sandbox() as sandbox:
38
+ output = sandbox.run("python --version")
39
+ print(output.text)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ name: Optional[str] = None,
45
+ machine: Optional[str] = None,
46
+ interruptible: Optional[bool] = None,
47
+ teamspace: Optional[Union[str, Teamspace]] = None,
48
+ org: Optional[Union[str, Organization]] = None,
49
+ user: Optional[Union[str, User]] = None,
50
+ cloud_account: Optional[str] = None,
51
+ disable_secrets: bool = True,
52
+ ) -> None:
53
+ if name is None:
54
+ timestr = datetime.now().strftime("%b-%d-%H_%M")
55
+ name = f"sandbox-{timestr}"
56
+
57
+ self._machine = machine or Machine.CPU
58
+ self._interruptible = interruptible
59
+ self._studio = Studio(
60
+ name=name,
61
+ teamspace=teamspace,
62
+ org=org,
63
+ user=user,
64
+ cloud_account=cloud_account,
65
+ disable_secrets=disable_secrets,
66
+ )
67
+
68
+ @property
69
+ def is_running(self) -> bool:
70
+ return self._studio.status == Status.Running
71
+
72
+ def start(self) -> None:
73
+ """Starts the sandbox if it is not already running."""
74
+ if self._studio.status == Status.Running:
75
+ raise RuntimeError(
76
+ "Cannot start sandbox: it is already running.\n\n"
77
+ "To ensure proper lifecycle management, either:\n"
78
+ " • Avoid calling `start()` multiple times manually, or\n"
79
+ " • Use the sandbox as a context manager:\n"
80
+ " with Sandbox() as sandbox:\n"
81
+ " # your code here\n"
82
+ )
83
+
84
+ if self._studio.status == Status.Pending:
85
+ raise RuntimeError("Cannot start sandbox: it is already starting. Wait for it to finish starting.")
86
+ self._studio.start(machine=self._machine, interruptible=self._interruptible)
87
+
88
+ def delete(self) -> None:
89
+ """Deletes the sandbox if it is not already deleted."""
90
+ if self._studio.status == Status.NotCreated:
91
+ raise RuntimeError("Cannot delete sandbox: it is not created.")
92
+ self._studio.delete()
93
+
94
+ def run(self, command: str) -> Output:
95
+ """Runs the command and returns the output."""
96
+ output, exit_code = self._studio.run_with_exit_code(command)
97
+ if exit_code != 0:
98
+ raise Exception(f"Command failed with exit code {exit_code}: {output}")
99
+ return Output(text=output, exit_code=exit_code)
100
+
101
+ @staticmethod
102
+ def _validate_python_code(code: str) -> None:
103
+ """Validates Python code for syntax errors.
104
+
105
+ Args:
106
+ code: The Python code string to validate
107
+
108
+ Raises:
109
+ SyntaxError: If the code has syntax errors
110
+ ValueError: If the code is empty or only whitespace
111
+ IndentationError: If the code has improper indentation
112
+
113
+ Note:
114
+ This validation only catches syntax-level errors. Runtime errors like
115
+ NameError or TypeError can only be caught when the code is actually executed.
116
+ """
117
+ if not code or not code.strip():
118
+ raise ValueError("Code cannot be empty or only whitespace")
119
+
120
+ try:
121
+ ast.parse(code)
122
+ except (SyntaxError, IndentationError) as e:
123
+ error_type = type(e).__name__
124
+ raise type(e)(f"Invalid Python {error_type.lower()}: {e}") from e
125
+
126
+ def run_python_code(self, code: str) -> Output:
127
+ """Runs the python code and returns the output.
128
+
129
+ Args:
130
+ code: The Python code string to execute
131
+
132
+ Returns:
133
+ Output: The result of executing the code
134
+
135
+ Raises:
136
+ SyntaxError: If the code has syntax errors
137
+ ValueError: If the code is empty or only whitespace
138
+ """
139
+ # Validate the code before running
140
+ self._validate_python_code(code)
141
+
142
+ command = f"python - <<EOF\n{code}\nEOF"
143
+ return self.run(command)
144
+
145
+ def __enter__(self) -> "_Sandbox":
146
+ """Starts the sandbox if it is not running and returns the sandbox."""
147
+ self.start()
148
+ return self
149
+
150
+ def __exit__(
151
+ self,
152
+ exc_type: Optional[Type[BaseException]] = None,
153
+ exc_value: Optional[BaseException] = None,
154
+ traceback: Optional[types.TracebackType] = None,
155
+ ) -> None:
156
+ """Deletes the sandbox after use."""
157
+ self._studio.delete()
@@ -39,8 +39,8 @@ MESSAGE_GUIDE_SIGN_LICENSE = (
39
39
  "│ Details: license key ({license_strats}...{license_ends}) for package {package_name:<56} │\n"
40
40
  "│ Please make sure you have signed the license agreement and set the license key. │\n"
41
41
  "│ │\n"
42
- f"│ Sign the license agreement here (if you dont have Lightning account, you will asked to create one): │\n"
43
- f"│ {generate_url_user_settings():<102}\n"
42
+ "│ Sign the license agreement here (if you dont have Lightning account, you will asked to create one): │\n"
43
+ "│ {link}\n"
44
44
  "│ │\n"
45
45
  "│ Once you have the license key, you may need to reinstall this package to activate it. Use the commands: │\n"
46
46
  "│ │\n"
@@ -57,6 +57,7 @@ def generate_message_guide_sign_license(package_name: str, reason: str, license_
57
57
  if not license_key:
58
58
  license_key = "." * 64
59
59
  return MESSAGE_GUIDE_SIGN_LICENSE.format(
60
+ link=generate_url_user_settings(name=package_name),
60
61
  package_name=package_name,
61
62
  reason=reason.ljust(102, " "),
62
63
  license_strats=license_key[:5],
@@ -273,12 +274,17 @@ class LightningLicense:
273
274
  @property
274
275
  def license_key(self) -> Optional[str]:
275
276
  """Get the license key."""
277
+ name = self.product_name.replace("-", "_")
278
+ if not self._license_key:
279
+ # If the license key is not set, first try to find it env variables
280
+ self._license_key = os.environ.get(f"LIGHTNING_{name.upper()}_LICENSE_KEY", None)
281
+ if not self._license_key:
282
+ # If the license key is not set, second try to find it in the package root
283
+ self._license_key = self._find_package_license_key(name)
284
+ # If not found, try to find it in the user home
276
285
  if not self._license_key:
277
- # If the license key is not set, fist try to find it in the package root
278
- self._license_key = self._find_package_license_key(self.product_name.replace("-", "_"))
279
286
  # If not found, try to find it in the user home
280
- if not self._license_key:
281
- self._license_key = self._find_user_license_key(self.product_name)
287
+ self._license_key = self._find_user_license_key(self.product_name)
282
288
  return self._license_key
283
289
 
284
290
  @property
lightning_sdk/studio.py CHANGED
@@ -76,6 +76,7 @@ class Studio:
76
76
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
77
77
  provider: Optional[str] = None,
78
78
  source: Optional[V1CloudSpaceSourceType] = None,
79
+ disable_secrets: bool = False,
79
80
  ) -> None:
80
81
  self._studio_api = StudioApi()
81
82
  self._cluster_api = ClusterApi()
@@ -83,6 +84,7 @@ class Studio:
83
84
  self._teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
84
85
  self._cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
85
86
  self._setup_done = False
87
+ self._disable_secrets = disable_secrets
86
88
 
87
89
  self._plugins = {}
88
90
 
@@ -107,7 +109,11 @@ class Studio:
107
109
  except ValueError as e:
108
110
  if create_ok:
109
111
  self._studio = self._studio_api.create_studio(
110
- name, self._teamspace.id, cloud_account=self._cloud_account, source=source
112
+ name,
113
+ self._teamspace.id,
114
+ cloud_account=self._cloud_account,
115
+ source=source,
116
+ disable_secrets=self._disable_secrets,
111
117
  )
112
118
  else:
113
119
  raise ValueError(f"Studio {name} does not exist.") from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 0.2.22
3
+ Version: 0.2.23
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License