lightning-sdk 2025.10.14__py3-none-any.whl → 2025.10.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 +6 -3
- lightning_sdk/api/base_studio_api.py +13 -9
- lightning_sdk/api/job_api.py +4 -1
- lightning_sdk/api/license_api.py +26 -59
- lightning_sdk/api/studio_api.py +7 -2
- lightning_sdk/base_studio.py +30 -17
- lightning_sdk/cli/base_studio/list.py +1 -3
- lightning_sdk/cli/entrypoint.py +11 -34
- lightning_sdk/cli/groups.py +7 -0
- lightning_sdk/cli/license/__init__.py +14 -0
- lightning_sdk/cli/license/get.py +15 -0
- lightning_sdk/cli/license/list.py +45 -0
- lightning_sdk/cli/license/set.py +13 -0
- lightning_sdk/cli/studio/connect.py +42 -92
- lightning_sdk/cli/studio/create.py +23 -1
- lightning_sdk/cli/studio/start.py +12 -2
- lightning_sdk/cli/utils/get_base_studio.py +24 -0
- lightning_sdk/cli/utils/handle_machine_and_gpus_args.py +69 -0
- lightning_sdk/cli/utils/logging.py +121 -0
- lightning_sdk/cli/utils/ssh_connection.py +1 -1
- lightning_sdk/constants.py +1 -0
- lightning_sdk/helpers.py +53 -34
- lightning_sdk/job/base.py +7 -0
- lightning_sdk/job/job.py +8 -0
- lightning_sdk/job/v1.py +3 -0
- lightning_sdk/job/v2.py +4 -0
- lightning_sdk/lightning_cloud/login.py +260 -10
- lightning_sdk/lightning_cloud/openapi/__init__.py +16 -3
- lightning_sdk/lightning_cloud/openapi/api/auth_service_api.py +279 -0
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +117 -0
- lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +108 -108
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +16 -3
- lightning_sdk/lightning_cloud/openapi/models/create_machine_request_represents_the_request_to_create_a_machine.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/id_fork_body1.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/license_key_validate_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/update1.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_license_request.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_data_connection.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_license_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_filesystem_metric.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_transfer_estimate_response.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_incident.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_incident_detail.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_incident_event.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_license.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_filesystem_metrics_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_list_product_licenses_response.py → v1_list_license_response.py} +16 -16
- lightning_sdk/lightning_cloud/openapi/models/v1_list_platform_notifications_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_platform_notification.py +279 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_reset_api_key_request.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_reset_api_key_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_token_login_request.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_token_login_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_token_owner_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +139 -191
- lightning_sdk/lightning_cloud/openapi/models/{v1_product_license_check_response.py → v1_validate_license_response.py} +21 -21
- lightning_sdk/lightning_cloud/rest_client.py +48 -45
- lightning_sdk/machine.py +5 -0
- lightning_sdk/pipeline/steps.py +1 -0
- lightning_sdk/studio.py +55 -13
- lightning_sdk/utils/config.py +18 -3
- lightning_sdk/utils/license.py +13 -0
- lightning_sdk/utils/resolve.py +6 -1
- {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/RECORD +74 -54
- lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +0 -435
- lightning_sdk/services/license.py +0 -363
- {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/top_level.txt +0 -0
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
|
-
from
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import Optional
|
|
6
7
|
|
|
7
8
|
import click
|
|
8
9
|
|
|
9
|
-
from lightning_sdk.
|
|
10
|
+
from lightning_sdk.cli.utils.get_base_studio import get_base_studio_id
|
|
11
|
+
from lightning_sdk.cli.utils.handle_machine_and_gpus_args import handle_machine_and_gpus_args
|
|
10
12
|
from lightning_sdk.cli.utils.richt_print import studio_name_link
|
|
11
13
|
from lightning_sdk.cli.utils.save_to_config import save_studio_to_config, save_teamspace_to_config
|
|
12
14
|
from lightning_sdk.cli.utils.ssh_connection import configure_ssh_internal
|
|
@@ -16,81 +18,42 @@ from lightning_sdk.machine import CloudProvider, Machine
|
|
|
16
18
|
from lightning_sdk.studio import Studio
|
|
17
19
|
from lightning_sdk.utils.names import random_unique_name
|
|
18
20
|
|
|
19
|
-
DEFAULT_MACHINE = "CPU"
|
|
20
21
|
|
|
22
|
+
def _parse_args_or_get_from_current_studio(
|
|
23
|
+
teamspace: Optional[str],
|
|
24
|
+
cloud_account: Optional[str],
|
|
25
|
+
studio_type: Optional[str],
|
|
26
|
+
machine: Optional[str],
|
|
27
|
+
gpus: Optional[str],
|
|
28
|
+
cloud_provider: Optional[str],
|
|
29
|
+
name: Optional[str],
|
|
30
|
+
) -> tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
|
|
31
|
+
# Parse args provided by user
|
|
32
|
+
menu = TeamspacesMenu()
|
|
33
|
+
resolved_teamspace = menu(teamspace)
|
|
34
|
+
save_teamspace_to_config(resolved_teamspace, overwrite=False)
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
machine_name, machine_val = gpus.split(":", 1)
|
|
24
|
-
machine_name = machine_name.strip()
|
|
25
|
-
machine_val = machine_val.strip()
|
|
26
|
-
|
|
27
|
-
if not machine_val.isdigit() or int(machine_val) <= 0:
|
|
28
|
-
raise ValueError(f"Invalid GPU count '{machine_val}'. Must be a positive integer.")
|
|
29
|
-
|
|
30
|
-
machine_num = int(machine_val)
|
|
31
|
-
return machine_name, machine_num
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _construct_available_gpus(machine_options: Dict[str, str]) -> Set[str]:
|
|
35
|
-
# returns available gpus:count
|
|
36
|
-
available_gpus = set()
|
|
37
|
-
for v in machine_options.values():
|
|
38
|
-
if "_X_" in v:
|
|
39
|
-
gpu_type_num = v.replace("_X_", ":")
|
|
40
|
-
available_gpus.add(gpu_type_num)
|
|
41
|
-
else:
|
|
42
|
-
available_gpus.add(v)
|
|
43
|
-
return available_gpus
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _get_machine_from_gpus(gpus: str) -> Machine:
|
|
47
|
-
machine_name = gpus
|
|
48
|
-
machine_num = 1
|
|
49
|
-
|
|
50
|
-
if ":" in gpus:
|
|
51
|
-
machine_name, machine_num = _split_gpus_spec(gpus)
|
|
52
|
-
|
|
53
|
-
machine_options = {
|
|
54
|
-
m.name.lower(): m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if machine_num == 1:
|
|
58
|
-
# e.g. gpus=L4 or gpus=L4:1
|
|
59
|
-
gpu_key = machine_name.lower()
|
|
60
|
-
try:
|
|
61
|
-
return machine_options[gpu_key]
|
|
62
|
-
except KeyError:
|
|
63
|
-
available = ", ".join(_construct_available_gpus(machine_options))
|
|
64
|
-
raise ValueError(f"Invalid GPU type '{machine_name}'. Available options: {available}") from None
|
|
65
|
-
|
|
66
|
-
# Else: e.g. gpus=L4:4
|
|
67
|
-
gpu_key = f"{machine_name.lower()}_x_{machine_num}"
|
|
68
|
-
try:
|
|
69
|
-
return machine_options[gpu_key]
|
|
70
|
-
except KeyError:
|
|
71
|
-
available = ", ".join(_construct_available_gpus(machine_options))
|
|
72
|
-
raise ValueError(f"Invalid GPU configuration '{gpus}'. Available options: {available}") from None
|
|
36
|
+
template_id = get_base_studio_id(studio_type)
|
|
73
37
|
|
|
38
|
+
if cloud_provider is not None:
|
|
39
|
+
cloud_provider = CloudProvider(cloud_provider)
|
|
74
40
|
|
|
75
|
-
|
|
76
|
-
base_studios = BaseStudio()
|
|
77
|
-
base_studios = base_studios.list()
|
|
78
|
-
template_id = None
|
|
41
|
+
name = name or random_unique_name()
|
|
79
42
|
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
43
|
+
with suppress(ValueError):
|
|
44
|
+
# Gets current studio context to use its parameters as defaults
|
|
45
|
+
s = Studio()
|
|
46
|
+
if not teamspace:
|
|
47
|
+
resolved_teamspace = s.teamspace
|
|
48
|
+
save_teamspace_to_config(resolved_teamspace, overwrite=False)
|
|
49
|
+
if not cloud_account:
|
|
50
|
+
cloud_account = s.cloud_account
|
|
51
|
+
if not template_id:
|
|
52
|
+
template_id = s._studio.environment_template_id
|
|
53
|
+
if not machine and not gpus:
|
|
54
|
+
machine = s.machine
|
|
92
55
|
|
|
93
|
-
return template_id
|
|
56
|
+
return resolved_teamspace, cloud_account, template_id, machine, cloud_provider, name
|
|
94
57
|
|
|
95
58
|
|
|
96
59
|
@click.command("connect")
|
|
@@ -124,6 +87,7 @@ def _get_base_studio_id(studio_type: Optional[str]) -> Optional[str]:
|
|
|
124
87
|
"Defaults to the first available template.",
|
|
125
88
|
type=click.STRING,
|
|
126
89
|
)
|
|
90
|
+
@click.option("--interruptible", is_flag=True, help="Start the studio on an interruptible instance.")
|
|
127
91
|
def connect_studio(
|
|
128
92
|
name: Optional[str] = None,
|
|
129
93
|
teamspace: Optional[str] = None,
|
|
@@ -132,29 +96,21 @@ def connect_studio(
|
|
|
132
96
|
machine: Optional[str] = None,
|
|
133
97
|
gpus: Optional[str] = None,
|
|
134
98
|
studio_type: Optional[str] = None,
|
|
99
|
+
interruptible: bool = False,
|
|
135
100
|
) -> None:
|
|
136
101
|
"""Connect to a Studio.
|
|
137
102
|
|
|
138
103
|
Example:
|
|
139
104
|
lightning studio connect
|
|
140
105
|
"""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
save_teamspace_to_config(resolved_teamspace, overwrite=False)
|
|
145
|
-
|
|
146
|
-
if cloud_provider is not None:
|
|
147
|
-
cloud_provider = CloudProvider(cloud_provider)
|
|
148
|
-
|
|
149
|
-
name = name or random_unique_name()
|
|
150
|
-
|
|
151
|
-
# check for available base studios
|
|
152
|
-
template_id = _get_base_studio_id(studio_type)
|
|
106
|
+
teamspace, cloud_account, template_id, machine, cloud_provider, name = _parse_args_or_get_from_current_studio(
|
|
107
|
+
teamspace, cloud_account, studio_type, machine, gpus, cloud_provider, name
|
|
108
|
+
)
|
|
153
109
|
|
|
154
110
|
try:
|
|
155
111
|
studio = Studio(
|
|
156
112
|
name=name,
|
|
157
|
-
teamspace=
|
|
113
|
+
teamspace=teamspace,
|
|
158
114
|
create_ok=True,
|
|
159
115
|
cloud_provider=cloud_provider,
|
|
160
116
|
cloud_account=cloud_account,
|
|
@@ -167,16 +123,10 @@ def connect_studio(
|
|
|
167
123
|
|
|
168
124
|
Studio.show_progress = True
|
|
169
125
|
|
|
170
|
-
|
|
171
|
-
raise click.UsageError("Options --machine and --gpu are mutually exclusive. Provide only one.")
|
|
172
|
-
elif gpus:
|
|
173
|
-
machine = _get_machine_from_gpus(gpus.strip())
|
|
174
|
-
elif not machine:
|
|
175
|
-
machine = DEFAULT_MACHINE
|
|
126
|
+
machine = handle_machine_and_gpus_args(machine, gpus)
|
|
176
127
|
|
|
177
128
|
save_studio_to_config(studio)
|
|
178
|
-
|
|
179
|
-
studio.start(machine=machine, interruptible=False)
|
|
129
|
+
studio.start(machine=machine, interruptible=interruptible)
|
|
180
130
|
|
|
181
131
|
ssh_private_key_path = configure_ssh_internal()
|
|
182
132
|
|
|
@@ -4,6 +4,7 @@ from typing import Optional
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from lightning_sdk.cli.utils.get_base_studio import get_base_studio_id
|
|
7
8
|
from lightning_sdk.cli.utils.richt_print import studio_name_link
|
|
8
9
|
from lightning_sdk.cli.utils.save_to_config import save_teamspace_to_config
|
|
9
10
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
@@ -25,18 +26,34 @@ from lightning_sdk.studio import VM, Studio
|
|
|
25
26
|
help="The cloud account to create the studio on. Defaults to teamspace default.",
|
|
26
27
|
type=click.STRING,
|
|
27
28
|
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--studio-type",
|
|
31
|
+
help="The base studio template name to use for creating the studio. "
|
|
32
|
+
"Must be lowercase and hyphenated (use '-' instead of spaces). "
|
|
33
|
+
"Run 'lightning base-studio list' to see all available templates. "
|
|
34
|
+
"Defaults to the first available template.",
|
|
35
|
+
type=click.STRING,
|
|
36
|
+
)
|
|
28
37
|
def create_studio(
|
|
29
38
|
name: Optional[str] = None,
|
|
30
39
|
teamspace: Optional[str] = None,
|
|
31
40
|
cloud_provider: Optional[str] = None,
|
|
32
41
|
cloud_account: Optional[str] = None,
|
|
42
|
+
studio_type: Optional[str] = None,
|
|
33
43
|
) -> None:
|
|
34
44
|
"""Create a new Studio.
|
|
35
45
|
|
|
36
46
|
Example:
|
|
37
47
|
lightning studio create
|
|
38
48
|
"""
|
|
39
|
-
create_impl(
|
|
49
|
+
create_impl(
|
|
50
|
+
name=name,
|
|
51
|
+
teamspace=teamspace,
|
|
52
|
+
cloud_provider=cloud_provider,
|
|
53
|
+
cloud_account=cloud_account,
|
|
54
|
+
vm=False,
|
|
55
|
+
studio_type=studio_type,
|
|
56
|
+
)
|
|
40
57
|
|
|
41
58
|
|
|
42
59
|
def create_impl(
|
|
@@ -45,6 +62,7 @@ def create_impl(
|
|
|
45
62
|
cloud_provider: Optional[str],
|
|
46
63
|
cloud_account: Optional[str],
|
|
47
64
|
vm: bool,
|
|
65
|
+
studio_type: Optional[str],
|
|
48
66
|
) -> None:
|
|
49
67
|
menu = TeamspacesMenu()
|
|
50
68
|
|
|
@@ -57,6 +75,9 @@ def create_impl(
|
|
|
57
75
|
create_cls = VM if vm else Studio
|
|
58
76
|
cls_name = create_cls.__qualname__
|
|
59
77
|
|
|
78
|
+
# check for available base studios
|
|
79
|
+
template_id = get_base_studio_id(studio_type)
|
|
80
|
+
|
|
60
81
|
try:
|
|
61
82
|
create_cls = VM if vm else Studio
|
|
62
83
|
studio = create_cls(
|
|
@@ -65,6 +86,7 @@ def create_impl(
|
|
|
65
86
|
create_ok=True,
|
|
66
87
|
cloud_provider=cloud_provider,
|
|
67
88
|
cloud_account=cloud_account,
|
|
89
|
+
studio_type=template_id,
|
|
68
90
|
)
|
|
69
91
|
except (RuntimeError, ValueError, ApiException):
|
|
70
92
|
if name:
|
|
@@ -4,6 +4,7 @@ from typing import Optional
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from lightning_sdk.cli.utils.handle_machine_and_gpus_args import handle_machine_and_gpus_args
|
|
7
8
|
from lightning_sdk.cli.utils.richt_print import studio_name_link
|
|
8
9
|
from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
|
|
9
10
|
from lightning_sdk.cli.utils.studio_selection import StudiosMenu
|
|
@@ -32,8 +33,7 @@ from lightning_sdk.studio import VM, Studio
|
|
|
32
33
|
@click.option(
|
|
33
34
|
"--cloud-provider",
|
|
34
35
|
help=(
|
|
35
|
-
"The cloud provider to start the studio on. Defaults to teamspace default. "
|
|
36
|
-
"Only used if --create is specified."
|
|
36
|
+
"The cloud provider to start the studio on. Defaults to teamspace default. Only used if --create is specified."
|
|
37
37
|
),
|
|
38
38
|
type=click.Choice(m.name for m in list(CloudProvider)),
|
|
39
39
|
)
|
|
@@ -42,11 +42,17 @@ from lightning_sdk.studio import VM, Studio
|
|
|
42
42
|
help="The cloud account to start the studio on. Defaults to teamspace default. Only used if --create is specified.",
|
|
43
43
|
type=click.STRING,
|
|
44
44
|
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--gpus",
|
|
47
|
+
help="The number and type of GPUs to start the studio on (format: TYPE:COUNT, e.g. L4:4)",
|
|
48
|
+
type=click.STRING,
|
|
49
|
+
)
|
|
45
50
|
def start_studio(
|
|
46
51
|
name: Optional[str] = None,
|
|
47
52
|
teamspace: Optional[str] = None,
|
|
48
53
|
create: bool = False,
|
|
49
54
|
machine: str = "CPU",
|
|
55
|
+
gpus: Optional[str] = None,
|
|
50
56
|
interruptible: bool = False,
|
|
51
57
|
cloud_provider: Optional[str] = None,
|
|
52
58
|
cloud_account: Optional[str] = None,
|
|
@@ -62,6 +68,7 @@ def start_studio(
|
|
|
62
68
|
teamspace=teamspace,
|
|
63
69
|
create=create,
|
|
64
70
|
machine=machine,
|
|
71
|
+
gpus=gpus,
|
|
65
72
|
interruptible=interruptible,
|
|
66
73
|
cloud_provider=cloud_provider,
|
|
67
74
|
cloud_account=cloud_account,
|
|
@@ -74,6 +81,7 @@ def start_impl(
|
|
|
74
81
|
teamspace: Optional[str],
|
|
75
82
|
create: bool,
|
|
76
83
|
machine: str,
|
|
84
|
+
gpus: Optional[str],
|
|
77
85
|
interruptible: bool,
|
|
78
86
|
cloud_provider: Optional[str],
|
|
79
87
|
cloud_account: Optional[str],
|
|
@@ -98,6 +106,8 @@ def start_impl(
|
|
|
98
106
|
cloud_account=cloud_account,
|
|
99
107
|
)
|
|
100
108
|
|
|
109
|
+
machine = handle_machine_and_gpus_args(machine, gpus)
|
|
110
|
+
|
|
101
111
|
save_studio_to_config(studio)
|
|
102
112
|
|
|
103
113
|
Studio.show_progress = True
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from lightning_sdk.base_studio import BaseStudio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_base_studio_id(studio_type: Optional[str]) -> Optional[str]:
|
|
7
|
+
base_studios = BaseStudio()
|
|
8
|
+
base_studios = base_studios.list()
|
|
9
|
+
template_id = None
|
|
10
|
+
|
|
11
|
+
if base_studios and len(base_studios):
|
|
12
|
+
# if not specified by user, use the first existing template studio
|
|
13
|
+
template_id = base_studios[0].id
|
|
14
|
+
# else, try to match the provided studio_type to base studio name
|
|
15
|
+
if studio_type:
|
|
16
|
+
normalized_studio_type = studio_type.lower().replace(" ", "-")
|
|
17
|
+
match = next(
|
|
18
|
+
(s for s in base_studios if s.name.lower().replace(" ", "-") == normalized_studio_type),
|
|
19
|
+
None,
|
|
20
|
+
)
|
|
21
|
+
if match:
|
|
22
|
+
template_id = match.id
|
|
23
|
+
|
|
24
|
+
return template_id
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Dict, Optional, Set
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from lightning_sdk.machine import DEFAULT_MACHINE, Machine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _split_gpus_spec(gpus: str) -> tuple[str, int]:
|
|
9
|
+
machine_name, machine_val = gpus.split(":", 1)
|
|
10
|
+
machine_name = machine_name.strip()
|
|
11
|
+
machine_val = machine_val.strip()
|
|
12
|
+
|
|
13
|
+
if not machine_val.isdigit() or int(machine_val) <= 0:
|
|
14
|
+
raise ValueError(f"Invalid GPU count '{machine_val}'. Must be a positive integer.")
|
|
15
|
+
|
|
16
|
+
machine_num = int(machine_val)
|
|
17
|
+
return machine_name, machine_num
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _construct_available_gpus(machine_options: Dict[str, str]) -> Set[str]:
|
|
21
|
+
# returns available gpus:count
|
|
22
|
+
available_gpus = set()
|
|
23
|
+
for v in machine_options.values():
|
|
24
|
+
if "_X_" in v:
|
|
25
|
+
gpu_type_num = v.replace("_X_", ":")
|
|
26
|
+
available_gpus.add(gpu_type_num)
|
|
27
|
+
else:
|
|
28
|
+
available_gpus.add(v)
|
|
29
|
+
return available_gpus
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_machine_from_gpus(gpus: str) -> Machine:
|
|
33
|
+
machine_name = gpus
|
|
34
|
+
machine_num = 1
|
|
35
|
+
|
|
36
|
+
if ":" in gpus:
|
|
37
|
+
machine_name, machine_num = _split_gpus_spec(gpus)
|
|
38
|
+
|
|
39
|
+
machine_options = {
|
|
40
|
+
m.name.lower(): m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if machine_num == 1:
|
|
44
|
+
# e.g. gpus=L4 or gpus=L4:1
|
|
45
|
+
gpu_key = machine_name.lower()
|
|
46
|
+
try:
|
|
47
|
+
return machine_options[gpu_key]
|
|
48
|
+
except KeyError:
|
|
49
|
+
available = ", ".join(_construct_available_gpus(machine_options))
|
|
50
|
+
raise ValueError(f"Invalid GPU type '{machine_name}'. Available options: {available}") from None
|
|
51
|
+
|
|
52
|
+
# Else: e.g. gpus=L4:4
|
|
53
|
+
gpu_key = f"{machine_name.lower()}_x_{machine_num}"
|
|
54
|
+
try:
|
|
55
|
+
return machine_options[gpu_key]
|
|
56
|
+
except KeyError:
|
|
57
|
+
available = ", ".join(_construct_available_gpus(machine_options))
|
|
58
|
+
raise ValueError(f"Invalid GPU configuration '{gpus}'. Available options: {available}") from None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def handle_machine_and_gpus_args(machine: Optional[str], gpus: Optional[str]) -> str:
|
|
62
|
+
if machine and gpus:
|
|
63
|
+
raise click.UsageError("Options --machine and --gpus are mutually exclusive. Provide only one.")
|
|
64
|
+
elif gpus:
|
|
65
|
+
machine = _get_machine_from_gpus(gpus.strip())
|
|
66
|
+
elif not machine:
|
|
67
|
+
machine = DEFAULT_MACHINE
|
|
68
|
+
|
|
69
|
+
return machine
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from time import time
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Optional, Type
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Group
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from lightning_sdk.cli.utils import rich_to_str
|
|
16
|
+
from lightning_sdk.constants import _LIGHTNING_DEBUG
|
|
17
|
+
from lightning_sdk.lightning_cloud.openapi.models.v1_create_sdk_command_history_request import (
|
|
18
|
+
V1CreateSDKCommandHistoryRequest,
|
|
19
|
+
)
|
|
20
|
+
from lightning_sdk.lightning_cloud.openapi.models.v1_sdk_command_history_severity import V1SDKCommandHistorySeverity
|
|
21
|
+
from lightning_sdk.lightning_cloud.openapi.models.v1_sdk_command_history_type import V1SDKCommandHistoryType
|
|
22
|
+
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _log_command(message: str = "", duration: int = 0, error: Optional[str] = None) -> None:
|
|
26
|
+
original_command = " ".join(shlex.quote(arg) for arg in sys.argv)
|
|
27
|
+
client = LightningClient(retry=False, max_tries=0)
|
|
28
|
+
|
|
29
|
+
body = V1CreateSDKCommandHistoryRequest(
|
|
30
|
+
command=original_command,
|
|
31
|
+
duration=duration,
|
|
32
|
+
message=message,
|
|
33
|
+
project_id=None,
|
|
34
|
+
severity=V1SDKCommandHistorySeverity.INFO,
|
|
35
|
+
type=V1SDKCommandHistoryType.CLI,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if error:
|
|
39
|
+
body.severity = V1SDKCommandHistorySeverity.WARNING if error == "0" else V1SDKCommandHistorySeverity.ERROR
|
|
40
|
+
body.message = body.message + f" | Error: {error}"
|
|
41
|
+
|
|
42
|
+
# limit characters
|
|
43
|
+
body.message = body.message[:1000]
|
|
44
|
+
|
|
45
|
+
with suppress(Exception):
|
|
46
|
+
client.s_dk_command_history_service_create_sdk_command_history(body=body)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _notify_exception(exception_type: Type[BaseException], value: BaseException, tb: TracebackType) -> None:
|
|
50
|
+
"""CLI won't show tracebacks, just print the exception message."""
|
|
51
|
+
message = str(value.args[0]) if value.args else str(value) or "An unknown error occurred"
|
|
52
|
+
|
|
53
|
+
error_text = Text()
|
|
54
|
+
error_text.append(f"{exception_type.__name__}: ", style="bold red")
|
|
55
|
+
error_text.append(message, style="white")
|
|
56
|
+
|
|
57
|
+
renderables = [error_text]
|
|
58
|
+
|
|
59
|
+
if _LIGHTNING_DEBUG:
|
|
60
|
+
tb_text = "".join(traceback.format_exception(exception_type, value, tb))
|
|
61
|
+
renderables.append(Text("\n\nFull traceback:\n", style="bold yellow"))
|
|
62
|
+
renderables.append(Syntax(tb_text, "python", theme="monokai light", line_numbers=False, word_wrap=True))
|
|
63
|
+
else:
|
|
64
|
+
renderables.append(Text("\n\n🐞 To view the full traceback, set: LIGHTNING_DEBUG=1"))
|
|
65
|
+
|
|
66
|
+
renderables.append(Text("\n📘 Need help? Run: lightning <command> --help", style="cyan"))
|
|
67
|
+
|
|
68
|
+
text = rich_to_str(Panel(Group(*renderables), title="⚡ Lightning CLI Error", border_style="red"))
|
|
69
|
+
click.echo(text, color=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def logging_excepthook(exception_type: Type[BaseException], value: BaseException, tb: TracebackType) -> None:
|
|
73
|
+
try:
|
|
74
|
+
tb_str = "".join(traceback.format_exception(exception_type, value, tb))
|
|
75
|
+
ctx = click.get_current_context(silent=True)
|
|
76
|
+
command_context = ctx.command_path if ctx else "outside_command_context"
|
|
77
|
+
|
|
78
|
+
message = (
|
|
79
|
+
f"Command: {command_context} | Type: {exception_type.__name__!s} | Value: {value!s} | Traceback: {tb_str}"
|
|
80
|
+
)
|
|
81
|
+
_log_command(message=message)
|
|
82
|
+
finally:
|
|
83
|
+
_notify_exception(exception_type, value, tb)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CommandLoggingGroup(click.Group):
|
|
87
|
+
def _format_ctx(self, ctx: click.Context) -> str:
|
|
88
|
+
parts = []
|
|
89
|
+
for k, v in ctx.params.items():
|
|
90
|
+
if v is True:
|
|
91
|
+
parts.append(f"--{k}")
|
|
92
|
+
elif v is False or v is None:
|
|
93
|
+
continue
|
|
94
|
+
else:
|
|
95
|
+
parts.append(f"--{k} {v}")
|
|
96
|
+
params = " ".join(parts)
|
|
97
|
+
args = " ".join(ctx.args)
|
|
98
|
+
return (
|
|
99
|
+
f"""Commands: {ctx.command_path} | Subcommand: {ctx.invoked_subcommand} | Params: {params} | Args:{args}"""
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def invoke(self, ctx: click.Context) -> any:
|
|
103
|
+
"""Overrides the default invoke to wrap command execution with tracking."""
|
|
104
|
+
start_time = time()
|
|
105
|
+
error_message = None
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
return super().invoke(ctx)
|
|
109
|
+
except click.ClickException as e:
|
|
110
|
+
error_message = str(e)
|
|
111
|
+
e.show()
|
|
112
|
+
ctx.exit(e.exit_code)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
error_message = str(e)
|
|
115
|
+
raise
|
|
116
|
+
finally:
|
|
117
|
+
_log_command(
|
|
118
|
+
message=self._format_ctx(ctx),
|
|
119
|
+
duration=int(time() - start_time),
|
|
120
|
+
error=error_message,
|
|
121
|
+
)
|
lightning_sdk/constants.py
CHANGED
lightning_sdk/helpers.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import functools
|
|
2
1
|
import importlib
|
|
3
2
|
import os
|
|
4
3
|
import sys
|
|
@@ -10,48 +9,68 @@ import tqdm
|
|
|
10
9
|
import tqdm.std
|
|
11
10
|
from packaging import version as packaging_version
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
from lightning_sdk.constants import _LIGHTNING_DISABLE_VERSION_CHECK
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"""Check PyPI for newer versions of ``lightning-sdk``.
|
|
19
|
-
|
|
20
|
-
Returning the newest version if different from the current or ``None`` otherwise.
|
|
15
|
+
class VersionChecker:
|
|
16
|
+
"""Handles version checking and upgrade prompts for lightning-sdk.
|
|
21
17
|
|
|
18
|
+
This class ensures that version check warnings are only shown once per session,
|
|
19
|
+
preventing duplicate warnings in multithreaded scenarios.
|
|
22
20
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
|
|
22
|
+
def __init__(self, package_name: str = "lightning-sdk") -> None:
|
|
23
|
+
self.package_name = package_name
|
|
24
|
+
self._warning_shown = False
|
|
25
|
+
self._cached_version: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
def _get_newer_version(self, curr_version: str) -> Optional[str]:
|
|
28
|
+
"""Check PyPI for newer versions of ``lightning-sdk``.
|
|
29
|
+
|
|
30
|
+
Returning the newest version if different from the current or ``None`` otherwise.
|
|
31
|
+
"""
|
|
32
|
+
if self._cached_version is not None:
|
|
33
|
+
return self._cached_version
|
|
34
|
+
|
|
35
|
+
if _LIGHTNING_DISABLE_VERSION_CHECK == 1 or packaging_version.parse(curr_version).is_prerelease:
|
|
36
|
+
self._cached_version = None
|
|
31
37
|
return None
|
|
32
|
-
latest_version = response_json["info"]["version"]
|
|
33
|
-
parsed_version = packaging_version.parse(latest_version)
|
|
34
|
-
is_invalid = response_json["info"]["yanked"] or parsed_version.is_devrelease or parsed_version.is_prerelease
|
|
35
|
-
return None if curr_version == latest_version or is_invalid else latest_version
|
|
36
|
-
except requests.exceptions.RequestException:
|
|
37
|
-
return None
|
|
38
38
|
|
|
39
|
+
try:
|
|
40
|
+
response = requests.get(f"https://pypi.org/pypi/{self.package_name}/json")
|
|
41
|
+
response_json = response.json()
|
|
42
|
+
releases = response_json["releases"]
|
|
43
|
+
if curr_version not in releases:
|
|
44
|
+
# Always return None if not installed from PyPI (e.g. dev versions)
|
|
45
|
+
self._cached_version = None
|
|
46
|
+
return None
|
|
47
|
+
latest_version = response_json["info"]["version"]
|
|
48
|
+
parsed_version = packaging_version.parse(latest_version)
|
|
49
|
+
is_invalid = response_json["info"]["yanked"] or parsed_version.is_devrelease or parsed_version.is_prerelease
|
|
50
|
+
self._cached_version = None if curr_version == latest_version or is_invalid else latest_version
|
|
51
|
+
return self._cached_version
|
|
52
|
+
except requests.exceptions.RequestException:
|
|
53
|
+
self._cached_version = None
|
|
54
|
+
return None
|
|
39
55
|
|
|
40
|
-
def
|
|
41
|
-
|
|
56
|
+
def check_and_prompt_upgrade(self, curr_version: str) -> None:
|
|
57
|
+
"""Checks that the current version of ``lightning-sdk`` is the latest on PyPI.
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
If not, warn the user to upgrade ``lightning-sdk``.
|
|
60
|
+
Tracks if the warning has already been shown in this session to avoid duplicate warnings.
|
|
61
|
+
"""
|
|
62
|
+
if self._warning_shown:
|
|
63
|
+
return
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
65
|
+
new_version = self._get_newer_version(curr_version)
|
|
66
|
+
if new_version:
|
|
67
|
+
warnings.warn(
|
|
68
|
+
f"A newer version of {self.package_name} is available ({new_version}). "
|
|
69
|
+
f"Please consider upgrading with `pip install -U {self.package_name}`. "
|
|
70
|
+
"Not all platform functionality can be guaranteed to work with the current version.",
|
|
71
|
+
UserWarning,
|
|
72
|
+
)
|
|
73
|
+
self._warning_shown = True
|
|
55
74
|
|
|
56
75
|
|
|
57
76
|
def _set_tqdm_envvars_noninteractive() -> None:
|