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.
Files changed (101) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/base_studio_api.py +9 -2
  3. lightning_sdk/api/deployment_api.py +9 -9
  4. lightning_sdk/api/license_api.py +2 -2
  5. lightning_sdk/api/llm_api.py +7 -11
  6. lightning_sdk/api/pipeline_api.py +31 -10
  7. lightning_sdk/api/studio_api.py +6 -0
  8. lightning_sdk/base_studio.py +22 -6
  9. lightning_sdk/cli/entrypoint.py +15 -13
  10. lightning_sdk/cli/start.py +5 -2
  11. lightning_sdk/deployment/deployment.py +17 -7
  12. lightning_sdk/lightning_cloud/openapi/__init__.py +20 -0
  13. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  14. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +114 -1
  15. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +206 -0
  16. lightning_sdk/lightning_cloud/openapi/api/cloudy_service_api.py +129 -0
  17. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +97 -0
  18. lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +105 -0
  19. lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +118 -1
  20. lightning_sdk/lightning_cloud/openapi/api/user_service_api.py +105 -0
  21. lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
  22. lightning_sdk/lightning_cloud/openapi/models/__init__.py +19 -0
  23. lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +27 -1
  24. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +81 -3
  25. lightning_sdk/lightning_cloud/openapi/models/cloudspace_id_visibility_body.py +123 -0
  26. lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +27 -1
  27. lightning_sdk/lightning_cloud/openapi/models/credits_autoreplenish_body.py +175 -0
  28. lightning_sdk/lightning_cloud/openapi/models/credits_autoreplenish_body1.py +175 -0
  29. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +79 -1
  30. lightning_sdk/lightning_cloud/openapi/models/metricsstream_create_body.py +27 -1
  31. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
  32. lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +27 -1
  33. lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body1.py +123 -0
  34. lightning_sdk/lightning_cloud/openapi/models/project_id_agents_body.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +53 -1
  36. lightning_sdk/lightning_cloud/openapi/models/update.py +29 -3
  37. lightning_sdk/lightning_cloud/openapi/models/v1_assistant.py +27 -1
  38. lightning_sdk/lightning_cloud/openapi/models/v1_check_cluster_name_availability_request.py +123 -0
  39. lightning_sdk/lightning_cloud/openapi/models/v1_check_cluster_name_availability_response.py +123 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +2 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +79 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_config.py +29 -3
  43. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +29 -3
  44. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
  45. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_session.py +29 -3
  46. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_specialized_view.py +104 -0
  47. lightning_sdk/lightning_cloud/openapi/models/v1_cloudy_expert.py +279 -0
  48. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +79 -1
  49. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
  50. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
  51. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +105 -1
  52. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_status.py +27 -1
  53. lightning_sdk/lightning_cloud/openapi/models/v1_conversation_response_chunk.py +29 -3
  54. lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +29 -3
  55. lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +27 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_create_organization_request.py +79 -1
  57. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_status.py +47 -21
  58. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster.py +253 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +853 -0
  60. lightning_sdk/lightning_cloud/openapi/models/v1_get_job_stats_response.py +53 -1
  61. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
  62. lightning_sdk/lightning_cloud/openapi/models/v1_instance_overprovisioning_spec.py +29 -27
  63. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +123 -0
  64. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1_status.py +149 -0
  65. lightning_sdk/lightning_cloud/openapi/models/v1_lightning_run.py +53 -1
  66. lightning_sdk/lightning_cloud/openapi/models/v1_list_cloudy_experts_response.py +123 -0
  67. lightning_sdk/lightning_cloud/openapi/models/v1_list_clusters_response.py +6 -6
  68. lightning_sdk/lightning_cloud/openapi/models/v1_list_project_clusters_response.py +6 -6
  69. lightning_sdk/lightning_cloud/openapi/models/v1_lite_published_cloud_space_response.py +513 -0
  70. lightning_sdk/lightning_cloud/openapi/models/v1_login_request.py +27 -1
  71. lightning_sdk/lightning_cloud/openapi/models/v1_magic_link_login_request.py +29 -3
  72. lightning_sdk/lightning_cloud/openapi/models/v1_magic_link_login_response.py +27 -1
  73. lightning_sdk/lightning_cloud/openapi/models/v1_metrics_stream.py +27 -1
  74. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
  75. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +27 -1
  76. lightning_sdk/lightning_cloud/openapi/models/v1_shared_filesystem.py +131 -1
  77. lightning_sdk/lightning_cloud/openapi/models/v1_token_usage.py +175 -0
  78. lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_visibility_response.py +97 -0
  79. lightning_sdk/lightning_cloud/openapi/models/v1_update_organization_credits_auto_replenish_response.py +97 -0
  80. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_credits_auto_replenish_response.py +97 -0
  81. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
  82. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +234 -104
  83. lightning_sdk/lightning_cloud/openapi/models/v1_volume.py +78 -104
  84. lightning_sdk/lightning_cloud/openapi/models/v1_volume_state.py +104 -0
  85. lightning_sdk/llm/llm.py +113 -115
  86. lightning_sdk/llm/public_assistants.json +8 -0
  87. lightning_sdk/pipeline/__init__.py +11 -2
  88. lightning_sdk/pipeline/pipeline.py +54 -14
  89. lightning_sdk/pipeline/printer.py +36 -16
  90. lightning_sdk/pipeline/schedule.py +2 -1
  91. lightning_sdk/pipeline/{types.py → steps.py} +77 -56
  92. lightning_sdk/pipeline/utils.py +65 -3
  93. lightning_sdk/sandbox.py +157 -0
  94. lightning_sdk/services/license.py +12 -6
  95. lightning_sdk/studio.py +10 -1
  96. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/METADATA +1 -1
  97. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/RECORD +101 -79
  98. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/LICENSE +0 -0
  99. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/WHEEL +0 -0
  100. {lightning_sdk-0.2.22.dist-info → lightning_sdk-0.2.24rc0.dist-info}/entry_points.txt +0 -0
  101. {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
- from lightning_sdk.pipeline.utils import DEFAULT
35
-
36
-
37
- class Deployment:
36
+ class DeploymentStep:
38
37
  # Note: This class is only temporary while pipeline is wip
39
38
 
40
39
  def __init__(
41
40
  self,
42
41
  name: Optional[str] = None,
42
+ studio: Optional[Union[str, Studio]] = None,
43
43
  machine: Optional["Machine"] = None,
44
44
  image: Optional[str] = None,
45
- autoscale: Optional["AutoScaleConfig"] = None,
46
- ports: Optional[List[float]] = None,
47
- release_strategy: Optional["ReleaseStrategy"] = None,
45
+ autoscale: Optional[AutoScaleConfig] = None,
46
+ ports: Optional[Union[float, List[float]]] = None,
47
+ release_strategy: Optional[ReleaseStrategy] = None,
48
48
  entrypoint: Optional[str] = None,
49
49
  command: Optional[str] = None,
50
- env: Union[List[Union["Secret", "Env"]], Dict[str, str], None] = None,
50
+ commands: Optional[List[str]] = None,
51
+ env: Union[List[Union[Secret, Env]], Dict[str, str], None] = None,
51
52
  spot: Optional[bool] = None,
52
53
  replicas: Optional[int] = None,
53
- health_check: Optional[Union["HttpHealthCheck", "ExecHealthCheck"]] = None,
54
- auth: Optional[Union["BasicAuth", "TokenAuth"]] = None,
54
+ health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None,
55
+ auth: Optional[Union[BasicAuth, TokenAuth]] = None,
55
56
  cloud_account: Optional[str] = None,
56
57
  custom_domain: Optional[str] = None,
57
58
  quantity: Optional[int] = None,
59
+ include_credentials: Optional[bool] = None,
58
60
  wait_for: Optional[Union[str, List[str]]] = DEFAULT,
59
61
  ) -> None:
60
62
  self.name = name
61
- self.machine = machine
63
+ self.studio = _get_studio(studio)
64
+ if cloud_account and studio and cloud_account != studio.cloud_account != cloud_account:
65
+ raise ValueError(
66
+ f"The provided cloud account `{cloud_account}` doesn't match"
67
+ f" the Studio cloud account {self.studio.cloud_account}"
68
+ )
69
+
70
+ self.machine = machine or Machine.CPU
62
71
  self.image = image
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="CPU" if machine.is_cpu() else "GPU",
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(self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: bool) -> V1PipelineStep:
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=to_wait_for(self.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 Job:
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(self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: bool) -> V1PipelineStep:
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=to_wait_for(self.wait_for),
211
+ wait_for=_to_wait_for(self.wait_for),
185
212
  job=body,
186
213
  )
187
214
 
188
215
 
189
- class MMT:
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(self, teamspace: "Teamspace", cloud_account: str, shared_filesystem: bool) -> V1PipelineStep:
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=to_wait_for(self.wait_for),
296
+ wait_for=_to_wait_for(self.wait_for),
262
297
  mmt=body,
263
298
  )
