lightning-sdk 0.2.22__py3-none-any.whl → 0.2.24rc0__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/base_studio_api.py +9 -2
- lightning_sdk/api/deployment_api.py +9 -9
- lightning_sdk/api/license_api.py +2 -2
- lightning_sdk/api/llm_api.py +7 -11
- lightning_sdk/api/pipeline_api.py +31 -10
- lightning_sdk/api/studio_api.py +6 -0
- lightning_sdk/base_studio.py +22 -6
- lightning_sdk/cli/entrypoint.py +15 -13
- lightning_sdk/cli/start.py +5 -2
- lightning_sdk/deployment/deployment.py +17 -7
- lightning_sdk/lightning_cloud/openapi/__init__.py +20 -0
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +114 -1
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +206 -0
- lightning_sdk/lightning_cloud/openapi/api/cloudy_service_api.py +129 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +97 -0
- lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +118 -1
- lightning_sdk/lightning_cloud/openapi/api/user_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +19 -0
- lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +81 -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/credits_autoreplenish_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/credits_autoreplenish_body1.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/metricsstream_create_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body1.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/project_id_agents_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/update.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_assistant.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 +2 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_config.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_session.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_specialized_view.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloudy_expert.py +279 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation_response_chunk.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_organization_request.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_status.py +47 -21
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +853 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_job_stats_response.py +53 -1
- 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 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1_status.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_lightning_run.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_cloudy_experts_response.py +123 -0
- 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_login_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_magic_link_login_request.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_magic_link_login_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_metrics_stream.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.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_token_usage.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_visibility_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_organization_credits_auto_replenish_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_user_credits_auto_replenish_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +234 -104
- 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/llm/llm.py +113 -115
- lightning_sdk/llm/public_assistants.json +8 -0
- lightning_sdk/pipeline/__init__.py +11 -2
- lightning_sdk/pipeline/pipeline.py +54 -14
- lightning_sdk/pipeline/printer.py +36 -16
- lightning_sdk/pipeline/schedule.py +2 -1
- lightning_sdk/pipeline/{types.py → steps.py} +77 -56
- lightning_sdk/pipeline/utils.py +65 -3
- lightning_sdk/sandbox.py +157 -0
- lightning_sdk/services/license.py +12 -6
- lightning_sdk/studio.py +10 -1
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/RECORD +101 -79
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.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,64 @@ 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
|
|
72
|
+
autoscaling_metric_name = (
|
|
73
|
+
("CPU" if self.machine.is_cpu() else "GPU") if isinstance(self.machine, Machine) else "CPU"
|
|
74
|
+
)
|
|
63
75
|
self.autoscale = autoscale or AutoScaleConfig(
|
|
64
76
|
min_replicas=0,
|
|
65
77
|
max_replicas=1,
|
|
66
78
|
target_metrics=[
|
|
67
79
|
AutoScalingMetric(
|
|
68
|
-
name=
|
|
80
|
+
name=autoscaling_metric_name,
|
|
69
81
|
target=80,
|
|
70
82
|
)
|
|
71
83
|
],
|
|
@@ -74,22 +86,26 @@ class Deployment:
|
|
|
74
86
|
self.release_strategy = release_strategy
|
|
75
87
|
self.entrypoint = entrypoint
|
|
76
88
|
self.command = command
|
|
89
|
+
self.commands = commands
|
|
77
90
|
self.env = env
|
|
78
91
|
self.spot = spot
|
|
79
92
|
self.replicas = replicas or 1
|
|
80
93
|
self.health_check = health_check
|
|
81
94
|
self.auth = auth
|
|
82
|
-
self.cloud_account = cloud_account or ""
|
|
95
|
+
self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
|
|
83
96
|
self.custom_domain = custom_domain
|
|
84
97
|
self.quantity = quantity
|
|
98
|
+
self.include_credentials = include_credentials or True
|
|
85
99
|
self.wait_for = wait_for
|
|
86
100
|
|
|
87
|
-
def to_proto(
|
|
101
|
+
def to_proto(
|
|
102
|
+
self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
|
|
103
|
+
) -> V1PipelineStep:
|
|
88
104
|
_validate_cloud_account(cloud_account, self.cloud_account, shared_filesystem)
|
|
89
105
|
return V1PipelineStep(
|
|
90
106
|
name=self.name,
|
|
91
107
|
type=V1PipelineStepType.DEPLOYMENT,
|
|
92
|
-
wait_for=
|
|
108
|
+
wait_for=_to_wait_for(self.wait_for),
|
|
93
109
|
deployment=V1CreateDeploymentRequest(
|
|
94
110
|
autoscaling=to_autoscaling(self.autoscale, self.replicas),
|
|
95
111
|
endpoint=to_endpoint(self.ports, self.auth, self.custom_domain),
|
|
@@ -106,18 +122,20 @@ class Deployment:
|
|
|
106
122
|
machine=self.machine,
|
|
107
123
|
health_check=self.health_check,
|
|
108
124
|
quantity=self.quantity,
|
|
125
|
+
cloudspace_id=self.studio._studio.id if self.studio else None,
|
|
126
|
+
include_credentials=self.include_credentials,
|
|
109
127
|
),
|
|
110
128
|
strategy=to_strategy(self.release_strategy),
|
|
111
129
|
),
|
|
112
130
|
)
|
|
113
131
|
|
|
114
132
|
|
|
115
|
-
class
|
|
133
|
+
class JobStep:
|
|
116
134
|
# Note: This class is only temporary while pipeline is wip
|
|
117
135
|
|
|
118
136
|
def __init__(
|
|
119
137
|
self,
|
|
120
|
-
machine: Union["Machine", str],
|
|
138
|
+
machine: Optional[Union["Machine", str]] = None,
|
|
121
139
|
name: Optional[str] = None,
|
|
122
140
|
command: Optional[str] = None,
|
|
123
141
|
studio: Union["Studio", str, None] = None,
|
|
@@ -135,14 +153,21 @@ class Job:
|
|
|
135
153
|
wait_for: Union[str, List[str], None] = DEFAULT,
|
|
136
154
|
) -> None:
|
|
137
155
|
self.name = name
|
|
138
|
-
self.machine = machine
|
|
156
|
+
self.machine = machine or Machine.CPU
|
|
139
157
|
self.command = command
|
|
140
|
-
self.studio = studio
|
|
158
|
+
self.studio = _get_studio(studio)
|
|
159
|
+
|
|
160
|
+
if cloud_account and self.studio and cloud_account != self.studio.cloud_account != cloud_account:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"The provided cloud account `{cloud_account}` doesn't match"
|
|
163
|
+
f" the Studio cloud account {self.studio.cloud_account}"
|
|
164
|
+
)
|
|
165
|
+
|
|
141
166
|
self.image = image
|
|
142
167
|
self.teamspace = teamspace
|
|
143
168
|
self.org = org
|
|
144
169
|
self.user = user
|
|
145
|
-
self.cloud_account = cloud_account
|
|
170
|
+
self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
|
|
146
171
|
self.env = env
|
|
147
172
|
self.interruptible = interruptible
|
|
148
173
|
self.image_credentials = image_credentials
|
|
@@ -151,7 +176,9 @@ class Job:
|
|
|
151
176
|
self.path_mappings = path_mappings
|
|
152
177
|
self.wait_for = wait_for
|
|
153
178
|
|
|
154
|
-
def to_proto(
|
|
179
|
+
def to_proto(
|
|
180
|
+
self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
|
|
181
|
+
) -> V1PipelineStep:
|
|
155
182
|
studio = _get_studio(self.studio)
|
|
156
183
|
if isinstance(studio, Studio):
|
|
157
184
|
if self.cloud_account is None:
|
|
@@ -181,12 +208,12 @@ class Job:
|
|
|
181
208
|
return V1PipelineStep(
|
|
182
209
|
name=self.name,
|
|
183
210
|
type=V1PipelineStepType.JOB,
|
|
184
|
-
wait_for=
|
|
211
|
+
wait_for=_to_wait_for(self.wait_for),
|
|
185
212
|
job=body,
|
|
186
213
|
)
|
|
187
214
|
|
|
188
215
|
|
|
189
|
-
class
|
|
216
|
+
class MMTStep:
|
|
190
217
|
# Note: This class is only temporary while pipeline is wip
|
|
191
218
|
|
|
192
219
|
def __init__(
|
|
@@ -209,16 +236,22 @@ class MMT:
|
|
|
209
236
|
path_mappings: Optional[Dict[str, str]] = None,
|
|
210
237
|
wait_for: Optional[Union[str, List[str]]] = DEFAULT,
|
|
211
238
|
) -> None:
|
|
212
|
-
self.machine = machine
|
|
239
|
+
self.machine = machine or Machine.CPU
|
|
213
240
|
self.num_machines = num_machines
|
|
214
241
|
self.name = name
|
|
215
242
|
self.command = command
|
|
216
|
-
self.studio = studio
|
|
243
|
+
self.studio = _get_studio(studio)
|
|
244
|
+
|
|
245
|
+
if cloud_account and self.studio and cloud_account != self.studio.cloud_account != cloud_account:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"The provided cloud account `{cloud_account}` doesn't match"
|
|
248
|
+
f" the Studio cloud account {self.studio.cloud_account}"
|
|
249
|
+
)
|
|
217
250
|
self.image = image
|
|
218
251
|
self.teamspace = teamspace
|
|
219
252
|
self.org = org
|
|
220
253
|
self.user = user
|
|
221
|
-
self.cloud_account = cloud_account
|
|
254
|
+
self.cloud_account = cloud_account or "" if self.studio is None else self.studio.cloud_account
|
|
222
255
|
self.env = env
|
|
223
256
|
self.interruptible = interruptible
|
|
224
257
|
self.image_credentials = image_credentials
|
|
@@ -227,7 +260,9 @@ class MMT:
|
|
|
227
260
|
self.path_mappings = path_mappings
|
|
228
261
|
self.wait_for = wait_for
|
|
229
262
|
|
|
230
|
-
def to_proto(
|
|
263
|
+
def to_proto(
|
|
264
|
+
self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
|
|
265
|
+
) -> V1PipelineStep:
|
|
231
266
|
studio = _get_studio(self.studio)
|
|
232
267
|
if isinstance(studio, Studio):
|
|
233
268
|
if self.cloud_account is None:
|
|
@@ -258,37 +293,23 @@ class MMT:
|
|
|
258
293
|
return V1PipelineStep(
|
|
259
294
|
name=self.name,
|
|
260
295
|
type=V1PipelineStepType.MMT,
|
|
261
|
-
wait_for=
|
|
296
|
+
wait_for=_to_wait_for(self.wait_for),
|
|
262
297
|
mmt=body,
|
|
263
298
|
)
|
|
264
299
|
|
|
265
300
|
|
|
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
|
-
|
|
301
|
+
class DeploymentReleaseStep(DeploymentStep):
|
|
302
|
+
def __init__(self, *args: Any, deployment_name: Optional[str] = None, **kwargs: Any) -> None:
|
|
303
|
+
if not deployment_name:
|
|
304
|
+
raise ValueError("The deployment name is required")
|
|
305
|
+
self._deployment_name = deployment_name
|
|
306
|
+
super().__init__(*args, **kwargs)
|
|
286
307
|
|
|
287
|
-
def
|
|
288
|
-
|
|
289
|
-
|
|
308
|
+
def to_proto(self, *args: Any, **kwargs: Any) -> V1PipelineStep:
|
|
309
|
+
proto: V1PipelineStep = super().to_proto(*args, **kwargs)
|
|
310
|
+
proto.deployment.name = self._deployment_name
|
|
311
|
+
proto.deployment.pipeline_reuse_deployment_between_runs = True
|
|
312
|
+
return proto
|
|
290
313
|
|
|
291
|
-
if isinstance(studio, Studio):
|
|
292
|
-
return studio
|
|
293
314
|
|
|
294
|
-
|
|
315
|
+
__all__ = ["JobStep", "MMTStep", "DeploymentStep", "DeploymentReleaseStep"]
|
lightning_sdk/pipeline/utils.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import Any, List, Literal, Optional, Union
|
|
2
2
|
|
|
3
|
-
from lightning_sdk.lightning_cloud.openapi.models import
|
|
3
|
+
from lightning_sdk.lightning_cloud.openapi.models import (
|
|
4
|
+
V1JobSpec,
|
|
5
|
+
V1PipelineStep,
|
|
6
|
+
V1PipelineStepType,
|
|
7
|
+
V1SharedFilesystem,
|
|
8
|
+
)
|
|
9
|
+
from lightning_sdk.studio import Studio
|
|
4
10
|
|
|
5
11
|
DEFAULT = "DEFAULT"
|
|
6
12
|
|
|
@@ -20,7 +26,7 @@ def prepare_steps(steps: List["V1PipelineStep"]) -> List["V1PipelineStep"]:
|
|
|
20
26
|
else:
|
|
21
27
|
raise ValueError(f"A step with the name {current_step.name} already exists.")
|
|
22
28
|
|
|
23
|
-
if steps[0].wait_for
|
|
29
|
+
if steps[0].wait_for not in [None, DEFAULT, []]:
|
|
24
30
|
raise ValueError("The first step isn't allowed to receive `wait_for=...`.")
|
|
25
31
|
|
|
26
32
|
steps[0].wait_for = []
|
|
@@ -52,3 +58,59 @@ def prepare_steps(steps: List["V1PipelineStep"]) -> List["V1PipelineStep"]:
|
|
|
52
58
|
raise ValueError("You can only reference prior steps")
|
|
53
59
|
|
|
54
60
|
return steps
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_studio(studio: Union["Studio", str, None]) -> Union[Studio, None]:
|
|
64
|
+
if studio is None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
if isinstance(studio, Studio):
|
|
68
|
+
return studio
|
|
69
|
+
|
|
70
|
+
return Studio(studio)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _validate_cloud_account(
|
|
74
|
+
pipeline_cloud_account: str, step_cloud_account: str, shared_filesystem: Union[bool, V1SharedFilesystem]
|
|
75
|
+
) -> None:
|
|
76
|
+
shared_filesystem_enable = (
|
|
77
|
+
shared_filesystem.enabled if isinstance(shared_filesystem, V1SharedFilesystem) else shared_filesystem
|
|
78
|
+
)
|
|
79
|
+
if not shared_filesystem_enable:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if pipeline_cloud_account != "" and step_cloud_account != "" and pipeline_cloud_account != step_cloud_account:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"With shared filesystem enabled, all the pipeline steps requires to be on the same cluster."
|
|
85
|
+
f" Found {pipeline_cloud_account} and {step_cloud_account}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _to_wait_for(wait_for: Optional[Union[str, List[str]]]) -> Optional[Union[List[str], Literal["DEFAULT"]]]:
|
|
90
|
+
if wait_for == DEFAULT:
|
|
91
|
+
return wait_for
|
|
92
|
+
|
|
93
|
+
if wait_for is None:
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
return wait_for if isinstance(wait_for, list) else [wait_for]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _get_cloud_account(steps: List[V1PipelineStep]) -> Optional[str]:
|
|
100
|
+
if len(steps) == 0:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
cluster_ids: set[str] = set()
|
|
104
|
+
for step in steps:
|
|
105
|
+
job_spec = _get_spec(step)
|
|
106
|
+
cluster_ids.add(job_spec.cluster_id)
|
|
107
|
+
|
|
108
|
+
return sorted(cluster_ids)[0]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_spec(step: Any) -> V1JobSpec:
|
|
112
|
+
if step.type == V1PipelineStepType.DEPLOYMENT:
|
|
113
|
+
return step.deployment.spec
|
|
114
|
+
if step.type == V1PipelineStepType.MMT:
|
|
115
|
+
return step.mmt.spec
|
|
116
|
+
return step.job.spec
|
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,9 +84,13 @@ 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
|
|
|
91
|
+
if self._teamspace is None:
|
|
92
|
+
raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
|
|
93
|
+
|
|
89
94
|
if provider is not None:
|
|
90
95
|
if isinstance(provider, str) and provider in Provider.__members__:
|
|
91
96
|
provider = Provider(provider)
|
|
@@ -107,7 +112,11 @@ class Studio:
|
|
|
107
112
|
except ValueError as e:
|
|
108
113
|
if create_ok:
|
|
109
114
|
self._studio = self._studio_api.create_studio(
|
|
110
|
-
name,
|
|
115
|
+
name,
|
|
116
|
+
self._teamspace.id,
|
|
117
|
+
cloud_account=self._cloud_account,
|
|
118
|
+
source=source,
|
|
119
|
+
disable_secrets=self._disable_secrets,
|
|
111
120
|
)
|
|
112
121
|
else:
|
|
113
122
|
raise ValueError(f"Studio {name} does not exist.") from e
|