lightning-sdk 0.2.21rc1__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 +69 -27
- lightning_sdk/api/pipeline_api.py +49 -9
- lightning_sdk/api/studio_api.py +2 -0
- lightning_sdk/cli/configure.py +34 -27
- lightning_sdk/cli/connect.py +2 -2
- lightning_sdk/cli/download.py +38 -2
- lightning_sdk/cli/entrypoint.py +15 -13
- lightning_sdk/cli/start.py +5 -2
- lightning_sdk/lightning_cloud/openapi/__init__.py +9 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +206 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +98 -5
- lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +118 -1
- lightning_sdk/lightning_cloud/openapi/api/schedules_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +9 -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/project_id_pipelines_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_schedules_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/schedules_id_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_delete_lit_registry_repository_image_artifact_version_by_digest_response.py +97 -0
- 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_project_storage_metadata_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_storage_breakdown_response.py +53 -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_schedule.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_update_user_request.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -183
- 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 +6 -1
- lightning_sdk/pipeline/__init__.py +12 -2
- lightning_sdk/pipeline/pipeline.py +59 -17
- lightning_sdk/pipeline/printer.py +121 -0
- lightning_sdk/pipeline/schedule.py +8 -0
- lightning_sdk/pipeline/{types.py → steps.py} +77 -59
- lightning_sdk/pipeline/utils.py +39 -17
- lightning_sdk/sandbox.py +157 -0
- lightning_sdk/services/license.py +12 -6
- lightning_sdk/studio.py +7 -1
- {lightning_sdk-0.2.21rc1.dist-info → lightning_sdk-0.2.23.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.21rc1.dist-info → lightning_sdk-0.2.23.dist-info}/RECORD +70 -58
- {lightning_sdk-0.2.21rc1.dist-info → lightning_sdk-0.2.23.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.21rc1.dist-info → lightning_sdk-0.2.23.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.21rc1.dist-info → lightning_sdk-0.2.23.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.2.21rc1.dist-info → lightning_sdk-0.2.23.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, ClassVar, Dict, List
|
|
3
|
+
|
|
4
|
+
from lightning_sdk.lightning_cloud.openapi.models import V1JobSpec, V1Pipeline, V1PipelineStepType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PipelinePrinter:
|
|
8
|
+
"""A helper class to print a formatted summary of a pipeline."""
|
|
9
|
+
|
|
10
|
+
STEP_TYPE_MAP: ClassVar[Dict[str, str]] = {
|
|
11
|
+
V1PipelineStepType.DEPLOYMENT: "Deployment",
|
|
12
|
+
V1PipelineStepType.JOB: "Job",
|
|
13
|
+
V1PipelineStepType.MMT: "MMT",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
initial: bool,
|
|
20
|
+
pipeline: V1Pipeline,
|
|
21
|
+
teamspace: Any,
|
|
22
|
+
proto_steps: List[Any],
|
|
23
|
+
schedules: List[Any],
|
|
24
|
+
) -> None:
|
|
25
|
+
self._name = name
|
|
26
|
+
self._initial = initial
|
|
27
|
+
self._pipeline = pipeline
|
|
28
|
+
self._teamspace = teamspace
|
|
29
|
+
self._proto_steps = proto_steps
|
|
30
|
+
self._shared_filesystem = pipeline.shared_filesystem
|
|
31
|
+
self._schedules = schedules
|
|
32
|
+
cluster_ids: set[str] = set()
|
|
33
|
+
for step in self._proto_steps:
|
|
34
|
+
job_spec = self._get_spec(step)
|
|
35
|
+
cluster_ids.add(job_spec.cluster_id)
|
|
36
|
+
self._cluster_ids = cluster_ids
|
|
37
|
+
|
|
38
|
+
def print_summary(self) -> None:
|
|
39
|
+
"""Prints the full, formatted summary of the created pipeline."""
|
|
40
|
+
self._print("\n" + "─" * 60)
|
|
41
|
+
self._print(f"✅ Pipeline '{self._name}' {'created' if self._initial else 'updated'} successfully!")
|
|
42
|
+
self._print("─" * 60)
|
|
43
|
+
|
|
44
|
+
self._print_steps()
|
|
45
|
+
self._print_schedules()
|
|
46
|
+
self._print_cloud_account()
|
|
47
|
+
self._print_shared_filesystem()
|
|
48
|
+
self._print_footer()
|
|
49
|
+
|
|
50
|
+
def _print(self, value: str) -> None:
|
|
51
|
+
print(value)
|
|
52
|
+
|
|
53
|
+
def _print_steps(self) -> None:
|
|
54
|
+
"""Prints the formatted list of pipeline steps."""
|
|
55
|
+
self._print("\nWorkflow Steps:")
|
|
56
|
+
if not self._proto_steps:
|
|
57
|
+
self._print(" - No steps defined.")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
for i, step in enumerate(self._proto_steps):
|
|
61
|
+
step_type = self.STEP_TYPE_MAP.get(step.type, "Unknown Step")
|
|
62
|
+
|
|
63
|
+
# Format the 'wait_for' list for cleaner output
|
|
64
|
+
if not step.wait_for:
|
|
65
|
+
wait_for_str = "(runs first)"
|
|
66
|
+
else:
|
|
67
|
+
# e.g., "'step_a', 'step_b'"
|
|
68
|
+
wait_for_str = f" waits for {', '.join([f'{w}' for w in step.wait_for])}"
|
|
69
|
+
|
|
70
|
+
self._print(f" ➡️ {i+1}. {step_type} '{step.name}' - {wait_for_str}")
|
|
71
|
+
|
|
72
|
+
def _print_schedules(self) -> None:
|
|
73
|
+
"""Prints the formatted list of schedules."""
|
|
74
|
+
self._print("\n🗓️ Schedules:")
|
|
75
|
+
if not self._schedules:
|
|
76
|
+
self._print(" - No schedules defined.")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
for schedule in self._schedules:
|
|
80
|
+
self._print(f" - '{schedule.name}' runs on cron schedule: `{schedule.cron_expression}`")
|
|
81
|
+
|
|
82
|
+
def _print_footer(self) -> None:
|
|
83
|
+
"""Prints the final link and closing message."""
|
|
84
|
+
cloud_url = os.getenv("LIGHTNING_CLOUD_URL", "https://lightning.ai").replace(":443", "")
|
|
85
|
+
|
|
86
|
+
# Using properties of assumed objects for a cleaner look
|
|
87
|
+
owner: str = self._teamspace.owner.name
|
|
88
|
+
team: str = self._teamspace.name
|
|
89
|
+
pipeline_name: str = self._name
|
|
90
|
+
|
|
91
|
+
pipeline_url = f"{cloud_url}/{owner}/{team}/pipelines/{pipeline_name}?app_id=pipeline"
|
|
92
|
+
|
|
93
|
+
self._print("\n" + "─" * 60)
|
|
94
|
+
self._print(f"🔗 View your pipeline in the browser:\n {pipeline_url}")
|
|
95
|
+
self._print("─" * 60 + "\n")
|
|
96
|
+
|
|
97
|
+
def _print_cloud_account(self) -> None:
|
|
98
|
+
if not self._proto_steps:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
self._print(f"\nCloud account{'s' if len(self._cluster_ids) > 1 else ''}:")
|
|
102
|
+
for cluster_id in sorted(self._cluster_ids):
|
|
103
|
+
self._print(f" - {cluster_id}")
|
|
104
|
+
|
|
105
|
+
def _print_shared_filesystem(self) -> None:
|
|
106
|
+
self._print(f"\nShared filesystem: {self._shared_filesystem.enabled}")
|
|
107
|
+
|
|
108
|
+
if self._shared_filesystem.enabled and len(self._cluster_ids) == 1:
|
|
109
|
+
shared_path = ""
|
|
110
|
+
if self._pipeline.shared_filesystem.s3_folder:
|
|
111
|
+
cluster_id = list(self._cluster_ids)[0] # noqa: RUF015
|
|
112
|
+
shared_path = f"/teamspace/s3_folders/pipelines-{cluster_id}"
|
|
113
|
+
if shared_path:
|
|
114
|
+
self._print(f" - {shared_path}")
|
|
115
|
+
|
|
116
|
+
def _get_spec(self, step: Any) -> V1JobSpec:
|
|
117
|
+
if step.type == V1PipelineStepType.DEPLOYMENT:
|
|
118
|
+
return step.deployment.spec
|
|
119
|
+
if step.type == V1PipelineStepType.MMT:
|
|
120
|
+
return step.mmt.spec
|
|
121
|
+
return step.job.spec
|
|
@@ -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,
|
|
58
|
-
|
|
59
|
+
include_credentials: Optional[bool] = None,
|
|
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,
|
|
@@ -132,17 +147,24 @@ class Job:
|
|
|
132
147
|
cloud_account_auth: bool = False,
|
|
133
148
|
entrypoint: str = "sh -c",
|
|
134
149
|
path_mappings: Optional[Dict[str, str]] = None,
|
|
135
|
-
wait_for: Union[str, List[str]] = DEFAULT,
|
|
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__(
|
|
@@ -207,18 +231,24 @@ class MMT:
|
|
|
207
231
|
cloud_account_auth: bool = False,
|
|
208
232
|
entrypoint: str = "sh -c",
|
|
209
233
|
path_mappings: Optional[Dict[str, str]] = None,
|
|
210
|
-
wait_for: Union[str, List[str]] = DEFAULT,
|
|
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
|
|
|
@@ -51,19 +52,40 @@ def prepare_steps(steps: List["V1PipelineStep"]) -> List["V1PipelineStep"]:
|
|
|
51
52
|
if name_to_idx[name] >= name_to_idx[current_step.name]:
|
|
52
53
|
raise ValueError("You can only reference prior steps")
|
|
53
54
|
|
|
54
|
-
print()
|
|
55
|
-
print("===== Generated Pipeline =====")
|
|
56
|
-
for step_idx, step in enumerate(steps):
|
|
57
|
-
step_type = ""
|
|
58
|
-
if step.type == V1PipelineStepType.DEPLOYMENT:
|
|
59
|
-
step_type = "Deployment"
|
|
60
|
-
elif step.type == V1PipelineStepType.JOB:
|
|
61
|
-
step_type = "Job"
|
|
62
|
-
else:
|
|
63
|
-
step_type = "MMT"
|
|
64
|
-
wait_for = "nothing" if len(step.wait_for) == 0 else step.wait_for
|
|
65
|
-
print(f"{step_idx} - {step_type}['{step.name}'] wait_for {wait_for}")
|
|
66
|
-
print("===== ================== =====")
|
|
67
|
-
print()
|
|
68
|
-
|
|
69
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
|