lightning-sdk 2025.7.17__py3-none-any.whl → 2025.7.30rc0__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 +3 -2
- lightning_sdk/api/cloud_account_api.py +204 -0
- lightning_sdk/api/deployment_api.py +11 -0
- lightning_sdk/api/job_api.py +82 -10
- lightning_sdk/api/llm_api.py +1 -1
- lightning_sdk/api/mmt_api.py +44 -5
- lightning_sdk/api/pipeline_api.py +4 -3
- lightning_sdk/api/studio_api.py +51 -8
- lightning_sdk/api/utils.py +6 -2
- lightning_sdk/cli/clusters_menu.py +3 -3
- lightning_sdk/cli/create.py +25 -11
- lightning_sdk/cli/deploy/_auth.py +19 -3
- lightning_sdk/cli/deploy/serve.py +21 -5
- lightning_sdk/cli/download.py +25 -1
- lightning_sdk/cli/entrypoint.py +4 -2
- lightning_sdk/cli/list.py +5 -1
- lightning_sdk/cli/run.py +3 -1
- lightning_sdk/cli/start.py +40 -8
- lightning_sdk/cli/switch.py +3 -1
- lightning_sdk/deployment/deployment.py +8 -0
- lightning_sdk/job/base.py +27 -3
- lightning_sdk/job/job.py +28 -4
- lightning_sdk/job/v1.py +10 -1
- lightning_sdk/job/v2.py +22 -2
- lightning_sdk/job/work.py +5 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +14 -1
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +428 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +153 -48
- lightning_sdk/lightning_cloud/openapi/api/cloudy_service_api.py +295 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +93 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +14 -1
- lightning_sdk/lightning_cloud/openapi/models/agentmanagedendpoints_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/blogposts_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/conversations_id_body1.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/messages_id_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/metricsstream_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_schedules_body.py +81 -3
- lightning_sdk/lightning_cloud/openapi/models/schedules_id_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/user_id_upgradetrigger_body.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/user_user_id_body.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_billing_subscription.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_blog_post.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloudy_settings.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation_response_chunk.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_billing_upgrade_trigger_record_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_blog_post_request.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_checkout_session_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_subscription_checkout_session_request.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_function_call.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_get_clickhouse_assistant_session_daily_aggregated_response.py → v1_get_assistant_session_daily_aggregated_response.py} +22 -22
- lightning_sdk/lightning_cloud/openapi/models/v1_get_cluster_health_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_like_status.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_published_managed_endpoints_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_endpoint.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +95 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_message.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_quote_subscription_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_resource_visibility.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_response_choice.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_schedule.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_service_health.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_slurm_v1.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_slurm_v1_status.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_tool_call.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_like_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_message_like_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -313
- lightning_sdk/lightning_cloud/openapi/models/v1_volume_state.py +1 -0
- lightning_sdk/llm/llm.py +69 -11
- lightning_sdk/llm/public_assistants.json +32 -8
- lightning_sdk/machine.py +151 -43
- lightning_sdk/mmt/base.py +20 -2
- lightning_sdk/mmt/mmt.py +25 -3
- lightning_sdk/mmt/v1.py +7 -1
- lightning_sdk/mmt/v2.py +27 -3
- lightning_sdk/models.py +1 -1
- lightning_sdk/organization.py +4 -0
- lightning_sdk/pipeline/pipeline.py +16 -5
- lightning_sdk/pipeline/printer.py +5 -3
- lightning_sdk/pipeline/schedule.py +844 -1
- lightning_sdk/pipeline/steps.py +19 -4
- lightning_sdk/sandbox.py +4 -1
- lightning_sdk/serve.py +2 -0
- lightning_sdk/studio.py +91 -44
- lightning_sdk/teamspace.py +19 -10
- lightning_sdk/utils/resolve.py +37 -2
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/METADATA +7 -5
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/RECORD +98 -85
- lightning_sdk/api/cluster_api.py +0 -119
- /lightning_sdk/cli/{inspect.py → inspection.py} +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.7.17.dist-info → lightning_sdk-2025.7.30rc0.dist-info}/top_level.txt +0 -0
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -4,7 +4,7 @@ import tempfile
|
|
|
4
4
|
import time
|
|
5
5
|
import zipfile
|
|
6
6
|
from threading import Event, Thread
|
|
7
|
-
from typing import Any, Dict, Generator, Mapping, Optional, Tuple, Union
|
|
7
|
+
from typing import Any, Dict, Generator, List, Mapping, Optional, Tuple, Union
|
|
8
8
|
|
|
9
9
|
import backoff
|
|
10
10
|
import requests
|
|
@@ -37,6 +37,7 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
37
37
|
V1CloudSpaceSeedFile,
|
|
38
38
|
V1CloudSpaceSourceType,
|
|
39
39
|
V1CloudSpaceState,
|
|
40
|
+
V1ClusterAccelerator,
|
|
40
41
|
V1EndpointType,
|
|
41
42
|
V1GetCloudSpaceInstanceStatusResponse,
|
|
42
43
|
V1GetLongRunningCommandInCloudSpaceResponse,
|
|
@@ -121,7 +122,7 @@ class StudioApi:
|
|
|
121
122
|
name: str,
|
|
122
123
|
teamspace_id: str,
|
|
123
124
|
cloud_account: Optional[str] = None,
|
|
124
|
-
source: Optional[V1CloudSpaceSourceType] = None,
|
|
125
|
+
source: Optional[Union[V1CloudSpaceSourceType, str]] = None,
|
|
125
126
|
disable_secrets: bool = False,
|
|
126
127
|
sandbox: bool = False,
|
|
127
128
|
cloud_space_environment_template_id: Optional[str] = None,
|
|
@@ -168,12 +169,26 @@ class StudioApi:
|
|
|
168
169
|
return startup_status and startup_status.top_up_restore_finished
|
|
169
170
|
|
|
170
171
|
def start_studio(
|
|
171
|
-
self,
|
|
172
|
+
self,
|
|
173
|
+
studio_id: str,
|
|
174
|
+
teamspace_id: str,
|
|
175
|
+
machine: Union[Machine, str],
|
|
176
|
+
interruptible: bool = False,
|
|
177
|
+
max_runtime: Optional[int] = None,
|
|
172
178
|
) -> None:
|
|
173
179
|
"""Start an existing Studio."""
|
|
180
|
+
# need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
|
|
181
|
+
optional_kwargs_compute_body = {}
|
|
182
|
+
|
|
183
|
+
if max_runtime is not None:
|
|
184
|
+
optional_kwargs_compute_body["requested_run_duration_seconds"] = str(max_runtime)
|
|
174
185
|
self._client.cloud_space_service_start_cloud_space_instance(
|
|
175
186
|
IdStartBody(
|
|
176
|
-
compute_config=V1UserRequestedComputeConfig(
|
|
187
|
+
compute_config=V1UserRequestedComputeConfig(
|
|
188
|
+
name=_machine_to_compute_name(machine),
|
|
189
|
+
spot=interruptible,
|
|
190
|
+
**optional_kwargs_compute_body,
|
|
191
|
+
)
|
|
177
192
|
),
|
|
178
193
|
teamspace_id,
|
|
179
194
|
studio_id,
|
|
@@ -253,12 +268,24 @@ class StudioApi:
|
|
|
253
268
|
break
|
|
254
269
|
time.sleep(1)
|
|
255
270
|
|
|
256
|
-
def get_machine(self, studio_id: str, teamspace_id: str) -> Machine:
|
|
271
|
+
def get_machine(self, studio_id: str, teamspace_id: str, cloud_account_id: str, org_id: str) -> Machine:
|
|
257
272
|
"""Get the current machine type the given Studio is running on."""
|
|
258
273
|
response: V1CloudSpaceInstanceConfig = self._client.cloud_space_service_get_cloud_space_instance_config(
|
|
259
274
|
project_id=teamspace_id, id=studio_id
|
|
260
275
|
)
|
|
261
|
-
|
|
276
|
+
accelerators = self._get_machines_for_cloud_account(
|
|
277
|
+
teamspace_id=teamspace_id, cloud_account_id=cloud_account_id, org_id=org_id
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
for accelerator in accelerators:
|
|
281
|
+
if response.compute_config.name in (
|
|
282
|
+
accelerator.slug,
|
|
283
|
+
accelerator.slug_multi_cloud,
|
|
284
|
+
accelerator.instance_id,
|
|
285
|
+
):
|
|
286
|
+
return Machine.from_str(accelerator.slug_multi_cloud)
|
|
287
|
+
|
|
288
|
+
return Machine.from_str(response.compute_config.name)
|
|
262
289
|
|
|
263
290
|
def get_interruptible(self, studio_id: str, teamspace_id: str) -> bool:
|
|
264
291
|
"""Get whether the Studio is running on a interruptible instance."""
|
|
@@ -268,6 +295,22 @@ class StudioApi:
|
|
|
268
295
|
|
|
269
296
|
return response.compute_config.spot
|
|
270
297
|
|
|
298
|
+
def _get_machines_for_cloud_account(
|
|
299
|
+
self, teamspace_id: str, cloud_account_id: str, org_id: str
|
|
300
|
+
) -> List[V1ClusterAccelerator]:
|
|
301
|
+
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
302
|
+
|
|
303
|
+
cloud_account_api = CloudAccountApi()
|
|
304
|
+
accelerators = cloud_account_api.list_cloud_account_accelerators(
|
|
305
|
+
teamspace_id=teamspace_id,
|
|
306
|
+
cloud_account_id=cloud_account_id,
|
|
307
|
+
org_id=org_id,
|
|
308
|
+
)
|
|
309
|
+
if not accelerators:
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
return list(filter(lambda acc: acc.enabled, accelerators.accelerator))
|
|
313
|
+
|
|
271
314
|
def _get_detached_command_status(
|
|
272
315
|
self, studio_id: str, teamspace_id: str, session_id: str
|
|
273
316
|
) -> V1GetLongRunningCommandInCloudSpaceResponse:
|
|
@@ -398,7 +441,7 @@ class StudioApi:
|
|
|
398
441
|
body=body,
|
|
399
442
|
)
|
|
400
443
|
|
|
401
|
-
def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str,
|
|
444
|
+
def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str, Any]:
|
|
402
445
|
"""Duplicates the given Studio from a given Teamspace into a given target Teamspace."""
|
|
403
446
|
target_teamspace = self._client.projects_service_get_project(target_teamspace_id)
|
|
404
447
|
init_kwargs = {}
|
|
@@ -421,7 +464,7 @@ class StudioApi:
|
|
|
421
464
|
init_kwargs["name"] = new_cloudspace.name
|
|
422
465
|
init_kwargs["teamspace"] = target_teamspace.name
|
|
423
466
|
|
|
424
|
-
self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False)
|
|
467
|
+
self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False, None)
|
|
425
468
|
return init_kwargs
|
|
426
469
|
|
|
427
470
|
def delete_studio(self, studio_id: str, teamspace_id: str) -> None:
|
lightning_sdk/api/utils.py
CHANGED
|
@@ -331,7 +331,9 @@ class _DummyResponse:
|
|
|
331
331
|
|
|
332
332
|
def _machine_to_compute_name(machine: Union[Machine, str]) -> str:
|
|
333
333
|
if isinstance(machine, Machine):
|
|
334
|
-
|
|
334
|
+
if machine.instance_type is not None:
|
|
335
|
+
return machine.instance_type
|
|
336
|
+
return machine.slug
|
|
335
337
|
return machine
|
|
336
338
|
|
|
337
339
|
|
|
@@ -352,7 +354,9 @@ def _get_registry_url() -> str:
|
|
|
352
354
|
|
|
353
355
|
|
|
354
356
|
def _sanitize_studio_remote_path(path: str, studio_id: str) -> str:
|
|
355
|
-
|
|
357
|
+
path = path.replace("/teamspace/studios/this_studio/", "")
|
|
358
|
+
root = f"/cloudspaces/{studio_id}/code/content/"
|
|
359
|
+
return os.path.join(root, path)
|
|
356
360
|
|
|
357
361
|
|
|
358
362
|
def _resolve_teamspace_remote_path(path: str) -> str:
|
|
@@ -5,7 +5,7 @@ from rich.console import Console
|
|
|
5
5
|
from simple_term_menu import TerminalMenu
|
|
6
6
|
|
|
7
7
|
from lightning_sdk import Teamspace
|
|
8
|
-
from lightning_sdk.api.
|
|
8
|
+
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
9
9
|
from lightning_sdk.lightning_cloud.openapi import Externalv1Cluster, V1ProjectClusterBinding
|
|
10
10
|
|
|
11
11
|
|
|
@@ -29,9 +29,9 @@ class _ClustersMenu:
|
|
|
29
29
|
selected_cluster_id = self._get_cluster_from_interactive_menu(
|
|
30
30
|
possible_clusters=teamspace.cloud_account_objs
|
|
31
31
|
)
|
|
32
|
-
|
|
32
|
+
cloud_account_api = CloudAccountApi()
|
|
33
33
|
|
|
34
|
-
return
|
|
34
|
+
return cloud_account_api.get_cloud_account(
|
|
35
35
|
cluster_id=selected_cluster_id, org_id=teamspace.owner.id, project_id=teamspace.id
|
|
36
36
|
)
|
|
37
37
|
except KeyboardInterrupt:
|
lightning_sdk/cli/create.py
CHANGED
|
@@ -7,12 +7,15 @@ import click
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
from lightning_sdk import Machine, Studio
|
|
10
|
-
from lightning_sdk.api.
|
|
10
|
+
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
11
11
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
12
|
-
from lightning_sdk.
|
|
12
|
+
from lightning_sdk.machine import CloudProvider
|
|
13
|
+
from lightning_sdk.utils.resolve import _resolve_deprecated_provider
|
|
13
14
|
|
|
14
|
-
_MACHINE_VALUES = tuple(
|
|
15
|
-
|
|
15
|
+
_MACHINE_VALUES = tuple(
|
|
16
|
+
[machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
|
|
17
|
+
)
|
|
18
|
+
_PROVIDER_VALUES = tuple([provider.value for provider in CloudProvider])
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
@click.group("create")
|
|
@@ -48,16 +51,26 @@ def create() -> None:
|
|
|
48
51
|
),
|
|
49
52
|
)
|
|
50
53
|
@click.option(
|
|
51
|
-
"--provider",
|
|
54
|
+
"--cloud-provider",
|
|
52
55
|
default=None,
|
|
53
56
|
type=click.Choice(_PROVIDER_VALUES),
|
|
54
57
|
help="The provider to create the studio on. If --cloud-account is specified, this option is prioritized.",
|
|
55
58
|
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--provider",
|
|
61
|
+
default=None,
|
|
62
|
+
type=click.Choice(_PROVIDER_VALUES),
|
|
63
|
+
help=(
|
|
64
|
+
"Deprecated. Use --cloud-provider instead. The provider to create the studio on. "
|
|
65
|
+
"If --cloud-account is specified, this option is prioritized."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
56
68
|
def studio(
|
|
57
69
|
name: str,
|
|
58
70
|
teamspace: Optional[str] = None,
|
|
59
71
|
start: Optional[str] = None,
|
|
60
72
|
cloud_account: Optional[str] = None,
|
|
73
|
+
cloud_provider: Optional[str] = None,
|
|
61
74
|
provider: Optional[str] = None,
|
|
62
75
|
) -> None:
|
|
63
76
|
"""Create a new studio on the Lightning AI platform.
|
|
@@ -70,12 +83,13 @@ def studio(
|
|
|
70
83
|
menu = _TeamspacesMenu()
|
|
71
84
|
teamspace_resolved = menu._resolve_teamspace(teamspace)
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
cloud_provider = str(_resolve_deprecated_provider(cloud_provider, provider))
|
|
87
|
+
|
|
88
|
+
if cloud_provider is not None:
|
|
89
|
+
cloud_account_api = CloudAccountApi()
|
|
90
|
+
cloud_account = cloud_account_api.resolve_cloud_account(
|
|
91
|
+
teamspace_resolved.id, cloud_account, cloud_provider, teamspace_resolved.default_cloud_account
|
|
92
|
+
)
|
|
79
93
|
|
|
80
94
|
# default cloud account to current studios cloud account if run from studio
|
|
81
95
|
# else it will fall back to teamspace default in the backend
|
|
@@ -2,7 +2,8 @@ import os
|
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import List, Optional, TypedDict
|
|
5
|
+
from typing import Any, List, Optional, TypedDict
|
|
6
|
+
from urllib.parse import urlencode
|
|
6
7
|
|
|
7
8
|
from rich.console import Console
|
|
8
9
|
from rich.prompt import Confirm
|
|
@@ -10,6 +11,7 @@ from rich.prompt import Confirm
|
|
|
10
11
|
from lightning_sdk import Teamspace
|
|
11
12
|
from lightning_sdk.api import UserApi
|
|
12
13
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
14
|
+
from lightning_sdk.lightning_cloud import env
|
|
13
15
|
from lightning_sdk.lightning_cloud.login import Auth, AuthServer
|
|
14
16
|
from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
|
|
15
17
|
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
@@ -24,6 +26,17 @@ class _AuthMode(Enum):
|
|
|
24
26
|
DEPLOY = "deploy"
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
class _AuthServer(AuthServer):
|
|
30
|
+
def __init__(self, mode: _AuthMode, *args: Any, **kwargs: Any) -> None:
|
|
31
|
+
self._mode = mode
|
|
32
|
+
super().__init__(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def get_auth_url(self, port: int) -> str:
|
|
35
|
+
redirect_uri = f"http://localhost:{port}/login-complete"
|
|
36
|
+
params = urlencode({"redirectTo": redirect_uri, "mode": self._mode.value, "okbhrt": LITSERVE_CODE})
|
|
37
|
+
return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
class _AuthLitServe(Auth):
|
|
28
41
|
def __init__(self, mode: _AuthMode, shall_confirm: bool = False) -> None:
|
|
29
42
|
super().__init__()
|
|
@@ -33,7 +46,10 @@ class _AuthLitServe(Auth):
|
|
|
33
46
|
def _run_server(self) -> None:
|
|
34
47
|
if self._shall_confirm:
|
|
35
48
|
proceed = Confirm.ask(
|
|
36
|
-
"
|
|
49
|
+
"[bold yellow]LitServe needs to authenticate with Lightning AI to deploy your server.[/bold yellow]\n"
|
|
50
|
+
"This will open a browser window for login.\n"
|
|
51
|
+
"Do you want to continue?",
|
|
52
|
+
default=True,
|
|
37
53
|
)
|
|
38
54
|
if not proceed:
|
|
39
55
|
raise RuntimeError(
|
|
@@ -42,7 +58,7 @@ class _AuthLitServe(Auth):
|
|
|
42
58
|
print("Opening browser for authentication...")
|
|
43
59
|
print("Please come back to the terminal after logging in.")
|
|
44
60
|
time.sleep(3)
|
|
45
|
-
|
|
61
|
+
_AuthServer(self._mode).login_with_browser(self)
|
|
46
62
|
|
|
47
63
|
|
|
48
64
|
def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
|
|
@@ -25,7 +25,9 @@ from lightning_sdk.cli.deploy._auth import (
|
|
|
25
25
|
from lightning_sdk.cli.deploy.devbox import _handle_devbox
|
|
26
26
|
from lightning_sdk.serve import _LitServeDeployer
|
|
27
27
|
|
|
28
|
-
_MACHINE_VALUES = tuple(
|
|
28
|
+
_MACHINE_VALUES = tuple(
|
|
29
|
+
[machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
|
|
30
|
+
)
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class _ServeGroup(click.Group):
|
|
@@ -317,12 +319,20 @@ def _handle_cloud(
|
|
|
317
319
|
ls_deployer = _LitServeDeployer(name=deployment_name, teamspace=None)
|
|
318
320
|
path = ls_deployer.dockerize_api(script_path, port=port, gpu=not machine.is_cpu(), tag=tag, print_success=False)
|
|
319
321
|
|
|
320
|
-
console.print(f"\
|
|
321
|
-
|
|
322
|
+
console.print(f"\n[bold]LitServe generated a Dockerfile at:[/bold]\n[u]{path}[/u]\n")
|
|
323
|
+
console.print("Please check that it matches your server setup.")
|
|
324
|
+
correct_dockerfile = (
|
|
325
|
+
True
|
|
326
|
+
if non_interactive
|
|
327
|
+
else Confirm.ask("Have you reviewed the Dockerfile and confirmed it's correct?", default=True)
|
|
328
|
+
)
|
|
322
329
|
if not correct_dockerfile:
|
|
323
|
-
console.print("Please
|
|
330
|
+
console.print("[red]Dockerfile review canceled. Please update the Dockerfile and try again.[/red]")
|
|
324
331
|
return
|
|
325
332
|
|
|
333
|
+
console.print(
|
|
334
|
+
"Building your container image now.\n[cyan bold]Make sure Docker is installed and running.[/cyan bold]\n"
|
|
335
|
+
)
|
|
326
336
|
with Progress(
|
|
327
337
|
SpinnerColumn(),
|
|
328
338
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -340,7 +350,12 @@ def _handle_cloud(
|
|
|
340
350
|
progress.remove_task(build_task)
|
|
341
351
|
|
|
342
352
|
except Exception as e:
|
|
343
|
-
console.print(
|
|
353
|
+
console.print(
|
|
354
|
+
"❌ Failed to build a container for your server.\n"
|
|
355
|
+
"Make sure Docker is installed and running, then try again.",
|
|
356
|
+
f"\n\nError details: {e}",
|
|
357
|
+
style="red",
|
|
358
|
+
)
|
|
344
359
|
return
|
|
345
360
|
|
|
346
361
|
# Push the container to the registry
|
|
@@ -373,6 +388,7 @@ def _handle_cloud(
|
|
|
373
388
|
f"{resolved_teamspace.owner.name}/{resolved_teamspace.name}/{container_basename}"
|
|
374
389
|
)
|
|
375
390
|
|
|
391
|
+
cloud_account = cloud_account or resolved_teamspace.default_cloud_account
|
|
376
392
|
if from_onboarding:
|
|
377
393
|
thread = Thread(
|
|
378
394
|
target=ls_deployer.run_on_cloud,
|
lightning_sdk/cli/download.py
CHANGED
|
@@ -17,6 +17,27 @@ from lightning_sdk.studio import Studio
|
|
|
17
17
|
from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _expand_remote_path(path: str) -> str:
|
|
21
|
+
"""Expand and normalize remote CLI paths.
|
|
22
|
+
|
|
23
|
+
- Strips leading `~/` or `~`
|
|
24
|
+
- Expands `~` to the user's home but returns relative to it
|
|
25
|
+
- Returns an empty string if path is empty or `~`
|
|
26
|
+
"""
|
|
27
|
+
if not path:
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
local_home = os.path.expanduser("~")
|
|
31
|
+
|
|
32
|
+
# Expand to absolute path and remove the local home prefix if present
|
|
33
|
+
path = os.path.expanduser(path)
|
|
34
|
+
if path.startswith(local_home):
|
|
35
|
+
path = path[len(local_home) :]
|
|
36
|
+
|
|
37
|
+
# Remove any leading "/" or "~" remnants
|
|
38
|
+
return path.lstrip("/~")
|
|
39
|
+
|
|
40
|
+
|
|
20
41
|
@click.group(name="download")
|
|
21
42
|
def download() -> None:
|
|
22
43
|
"""Download resources from Lightning AI."""
|
|
@@ -86,17 +107,20 @@ def folder(
|
|
|
86
107
|
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
87
108
|
|
|
88
109
|
if studio:
|
|
110
|
+
path = _expand_remote_path(path)
|
|
89
111
|
resolved_downloader = _resolve_studio(studio)
|
|
90
112
|
elif teamspace:
|
|
91
113
|
menu = _TeamspacesMenu()
|
|
92
114
|
resolved_downloader = menu._resolve_teamspace(teamspace)
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError("Either --studio or --teamspace must be provided")
|
|
93
117
|
|
|
94
118
|
if not path:
|
|
95
119
|
local_path /= resolved_downloader.name
|
|
96
120
|
path = ""
|
|
97
121
|
|
|
98
122
|
try:
|
|
99
|
-
if not path:
|
|
123
|
+
if not path and teamspace:
|
|
100
124
|
raise FileNotFoundError()
|
|
101
125
|
resolved_downloader.download_folder(remote_path=path, target_path=str(local_path))
|
|
102
126
|
except Exception as e:
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -21,7 +21,7 @@ from lightning_sdk.cli.deploy.serve import deploy
|
|
|
21
21
|
from lightning_sdk.cli.docker_cli import dockerize
|
|
22
22
|
from lightning_sdk.cli.download import download
|
|
23
23
|
from lightning_sdk.cli.generate import generate
|
|
24
|
-
from lightning_sdk.cli.
|
|
24
|
+
from lightning_sdk.cli.inspection import inspect
|
|
25
25
|
from lightning_sdk.cli.list import list_cli
|
|
26
26
|
from lightning_sdk.cli.open import open
|
|
27
27
|
from lightning_sdk.cli.run import run
|
|
@@ -50,7 +50,9 @@ def _notify_exception(exception_type: Type[BaseException], value: BaseException,
|
|
|
50
50
|
renderables.append(Text("\n\nFull traceback:\n", style="bold yellow"))
|
|
51
51
|
renderables.append(Syntax(tb_text, "python", theme="monokai", line_numbers=False, word_wrap=True))
|
|
52
52
|
else:
|
|
53
|
-
renderables.append(Text("\n\
|
|
53
|
+
renderables.append(Text("\n\n🐞 To view the full traceback, set: LIGHTNING_DEBUG=1"))
|
|
54
|
+
|
|
55
|
+
renderables.append(Text("\n📘 Need help? Run: lightning <command> --help", style="cyan"))
|
|
54
56
|
|
|
55
57
|
console.print(Panel(Group(*renderables), title="⚡ Lightning CLI Error", border_style="red"))
|
|
56
58
|
|
lightning_sdk/cli/list.py
CHANGED
|
@@ -274,7 +274,11 @@ def machines() -> None:
|
|
|
274
274
|
table.add_column("Name")
|
|
275
275
|
|
|
276
276
|
# Get all machine types from the enum
|
|
277
|
-
machine_types = [
|
|
277
|
+
machine_types = [
|
|
278
|
+
name
|
|
279
|
+
for name in dir(Machine)
|
|
280
|
+
if isinstance(getattr(Machine, name), Machine) and getattr(Machine, name)._include_in_cli
|
|
281
|
+
]
|
|
278
282
|
|
|
279
283
|
# Add rows to table
|
|
280
284
|
for name in sorted(machine_types):
|
lightning_sdk/cli/run.py
CHANGED
|
@@ -8,7 +8,9 @@ from lightning_sdk.machine import Machine
|
|
|
8
8
|
from lightning_sdk.mmt import MMT
|
|
9
9
|
from lightning_sdk.teamspace import Teamspace
|
|
10
10
|
|
|
11
|
-
_MACHINE_VALUES = tuple(
|
|
11
|
+
_MACHINE_VALUES = tuple(
|
|
12
|
+
[machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
|
|
13
|
+
)
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@click.group(name="run")
|
lightning_sdk/cli/start.py
CHANGED
|
@@ -4,10 +4,12 @@ import click
|
|
|
4
4
|
|
|
5
5
|
from lightning_sdk import Machine, Studio
|
|
6
6
|
from lightning_sdk.lightning_cloud.openapi.rest import ApiException
|
|
7
|
-
from lightning_sdk.
|
|
7
|
+
from lightning_sdk.machine import CloudProvider
|
|
8
8
|
|
|
9
|
-
_MACHINE_VALUES = tuple(
|
|
10
|
-
|
|
9
|
+
_MACHINE_VALUES = tuple(
|
|
10
|
+
[machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
|
|
11
|
+
)
|
|
12
|
+
_PROVIDER_VALUES = tuple([provider.value for provider in CloudProvider])
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@click.group("start")
|
|
@@ -35,14 +37,28 @@ def start() -> None:
|
|
|
35
37
|
type=click.Choice(_MACHINE_VALUES),
|
|
36
38
|
help="The machine type to start the studio on.",
|
|
37
39
|
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--cloud-provider",
|
|
42
|
+
default=None,
|
|
43
|
+
type=click.Choice(_PROVIDER_VALUES),
|
|
44
|
+
help=("The provider to create the studio on. If --cloud-account is specified, this option is prioritized."),
|
|
45
|
+
)
|
|
38
46
|
@click.option(
|
|
39
47
|
"--provider",
|
|
40
48
|
default=None,
|
|
41
|
-
show_default=True,
|
|
42
49
|
type=click.Choice(_PROVIDER_VALUES),
|
|
43
|
-
help=
|
|
50
|
+
help=(
|
|
51
|
+
"Deprecated. Use --cloud-provider instead. The provider to create the studio on. "
|
|
52
|
+
"If --cloud-account is specified, this option is prioritized."
|
|
53
|
+
),
|
|
44
54
|
)
|
|
45
|
-
def studio(
|
|
55
|
+
def studio(
|
|
56
|
+
name: str,
|
|
57
|
+
teamspace: Optional[str] = None,
|
|
58
|
+
machine: str = "CPU",
|
|
59
|
+
cloud_provider: Optional[str] = None,
|
|
60
|
+
provider: Optional[str] = None,
|
|
61
|
+
) -> None:
|
|
46
62
|
"""Start a studio on a given machine.
|
|
47
63
|
|
|
48
64
|
Example:
|
|
@@ -59,10 +75,26 @@ def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU", pro
|
|
|
59
75
|
owner, teamspace = None, None
|
|
60
76
|
|
|
61
77
|
try:
|
|
62
|
-
studio = Studio(
|
|
78
|
+
studio = Studio(
|
|
79
|
+
name=name,
|
|
80
|
+
teamspace=teamspace,
|
|
81
|
+
org=owner,
|
|
82
|
+
user=None,
|
|
83
|
+
create_ok=False,
|
|
84
|
+
cloud_provider=cloud_provider,
|
|
85
|
+
provider=provider,
|
|
86
|
+
)
|
|
63
87
|
except (RuntimeError, ValueError, ApiException) as first_error:
|
|
64
88
|
try:
|
|
65
|
-
studio = Studio(
|
|
89
|
+
studio = Studio(
|
|
90
|
+
name=name,
|
|
91
|
+
teamspace=teamspace,
|
|
92
|
+
org=None,
|
|
93
|
+
user=owner,
|
|
94
|
+
create_ok=False,
|
|
95
|
+
cloud_provider=cloud_provider,
|
|
96
|
+
provider=provider,
|
|
97
|
+
)
|
|
66
98
|
except (RuntimeError, ValueError, ApiException) as second_error:
|
|
67
99
|
raise first_error from second_error
|
|
68
100
|
|
lightning_sdk/cli/switch.py
CHANGED
|
@@ -5,7 +5,9 @@ import click
|
|
|
5
5
|
from lightning_sdk import Machine, Studio
|
|
6
6
|
from lightning_sdk.lightning_cloud.openapi.rest import ApiException
|
|
7
7
|
|
|
8
|
-
_MACHINE_VALUES = tuple(
|
|
8
|
+
_MACHINE_VALUES = tuple(
|
|
9
|
+
[machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
|
|
10
|
+
)
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@click.group("switch")
|
|
@@ -130,6 +130,7 @@ class Deployment:
|
|
|
130
130
|
include_credentials: Optional[bool] = None,
|
|
131
131
|
from_onboarding: Optional[bool] = None,
|
|
132
132
|
from_litserve: Optional[bool] = None,
|
|
133
|
+
max_runtime: Optional[int] = None,
|
|
133
134
|
) -> None:
|
|
134
135
|
"""The Lightning AI Deployment.
|
|
135
136
|
|
|
@@ -157,6 +158,10 @@ class Deployment:
|
|
|
157
158
|
quantity: The number of machines per replica to deploy.
|
|
158
159
|
include_credentials: Whether to include the environment variables for the SDK to authenticate
|
|
159
160
|
from_onboarding: Whether the deployment is from onboarding.
|
|
161
|
+
max_runtime: the duration (in seconds) for which to allocate the machine.
|
|
162
|
+
Irrelevant for most machines, required for some of the top-end machines on GCP.
|
|
163
|
+
If in doubt, set it. Won't have an effect on machines not requiring it.
|
|
164
|
+
Defaults to 3h
|
|
160
165
|
|
|
161
166
|
Note:
|
|
162
167
|
Since a teamspace can either be owned by an org or by a user directly,
|
|
@@ -227,6 +232,7 @@ class Deployment:
|
|
|
227
232
|
quantity=quantity,
|
|
228
233
|
include_credentials=include_credentials if include_credentials is not None else True,
|
|
229
234
|
cloudspace_id=cloudspace_id,
|
|
235
|
+
max_runtime=max_runtime,
|
|
230
236
|
),
|
|
231
237
|
strategy=to_strategy(release_strategy),
|
|
232
238
|
),
|
|
@@ -262,6 +268,7 @@ class Deployment:
|
|
|
262
268
|
cluster: Optional[str] = None, # deprecated in favor of cloud_account
|
|
263
269
|
quantity: Optional[int] = None,
|
|
264
270
|
include_credentials: Optional[bool] = None,
|
|
271
|
+
max_runtime: Optional[int] = None,
|
|
265
272
|
) -> None:
|
|
266
273
|
cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
|
|
267
274
|
|
|
@@ -288,6 +295,7 @@ class Deployment:
|
|
|
288
295
|
release_strategy=release_strategy,
|
|
289
296
|
quantity=quantity,
|
|
290
297
|
include_credentials=include_credentials,
|
|
298
|
+
max_runtime=max_runtime,
|
|
291
299
|
)
|
|
292
300
|
|
|
293
301
|
def stop(self) -> None:
|