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.
- lightning_sdk/__init__.py +1 -1
- lightning_sdk/api/license_api.py +2 -2
- lightning_sdk/api/llm_api.py +2 -0
- lightning_sdk/api/pipeline_api.py +1 -0
- lightning_sdk/api/studio_api.py +2 -0
- lightning_sdk/cli/entrypoint.py +15 -13
- lightning_sdk/cli/start.py +5 -2
- lightning_sdk/lightning_cloud/openapi/__init__.py +8 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +206 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +97 -0
- lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +118 -1
- lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +8 -0
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/cloudspace_id_visibility_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/metricsstream_create_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_check_cluster_name_availability_request.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_check_cluster_name_availability_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_session.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +827 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_instance_overprovisioning_spec.py +29 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_lightning_run.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_clusters_response.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_list_project_clusters_response.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_lite_published_cloud_space_response.py +513 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_metrics_stream.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_shared_filesystem.py +131 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_visibility_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -79
- lightning_sdk/lightning_cloud/openapi/models/v1_volume.py +78 -104
- lightning_sdk/lightning_cloud/openapi/models/v1_volume_state.py +104 -0
- lightning_sdk/pipeline/__init__.py +11 -2
- lightning_sdk/pipeline/pipeline.py +38 -13
- lightning_sdk/pipeline/printer.py +29 -10
- lightning_sdk/pipeline/schedule.py +2 -1
- lightning_sdk/pipeline/{types.py → steps.py} +74 -56
- lightning_sdk/pipeline/utils.py +39 -2
- lightning_sdk/sandbox.py +157 -0
- lightning_sdk/services/license.py +12 -6
- lightning_sdk/studio.py +7 -1
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/RECORD +57 -48
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.23.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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[
|
|
46
|
-
ports: Optional[List[float]] = None,
|
|
47
|
-
release_strategy: Optional[
|
|
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
|
-
|
|
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[
|
|
54
|
-
auth: Optional[Union[
|
|
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.
|
|
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(
|
|
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=
|
|
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
|
|
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(
|
|
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=
|
|
208
|
+
wait_for=_to_wait_for(self.wait_for),
|
|
185
209
|
job=body,
|
|
186
210
|
)
|
|
187
211
|
|
|
188
212
|
|
|
189
|
-
class
|
|
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(
|
|
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=
|
|
293
|
+
wait_for=_to_wait_for(self.wait_for),
|
|
262
294
|
mmt=body,
|
|
263
295
|
)
|
|
264
296
|
|
|
265
297
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
312
|
+
__all__ = ["JobStep", "MMTStep", "DeploymentStep", "DeploymentReleaseStep"]
|
lightning_sdk/pipeline/utils.py
CHANGED
|
@@ -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]
|
lightning_sdk/sandbox.py
ADDED
|
@@ -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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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,
|
|
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
|