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
lightning_sdk/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@ from lightning_sdk.agents import Agent
|
|
|
2
2
|
from lightning_sdk.ai_hub import AIHub
|
|
3
3
|
from lightning_sdk.constants import __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__ # noqa: F401
|
|
4
4
|
from lightning_sdk.deployment import Deployment
|
|
5
|
-
from lightning_sdk.helpers import
|
|
5
|
+
from lightning_sdk.helpers import VersionChecker, _set_tqdm_envvars_noninteractive
|
|
6
6
|
from lightning_sdk.job import Job
|
|
7
7
|
from lightning_sdk.machine import CloudProvider, Machine
|
|
8
8
|
from lightning_sdk.mmt import MMT
|
|
@@ -35,6 +35,9 @@ __all__ = [
|
|
|
35
35
|
"VM",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
-
__version__ = "2025.10.
|
|
39
|
-
|
|
38
|
+
__version__ = "2025.10.23"
|
|
39
|
+
|
|
40
|
+
_version_checker = VersionChecker()
|
|
41
|
+
_version_checker.check_and_prompt_upgrade(__version__)
|
|
42
|
+
|
|
40
43
|
_set_tqdm_envvars_noninteractive()
|
|
@@ -15,24 +15,28 @@ class BaseStudioApi:
|
|
|
15
15
|
def __init__(self) -> None:
|
|
16
16
|
self._client = LightningClient(retry=False, max_tries=0)
|
|
17
17
|
|
|
18
|
-
def get_base_studio(self, base_studio_id: str, org_id: str) -> V1CloudSpaceEnvironmentTemplate:
|
|
18
|
+
def get_base_studio(self, base_studio_id: str, org_id: Optional[str] = None) -> V1CloudSpaceEnvironmentTemplate:
|
|
19
19
|
"""Retrieve the base studio by its ID."""
|
|
20
20
|
try:
|
|
21
21
|
return self._client.cloud_space_environment_template_service_get_cloud_space_environment_template(
|
|
22
|
-
base_studio_id, org_id=org_id
|
|
22
|
+
base_studio_id, org_id=org_id or ""
|
|
23
23
|
)
|
|
24
24
|
except ValueError as e:
|
|
25
25
|
raise ValueError(f"Base studio {base_studio_id} does not exist") from e
|
|
26
26
|
|
|
27
|
-
def get_all_base_studios(self, org_id: str
|
|
27
|
+
def get_all_base_studios(self, org_id: Optional[str]) -> V1ListCloudSpaceEnvironmentTemplatesResponse:
|
|
28
28
|
"""Retrieve all base studios for a given organization."""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
org_id=org_id
|
|
32
|
-
)
|
|
33
|
-
return self._client.cloud_space_environment_template_service_list_cloud_space_environment_templates(
|
|
34
|
-
org_id=org_id
|
|
29
|
+
result = self._client.cloud_space_environment_template_service_list_managed_cloud_space_environment_templates(
|
|
30
|
+
org_id=org_id or ""
|
|
35
31
|
)
|
|
32
|
+
if org_id is not None:
|
|
33
|
+
org_templates = (
|
|
34
|
+
self._client.cloud_space_environment_template_service_list_cloud_space_environment_templates(
|
|
35
|
+
org_id=org_id
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
result.templates = result.templates + org_templates.templates
|
|
39
|
+
return result
|
|
36
40
|
|
|
37
41
|
def update_base_studio(
|
|
38
42
|
self,
|
lightning_sdk/api/job_api.py
CHANGED
|
@@ -250,6 +250,7 @@ class JobApiV2:
|
|
|
250
250
|
artifacts_local: Optional[str], # deprecated in favor of path_mappings
|
|
251
251
|
artifacts_remote: Optional[str], # deprecated in favor of path_mappings
|
|
252
252
|
max_runtime: Optional[int] = None,
|
|
253
|
+
reuse_snapshot: bool = True,
|
|
253
254
|
) -> V1Job:
|
|
254
255
|
body = self._create_job_body(
|
|
255
256
|
name=name,
|
|
@@ -267,6 +268,7 @@ class JobApiV2:
|
|
|
267
268
|
artifacts_local=artifacts_local,
|
|
268
269
|
artifacts_remote=artifacts_remote,
|
|
269
270
|
max_runtime=max_runtime,
|
|
271
|
+
reuse_snapshot=reuse_snapshot,
|
|
270
272
|
)
|
|
271
273
|
|
|
272
274
|
job: V1Job = self._client.jobs_service_create_job(project_id=teamspace_id, body=body)
|
|
@@ -288,6 +290,7 @@ class JobApiV2:
|
|
|
288
290
|
path_mappings: Optional[Dict[str, str]],
|
|
289
291
|
artifacts_local: Optional[str], # deprecated in favor of path_mappings
|
|
290
292
|
artifacts_remote: Optional[str], # deprecated in favor of path_mappings)
|
|
293
|
+
reuse_snapshot: bool,
|
|
291
294
|
max_runtime: Optional[int] = None,
|
|
292
295
|
machine_image_version: Optional[str] = None,
|
|
293
296
|
) -> ProjectIdJobsBody:
|
|
@@ -298,7 +301,7 @@ class JobApiV2:
|
|
|
298
301
|
|
|
299
302
|
instance_name = _machine_to_compute_name(machine)
|
|
300
303
|
|
|
301
|
-
run_id = __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__[studio_id] if studio_id is not None else ""
|
|
304
|
+
run_id = __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__[studio_id] if (studio_id is not None and reuse_snapshot) else ""
|
|
302
305
|
|
|
303
306
|
path_mappings_list = resolve_path_mappings(
|
|
304
307
|
mappings=path_mappings or {},
|
lightning_sdk/api/license_api.py
CHANGED
|
@@ -1,70 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from lightning_sdk.lightning_cloud import env
|
|
6
|
-
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
7
|
-
|
|
8
|
-
LICENSE_CODE = os.environ.get("LICENSE_CODE", "d9s79g79ss")
|
|
9
|
-
# https://lightning.ai/home?settings=licenses
|
|
10
|
-
LICENSE_SIGNING_URL = f"{env.LIGHTNING_CLOUD_URL}?settings=licenses"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def generate_url_user_settings(name: str, redirect_to: str = LICENSE_SIGNING_URL) -> str:
|
|
14
|
-
params = urlencode({"redirectTo": redirect_to, "okbhrt": LICENSE_CODE, "licenseName": name})
|
|
15
|
-
return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
|
|
1
|
+
from lightning_sdk.api.utils import _get_cloud_url as _cloud_url
|
|
2
|
+
from lightning_sdk.lightning_cloud.login import Auth
|
|
3
|
+
from lightning_sdk.lightning_cloud.openapi import LicenseKeyValidateBody, ProductLicenseServiceApi
|
|
16
4
|
|
|
17
5
|
|
|
18
6
|
class LicenseApi:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self._client_public = LightningClient(retry=False, max_tries=0, with_auth=False)
|
|
26
|
-
return self._client_public
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def client_authenticated(self) -> LightningClient:
|
|
30
|
-
if not self._client_authenticated:
|
|
31
|
-
self._client_authenticated = LightningClient(retry=True, max_tries=3, with_auth=True)
|
|
32
|
-
return self._client_authenticated
|
|
7
|
+
def __init__(self, login_token: str) -> None:
|
|
8
|
+
self._cloud_url = _cloud_url()
|
|
9
|
+
self._auth = Auth()
|
|
10
|
+
self._auth.token_login(login_token, save_token=True)
|
|
11
|
+
self._client = self._auth.create_api_client()
|
|
12
|
+
self._api = ProductLicenseServiceApi(self._client)
|
|
33
13
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
license_key: str,
|
|
37
|
-
product_name: str,
|
|
38
|
-
product_version: Optional[str] = None,
|
|
39
|
-
product_type: str = "package",
|
|
40
|
-
) -> bool:
|
|
41
|
-
"""Check if the license key is valid.
|
|
14
|
+
def validate_license(self, license_key: str, product_id: str) -> bool:
|
|
15
|
+
"""Validate a license key for a specific product.
|
|
42
16
|
|
|
43
17
|
Args:
|
|
44
|
-
license_key: The license key to
|
|
45
|
-
|
|
46
|
-
product_version: The version of the product.
|
|
47
|
-
product_type: The type of the product. Default is "package".
|
|
18
|
+
license_key: The license key to validate
|
|
19
|
+
product_id: The product ID
|
|
48
20
|
|
|
49
21
|
Returns:
|
|
50
|
-
True if
|
|
51
|
-
"""
|
|
52
|
-
response = self.client_public.product_license_service_validate_product_license(
|
|
53
|
-
license_key=license_key,
|
|
54
|
-
product_name=product_name,
|
|
55
|
-
product_version=product_version,
|
|
56
|
-
product_type=product_type,
|
|
57
|
-
)
|
|
58
|
-
return response.valid
|
|
22
|
+
bool: True if license is valid, False otherwise
|
|
59
23
|
|
|
60
|
-
|
|
61
|
-
|
|
24
|
+
Raises:
|
|
25
|
+
Exception: If license validation fails
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
response = self._api.product_license_service_validate_license(
|
|
29
|
+
body=LicenseKeyValidateBody(product_id=product_id), license_key=license_key
|
|
30
|
+
)
|
|
31
|
+
return response.is_valid
|
|
32
|
+
except Exception:
|
|
33
|
+
raise InvalidLicenseError(f"Invalid license key {license_key} for product {product_id}") from None
|
|
62
34
|
|
|
63
|
-
Args:
|
|
64
|
-
user_id: The ID of the user.
|
|
65
35
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"""
|
|
69
|
-
response = self.client_authenticated.product_license_service_list_user_licenses(user_id=user_id)
|
|
70
|
-
return response.licenses
|
|
36
|
+
class InvalidLicenseError(Exception):
|
|
37
|
+
pass
|
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -589,7 +589,12 @@ class StudioApi:
|
|
|
589
589
|
)
|
|
590
590
|
|
|
591
591
|
def duplicate_studio(
|
|
592
|
-
self,
|
|
592
|
+
self,
|
|
593
|
+
studio_id: str,
|
|
594
|
+
teamspace_id: str,
|
|
595
|
+
target_teamspace_id: str,
|
|
596
|
+
machine: Machine = Machine.CPU,
|
|
597
|
+
new_name: Optional[str] = None,
|
|
593
598
|
) -> Dict[str, Any]:
|
|
594
599
|
"""Duplicates the given Studio from a given Teamspace into a given target Teamspace."""
|
|
595
600
|
target_teamspace = self._client.projects_service_get_project(target_teamspace_id)
|
|
@@ -604,7 +609,7 @@ class StudioApi:
|
|
|
604
609
|
init_kwargs["org"] = OrgApi()._get_org_by_id(target_teamspace.owner_id).name
|
|
605
610
|
|
|
606
611
|
new_cloudspace = self._client.cloud_space_service_fork_cloud_space(
|
|
607
|
-
IdForkBody1(target_project_id=target_teamspace_id), project_id=teamspace_id, id=studio_id
|
|
612
|
+
IdForkBody1(target_project_id=target_teamspace_id, new_name=new_name), project_id=teamspace_id, id=studio_id
|
|
608
613
|
)
|
|
609
614
|
|
|
610
615
|
while self.get_studio_by_id(new_cloudspace.id, target_teamspace_id).state != V1CloudSpaceState.READY:
|
lightning_sdk/base_studio.py
CHANGED
|
@@ -3,11 +3,11 @@ from typing import List, Optional, Union
|
|
|
3
3
|
|
|
4
4
|
from lightning_sdk.api.base_studio_api import BaseStudioApi
|
|
5
5
|
from lightning_sdk.api.user_api import UserApi
|
|
6
|
-
from lightning_sdk.lightning_cloud import login
|
|
7
6
|
from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_environment_type import V1CloudSpaceEnvironmentType
|
|
8
7
|
from lightning_sdk.organization import Organization
|
|
8
|
+
from lightning_sdk.teamspace import Teamspace
|
|
9
9
|
from lightning_sdk.user import User
|
|
10
|
-
from lightning_sdk.utils.resolve import
|
|
10
|
+
from lightning_sdk.utils.resolve import _resolve_teamspace
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -24,6 +24,7 @@ class BaseStudio:
|
|
|
24
24
|
def __init__(
|
|
25
25
|
self,
|
|
26
26
|
name: Optional[str] = None,
|
|
27
|
+
teamspace: Optional[Union[str, Teamspace]] = None,
|
|
27
28
|
org: Optional[Union[str, Organization]] = None,
|
|
28
29
|
user: Optional[Union[str, User]] = None,
|
|
29
30
|
) -> None:
|
|
@@ -38,26 +39,35 @@ class BaseStudio:
|
|
|
38
39
|
Raises:
|
|
39
40
|
ConnectionError: If there is an issue with the authentication process.
|
|
40
41
|
"""
|
|
41
|
-
self.
|
|
42
|
-
self._user = None
|
|
42
|
+
self._teamspace = None
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self._user = User(name=UserApi()._get_user_by_id(self._auth.user_id).username)
|
|
48
|
-
except ConnectionError as e:
|
|
49
|
-
raise e
|
|
44
|
+
_teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
|
|
45
|
+
if _teamspace is None:
|
|
46
|
+
raise ValueError("Couldn't resolve teamspace from the provided name, org, or user")
|
|
50
47
|
|
|
51
|
-
self.
|
|
52
|
-
|
|
48
|
+
self._teamspace = _teamspace
|
|
49
|
+
|
|
50
|
+
# self._auth = login.Auth()
|
|
51
|
+
# self._user = None
|
|
52
|
+
|
|
53
|
+
# try:
|
|
54
|
+
# self._auth.authenticate()
|
|
55
|
+
# if user is None:
|
|
56
|
+
# self._user = User(name=UserApi()._get_user_by_id(self._auth.user_id).username)
|
|
57
|
+
# except ConnectionError as e:
|
|
58
|
+
# raise e
|
|
59
|
+
|
|
60
|
+
# self._user = _resolve_user(self._user or user)
|
|
61
|
+
# self._org = _resolve_org(org)
|
|
53
62
|
|
|
54
63
|
self._base_studio_api = BaseStudioApi()
|
|
55
64
|
|
|
56
65
|
if name is not None:
|
|
57
|
-
|
|
66
|
+
org_id = self._teamspace._org.id if self._teamspace._org is not None else None
|
|
67
|
+
base_studio = self._base_studio_api.get_base_studio(name, org_id)
|
|
58
68
|
|
|
59
69
|
if base_studio is None:
|
|
60
|
-
raise ValueError(f"Base studio with name {name} does not exist
|
|
70
|
+
raise ValueError(f"Base studio with name {name} does not exist")
|
|
61
71
|
self._base_studio = base_studio
|
|
62
72
|
|
|
63
73
|
def update(
|
|
@@ -70,9 +80,11 @@ class BaseStudio:
|
|
|
70
80
|
machine_image_version: Optional[str] = None,
|
|
71
81
|
setup_script_text: Optional[str] = None,
|
|
72
82
|
) -> None:
|
|
83
|
+
org_id = self._teamspace._org.id if self._teamspace._org is not None else None
|
|
84
|
+
# TODO: if not in an org, can't update them
|
|
73
85
|
self._base_studio = self._base_studio_api.update_base_studio(
|
|
74
86
|
self._base_studio.id,
|
|
75
|
-
|
|
87
|
+
org_id,
|
|
76
88
|
name=name,
|
|
77
89
|
allowed_machines=allowed_machines,
|
|
78
90
|
default_machine=default_machine,
|
|
@@ -82,7 +94,7 @@ class BaseStudio:
|
|
|
82
94
|
disabled=disabled,
|
|
83
95
|
)
|
|
84
96
|
|
|
85
|
-
def list(self,
|
|
97
|
+
def list(self, include_disabled: bool = False) -> List[BaseStudioInfo]:
|
|
86
98
|
"""List all base studios in the organization.
|
|
87
99
|
|
|
88
100
|
Args:
|
|
@@ -92,7 +104,8 @@ class BaseStudio:
|
|
|
92
104
|
Returns:
|
|
93
105
|
List[BaseStudioInfo]: A list of base studio templates.
|
|
94
106
|
"""
|
|
95
|
-
|
|
107
|
+
org_id = self._teamspace._org.id if self._teamspace._org is not None else None
|
|
108
|
+
templates = self._base_studio_api.get_all_base_studios(org_id).templates
|
|
96
109
|
|
|
97
110
|
return [
|
|
98
111
|
BaseStudioInfo(
|
|
@@ -21,9 +21,7 @@ def list_base_studios(include_disabled: bool) -> None:
|
|
|
21
21
|
|
|
22
22
|
def list_impl(include_disabled: bool) -> None:
|
|
23
23
|
base_studio_cls = BaseStudio()
|
|
24
|
-
base_studios = base_studio_cls.list(include_disabled=include_disabled)
|
|
25
|
-
managed=False, include_disabled=include_disabled
|
|
26
|
-
)
|
|
24
|
+
base_studios = base_studio_cls.list(include_disabled=include_disabled)
|
|
27
25
|
|
|
28
26
|
table = Table(
|
|
29
27
|
pad_edge=True,
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -2,15 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
-
import traceback
|
|
6
|
-
from types import TracebackType
|
|
7
|
-
from typing import Type
|
|
8
5
|
|
|
9
6
|
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
7
|
|
|
15
8
|
from lightning_sdk import __version__
|
|
16
9
|
from lightning_sdk.api.studio_api import _cloud_url
|
|
@@ -20,42 +13,24 @@ from lightning_sdk.cli.groups import (
|
|
|
20
13
|
base_studio,
|
|
21
14
|
config,
|
|
22
15
|
# job,
|
|
16
|
+
license,
|
|
23
17
|
# mmt,
|
|
24
18
|
studio,
|
|
25
19
|
vm,
|
|
26
20
|
)
|
|
27
|
-
from lightning_sdk.cli.utils import CustomHelpFormatter
|
|
28
|
-
from lightning_sdk.
|
|
21
|
+
from lightning_sdk.cli.utils import CustomHelpFormatter
|
|
22
|
+
from lightning_sdk.cli.utils.logging import CommandLoggingGroup, logging_excepthook
|
|
29
23
|
from lightning_sdk.lightning_cloud.login import Auth
|
|
30
24
|
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
error_text.append(f"{exception_type.__name__}: ", style="bold red")
|
|
38
|
-
error_text.append(message, style="white")
|
|
39
|
-
|
|
40
|
-
renderables = [error_text]
|
|
41
|
-
|
|
42
|
-
if _LIGHTNING_DEBUG:
|
|
43
|
-
tb_text = "".join(traceback.format_exception(exception_type, value, tb))
|
|
44
|
-
renderables.append(Text("\n\nFull traceback:\n", style="bold yellow"))
|
|
45
|
-
renderables.append(Syntax(tb_text, "python", theme="monokai light", line_numbers=False, word_wrap=True))
|
|
46
|
-
else:
|
|
47
|
-
renderables.append(Text("\n\n🐞 To view the full traceback, set: LIGHTNING_DEBUG=1"))
|
|
48
|
-
|
|
49
|
-
renderables.append(Text("\n📘 Need help? Run: lightning <command> --help", style="cyan"))
|
|
50
|
-
|
|
51
|
-
text = rich_to_str(Panel(Group(*renderables), title="⚡ Lightning CLI Error", border_style="red"))
|
|
52
|
-
click.echo(text, color=True)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@click.group(name="lightning", help="Command line interface (CLI) to interact with/manage Lightning AI Studios.")
|
|
26
|
+
@click.group(
|
|
27
|
+
name="lightning",
|
|
28
|
+
help="Command line interface (CLI) to interact with/manage Lightning AI Studios.",
|
|
29
|
+
cls=CommandLoggingGroup,
|
|
30
|
+
)
|
|
56
31
|
@click.version_option(__version__, message="Lightning CLI version %(version)s")
|
|
57
32
|
def main_cli() -> None:
|
|
58
|
-
sys.excepthook =
|
|
33
|
+
sys.excepthook = logging_excepthook
|
|
59
34
|
|
|
60
35
|
|
|
61
36
|
main_cli.context_class.formatter_class = CustomHelpFormatter
|
|
@@ -87,6 +62,8 @@ main_cli.add_command(config)
|
|
|
87
62
|
main_cli.add_command(studio)
|
|
88
63
|
main_cli.add_command(vm)
|
|
89
64
|
main_cli.add_command(base_studio)
|
|
65
|
+
main_cli.add_command(license)
|
|
66
|
+
|
|
90
67
|
if os.environ.get("LIGHTNING_EXPERIMENTAL_CLI_ONLY", "0") != "1":
|
|
91
68
|
#### LEGACY COMMANDS ####
|
|
92
69
|
# these commands are currently supported for backwards compatibility, but will potentially be removed in the future.
|
lightning_sdk/cli/groups.py
CHANGED
|
@@ -5,6 +5,7 @@ import click
|
|
|
5
5
|
from lightning_sdk.cli.base_studio import register_commands as register_base_studio_commands
|
|
6
6
|
from lightning_sdk.cli.config import register_commands as register_config_commands
|
|
7
7
|
from lightning_sdk.cli.job import register_commands as register_job_commands
|
|
8
|
+
from lightning_sdk.cli.license import register_commands as register_license_commands
|
|
8
9
|
from lightning_sdk.cli.mmt import register_commands as register_mmt_commands
|
|
9
10
|
from lightning_sdk.cli.studio import register_commands as register_studio_commands
|
|
10
11
|
from lightning_sdk.cli.vm import register_commands as register_vm_commands
|
|
@@ -40,6 +41,11 @@ def base_studio() -> None:
|
|
|
40
41
|
"""Manage Lightning AI Base Studios."""
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
@click.group(name="license")
|
|
45
|
+
def license() -> None: # noqa: A001
|
|
46
|
+
"""Manage Lightning AI Product Licenses."""
|
|
47
|
+
|
|
48
|
+
|
|
43
49
|
# Register config commands with the main config group
|
|
44
50
|
register_job_commands(job)
|
|
45
51
|
register_mmt_commands(mmt)
|
|
@@ -47,3 +53,4 @@ register_studio_commands(studio)
|
|
|
47
53
|
register_config_commands(config)
|
|
48
54
|
register_vm_commands(vm)
|
|
49
55
|
register_base_studio_commands(base_studio)
|
|
56
|
+
register_license_commands(license)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Base Studio CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_commands(group: click.Group) -> None:
|
|
7
|
+
"""Register base studio commands with the given group."""
|
|
8
|
+
from lightning_sdk.cli.license.get import get_license
|
|
9
|
+
from lightning_sdk.cli.license.list import list_licenses
|
|
10
|
+
from lightning_sdk.cli.license.set import set_license
|
|
11
|
+
|
|
12
|
+
group.add_command(list_licenses)
|
|
13
|
+
group.add_command(get_license)
|
|
14
|
+
group.add_command(set_license)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH, Config, DefaultConfigKeys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command("get")
|
|
7
|
+
@click.argument("product_name")
|
|
8
|
+
@click.option("--config-file", help="Path to the config file")
|
|
9
|
+
def get_license(product_name: str, config_file: str = _DEFAULT_CONFIG_FILE_PATH) -> None:
|
|
10
|
+
"""Get a license key for a given product."""
|
|
11
|
+
cfg = Config(config_file)
|
|
12
|
+
license_key = cfg.get(f"{DefaultConfigKeys.license}.{product_name}")
|
|
13
|
+
if license_key:
|
|
14
|
+
# echo the license key without any additional output to make parsing simpler
|
|
15
|
+
click.echo(license_key)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""License list command."""
|
|
2
|
+
|
|
3
|
+
from typing import Mapping
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from lightning_sdk.cli.utils.richt_print import rich_to_str
|
|
9
|
+
from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH, Config, DefaultConfigKeys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command("list")
|
|
13
|
+
@click.option("--include-key", help="Print the key as well", is_flag=True)
|
|
14
|
+
@click.option("--config-file", help="Path to the config file")
|
|
15
|
+
def list_licenses(include_key: bool, config_file: str = _DEFAULT_CONFIG_FILE_PATH) -> None:
|
|
16
|
+
"""List configured licenses.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
lightning license list --include-key
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
return list_impl(include_key=include_key, config_path=config_file)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_impl(include_key: bool, config_path: str) -> None:
|
|
26
|
+
cfg = Config(config_file=config_path)
|
|
27
|
+
|
|
28
|
+
license_cfg = cfg.get_sub_config(DefaultConfigKeys.license)
|
|
29
|
+
|
|
30
|
+
if isinstance(license_cfg, Mapping):
|
|
31
|
+
table = Table(
|
|
32
|
+
pad_edge=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
table.add_column("Product")
|
|
36
|
+
table.add_column("License Key")
|
|
37
|
+
|
|
38
|
+
# sort by product_name
|
|
39
|
+
for product_name, license_key in sorted(license_cfg.items(), key=lambda x: x[0]):
|
|
40
|
+
table.add_row(product_name, license_key if include_key else "********")
|
|
41
|
+
|
|
42
|
+
click.echo(rich_to_str(table), color=True)
|
|
43
|
+
|
|
44
|
+
else:
|
|
45
|
+
click.echo("No licenses configured!")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH, Config, DefaultConfigKeys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command("set")
|
|
7
|
+
@click.argument("product_name")
|
|
8
|
+
@click.argument("license_key")
|
|
9
|
+
@click.option("--config-file", help="Path to the config file")
|
|
10
|
+
def set_license(product_name: str, license_key: str, config_file: str = _DEFAULT_CONFIG_FILE_PATH) -> None:
|
|
11
|
+
"""Set a license key for a given product."""
|
|
12
|
+
cfg = Config(config_file)
|
|
13
|
+
cfg.set(f"{DefaultConfigKeys.license}.{product_name}", license_key)
|