264
299
 
265
300
 
266
- def to_wait_for(wait_for: Optional[Union[str, List[str]]]) -> Optional[List[str]]:
267
- if wait_for == DEFAULT:
268
- return wait_for
269
-
270
- if wait_for is None:
271
- return []
272
-
273
- return wait_for if isinstance(wait_for, list) else [wait_for]
274
-
275
-
276
- def _validate_cloud_account(pipeline_cloud_account: str, step_cloud_account: str, shared_filesystem: bool) -> None:
277
- if not shared_filesystem:
278
- return
279
-
280
- if pipeline_cloud_account != "" and step_cloud_account != "" and pipeline_cloud_account != step_cloud_account:
281
- raise ValueError(
282
- "With shared filesystem enabled, all the pipeline steps wait_for to be on the same cluster."
283
- f" Found {pipeline_cloud_account} and {step_cloud_account}"
284
- )
285
-
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 _get_studio(studio: Union["Studio", str, None]) -> Union[Studio, None]:
288
- if studio is None:
289
- return None
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
- return Studio(studio)
315
+ __all__ = ["JobStep", "MMTStep", "DeploymentStep", "DeploymentReleaseStep"]
@@ -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 V1PipelineStep
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 != DEFAULT:
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
@@ -0,0 +1,157 @@
1
+ import ast
2
+ import types
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Optional, Type, Union
6
+
7
+ from lightning_sdk.machine import Machine
8
+ from lightning_sdk.organization import Organization
9
+ from lightning_sdk.status import Status
10
+ from lightning_sdk.studio import Studio
11
+ from lightning_sdk.teamspace import Teamspace
12
+ from lightning_sdk.user import User
13
+
14
+
15
+ @dataclass
16
+ class Output:
17
+ text: str
18
+ exit_code: int
19
+
20
+
21
+ class _Sandbox:
22
+ """Sandbox runs AI generated code safely and discards the machine after use.
23
+
24
+ Users can run any arbitrary code in a sandbox with sudo permissions.
25
+
26
+ Args:
27
+ name: The name of the sandbox.
28
+ machine: The machine to use for the sandbox.
29
+ interruptible: Whether the sandbox is interruptible.
30
+ teamspace: The teamspace to use for the sandbox.
31
+ org: The organization to use for the sandbox.
32
+ user: The user to use for the sandbox.
33
+ cloud_account: The cloud account to use for the sandbox.
34
+ disable_secrets: If true, user secrets such as LIGHTNING_API_KEY are not stored in the sandbox.
35
+
36
+ Example:
37
+ with Sandbox() as sandbox:
38
+ output = sandbox.run("python --version")
39
+ print(output.text)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ name: Optional[str] = None,
45
+ machine: Optional[str] = None,
46
+ interruptible: Optional[bool] = None,
47
+ teamspace: Optional[Union[str, Teamspace]] = None,
48
+ org: Optional[Union[str, Organization]] = None,
49
+ user: Optional[Union[str, User]] = None,
50
+ cloud_account: Optional[str] = None,
51
+ disable_secrets: bool = True,
52
+ ) -> None:
53
+ if name is None:
54
+ timestr = datetime.now().strftime("%b-%d-%H_%M")
55
+ name = f"sandbox-{timestr}"
56
+
57
+ self._machine = machine or Machine.CPU
58
+ self._interruptible = interruptible
59
+ self._studio = Studio(
60
+ name=name,
61
+ teamspace=teamspace,
62
+ org=org,
63
+ user=user,
64
+ cloud_account=cloud_account,
65
+ disable_secrets=disable_secrets,
66
+ )
67
+
68
+ @property
69
+ def is_running(self) -> bool:
70
+ return self._studio.status == Status.Running
71
+
72
+ def start(self) -> None:
73
+ """Starts the sandbox if it is not already running."""
74
+ if self._studio.status == Status.Running:
75
+ raise RuntimeError(
76
+ "Cannot start sandbox: it is already running.\n\n"
77
+ "To ensure proper lifecycle management, either:\n"
78
+ " • Avoid calling `start()` multiple times manually, or\n"
79
+ " • Use the sandbox as a context manager:\n"
80
+ " with Sandbox() as sandbox:\n"
81
+ " # your code here\n"
82
+ )
83
+
84
+ if self._studio.status == Status.Pending:
85
+ raise RuntimeError("Cannot start sandbox: it is already starting. Wait for it to finish starting.")
86
+ self._studio.start(machine=self._machine, interruptible=self._interruptible)
87
+
88
+ def delete(self) -> None:
89
+ """Deletes the sandbox if it is not already deleted."""
90
+ if self._studio.status == Status.NotCreated:
91
+ raise RuntimeError("Cannot delete sandbox: it is not created.")
92
+ self._studio.delete()
93
+
94
+ def run(self, command: str) -> Output:
95
+ """Runs the command and returns the output."""
96
+ output, exit_code = self._studio.run_with_exit_code(command)
97
+ if exit_code != 0:
98
+ raise Exception(f"Command failed with exit code {exit_code}: {output}")
99
+ return Output(text=output, exit_code=exit_code)
100
+
101
+ @staticmethod
102
+ def _validate_python_code(code: str) -> None:
103
+ """Validates Python code for syntax errors.
104
+
105
+ Args:
106
+ code: The Python code string to validate
107
+
108
+ Raises:
109
+ SyntaxError: If the code has syntax errors
110
+ ValueError: If the code is empty or only whitespace
111
+ IndentationError: If the code has improper indentation
112
+
113
+ Note:
114
+ This validation only catches syntax-level errors. Runtime errors like
115
+ NameError or TypeError can only be caught when the code is actually executed.
116
+ """
117
+ if not code or not code.strip():
118
+ raise ValueError("Code cannot be empty or only whitespace")
119
+
120
+ try:
121
+ ast.parse(code)
122
+ except (SyntaxError, IndentationError) as e:
123
+ error_type = type(e).__name__
124
+ raise type(e)(f"Invalid Python {error_type.lower()}: {e}") from e
125
+
126
+ def run_python_code(self, code: str) -> Output:
127
+ """Runs the python code and returns the output.
128
+
129
+ Args:
130
+ code: The Python code string to execute
131
+
132
+ Returns:
133
+ Output: The result of executing the code
134
+
135
+ Raises:
136
+ SyntaxError: If the code has syntax errors
137
+ ValueError: If the code is empty or only whitespace
138
+ """
139
+ # Validate the code before running
140
+ self._validate_python_code(code)
141
+
142
+ command = f"python - <<EOF\n{code}\nEOF"
143
+ return self.run(command)
144
+
145
+ def __enter__(self) -> "_Sandbox":
146
+ """Starts the sandbox if it is not running and returns the sandbox."""
147
+ self.start()
148
+ return self
149
+
150
+ def __exit__(
151
+ self,
152
+ exc_type: Optional[Type[BaseException]] = None,
153
+ exc_value: Optional[BaseException] = None,
154
+ traceback: Optional[types.TracebackType] = None,
155
+ ) -> None:
156
+ """Deletes the sandbox after use."""
157
+ self._studio.delete()
@@ -39,8 +39,8 @@ MESSAGE_GUIDE_SIGN_LICENSE = (
39
39
  "│ Details: license key ({license_strats}...{license_ends}) for package {package_name:<56} │\n"
40
40
  "│ Please make sure you have signed the license agreement and set the license key. │\n"
41
41
  "│ │\n"
42
- f"│ Sign the license agreement here (if you dont have Lightning account, you will asked to create one): │\n"
43
- f"│ {generate_url_user_settings():<102}\n"
42
+ "│ Sign the license agreement here (if you dont have Lightning account, you will asked to create one): │\n"
43
+ "│ {link}\n"
44
44
  "│ │\n"
45
45
  "│ Once you have the license key, you may need to reinstall this package to activate it. Use the commands: │\n"
46
46
  "│ │\n"
@@ -57,6 +57,7 @@ def generate_message_guide_sign_license(package_name: str, reason: str, license_
57
57
  if not license_key:
58
58
  license_key = "." * 64
59
59
  return MESSAGE_GUIDE_SIGN_LICENSE.format(
60
+ link=generate_url_user_settings(name=package_name),
60
61
  package_name=package_name,
61
62
  reason=reason.ljust(102, " "),
62
63
  license_strats=license_key[:5],
@@ -273,12 +274,17 @@ class LightningLicense:
273
274
  @property
274
275
  def license_key(self) -> Optional[str]:
275
276
  """Get the license key."""
277
+ name = self.product_name.replace("-", "_")
278
+ if not self._license_key:
279
+ # If the license key is not set, first try to find it env variables
280
+ self._license_key = os.environ.get(f"LIGHTNING_{name.upper()}_LICENSE_KEY", None)
281
+ if not self._license_key:
282
+ # If the license key is not set, second try to find it in the package root
283
+ self._license_key = self._find_package_license_key(name)
284
+ # If not found, try to find it in the user home
276
285
  if not self._license_key:
277
- # If the license key is not set, fist try to find it in the package root
278
- self._license_key = self._find_package_license_key(self.product_name.replace("-", "_"))
279
286
  # If not found, try to find it in the user home
280
- if not self._license_key:
281
- self._license_key = self._find_user_license_key(self.product_name)
287
+ self._license_key = self._find_user_license_key(self.product_name)
282
288
  return self._license_key
283
289
 
284
290
  @property
lightning_sdk/studio.py CHANGED
@@ -76,6 +76,7 @@ class Studio:
76
76
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
77
77
  provider: Optional[str] = None,
78
78
  source: Optional[V1CloudSpaceSourceType] = None,
79
+ disable_secrets: bool = False,
79
80
  ) -> None:
80
81
  self._studio_api = StudioApi()
81
82
  self._cluster_api = ClusterApi()
@@ -83,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, self._teamspace.id, cloud_account=self._cloud_account, source=source
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 0.2.22
3
+ Version: 0.2.24rc0
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License