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.
Files changed (76) hide show
  1. lightning_sdk/__init__.py +6 -3
  2. lightning_sdk/api/base_studio_api.py +13 -9
  3. lightning_sdk/api/job_api.py +4 -1
  4. lightning_sdk/api/license_api.py +26 -59
  5. lightning_sdk/api/studio_api.py +7 -2
  6. lightning_sdk/base_studio.py +30 -17
  7. lightning_sdk/cli/base_studio/list.py +1 -3
  8. lightning_sdk/cli/entrypoint.py +11 -34
  9. lightning_sdk/cli/groups.py +7 -0
  10. lightning_sdk/cli/license/__init__.py +14 -0
  11. lightning_sdk/cli/license/get.py +15 -0
  12. lightning_sdk/cli/license/list.py +45 -0
  13. lightning_sdk/cli/license/set.py +13 -0
  14. lightning_sdk/cli/studio/connect.py +42 -92
  15. lightning_sdk/cli/studio/create.py +23 -1
  16. lightning_sdk/cli/studio/start.py +12 -2
  17. lightning_sdk/cli/utils/get_base_studio.py +24 -0
  18. lightning_sdk/cli/utils/handle_machine_and_gpus_args.py +69 -0
  19. lightning_sdk/cli/utils/logging.py +121 -0
  20. lightning_sdk/cli/utils/ssh_connection.py +1 -1
  21. lightning_sdk/constants.py +1 -0
  22. lightning_sdk/helpers.py +53 -34
  23. lightning_sdk/job/base.py +7 -0
  24. lightning_sdk/job/job.py +8 -0
  25. lightning_sdk/job/v1.py +3 -0
  26. lightning_sdk/job/v2.py +4 -0
  27. lightning_sdk/lightning_cloud/login.py +260 -10
  28. lightning_sdk/lightning_cloud/openapi/__init__.py +16 -3
  29. lightning_sdk/lightning_cloud/openapi/api/auth_service_api.py +279 -0
  30. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +117 -0
  31. lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +108 -108
  32. lightning_sdk/lightning_cloud/openapi/models/__init__.py +16 -3
  33. lightning_sdk/lightning_cloud/openapi/models/create_machine_request_represents_the_request_to_create_a_machine.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/id_fork_body1.py +27 -1
  36. lightning_sdk/lightning_cloud/openapi/models/license_key_validate_body.py +123 -0
  37. lightning_sdk/lightning_cloud/openapi/models/update1.py +27 -1
  38. lightning_sdk/lightning_cloud/openapi/models/v1_create_license_request.py +175 -0
  39. lightning_sdk/lightning_cloud/openapi/models/v1_data_connection.py +27 -1
  40. lightning_sdk/lightning_cloud/openapi/models/v1_delete_license_response.py +97 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
  43. lightning_sdk/lightning_cloud/openapi/models/v1_filesystem_metric.py +201 -0
  44. lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_transfer_estimate_response.py +29 -3
  45. lightning_sdk/lightning_cloud/openapi/models/v1_incident.py +27 -1
  46. lightning_sdk/lightning_cloud/openapi/models/v1_incident_detail.py +149 -0
  47. lightning_sdk/lightning_cloud/openapi/models/v1_incident_event.py +27 -1
  48. lightning_sdk/lightning_cloud/openapi/models/v1_license.py +227 -0
  49. lightning_sdk/lightning_cloud/openapi/models/v1_list_filesystem_metrics_response.py +123 -0
  50. lightning_sdk/lightning_cloud/openapi/models/{v1_list_product_licenses_response.py → v1_list_license_response.py} +16 -16
  51. lightning_sdk/lightning_cloud/openapi/models/v1_list_platform_notifications_response.py +123 -0
  52. lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -1
  53. lightning_sdk/lightning_cloud/openapi/models/v1_platform_notification.py +279 -0
  54. lightning_sdk/lightning_cloud/openapi/models/v1_reset_api_key_request.py +97 -0
  55. lightning_sdk/lightning_cloud/openapi/models/v1_reset_api_key_response.py +123 -0
  56. lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +53 -1
  57. lightning_sdk/lightning_cloud/openapi/models/v1_token_login_request.py +123 -0
  58. lightning_sdk/lightning_cloud/openapi/models/v1_token_login_response.py +123 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_token_owner_type.py +104 -0
  60. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +139 -191
  61. lightning_sdk/lightning_cloud/openapi/models/{v1_product_license_check_response.py → v1_validate_license_response.py} +21 -21
  62. lightning_sdk/lightning_cloud/rest_client.py +48 -45
  63. lightning_sdk/machine.py +5 -0
  64. lightning_sdk/pipeline/steps.py +1 -0
  65. lightning_sdk/studio.py +55 -13
  66. lightning_sdk/utils/config.py +18 -3
  67. lightning_sdk/utils/license.py +13 -0
  68. lightning_sdk/utils/resolve.py +6 -1
  69. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/METADATA +1 -1
  70. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/RECORD +74 -54
  71. lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +0 -435
  72. lightning_sdk/services/license.py +0 -363
  73. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/LICENSE +0 -0
  74. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/WHEEL +0 -0
  75. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.23.dist-info}/entry_points.txt +0 -0
  76. {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 _check_version_and_prompt_upgrade, _set_tqdm_envvars_noninteractive
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.14"
39
- _check_version_and_prompt_upgrade(__version__)
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, managed: bool = True) -> V1ListCloudSpaceEnvironmentTemplatesResponse:
27
+ def get_all_base_studios(self, org_id: Optional[str]) -> V1ListCloudSpaceEnvironmentTemplatesResponse:
28
28
  """Retrieve all base studios for a given organization."""
29
- if managed:
30
- return self._client.cloud_space_environment_template_service_list_managed_cloud_space_environment_templates(
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,
@@ -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 {},
@@ -1,70 +1,37 @@
1
- import os
2
- from typing import Optional
3
- from urllib.parse import urlencode
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
- _client_authenticated: LightningClient = None
20
- _client_public: LightningClient = None
21
-
22
- @property
23
- def client_public(self) -> LightningClient:
24
- if not self._client_public:
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 valid_license(
35
- self,
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 check.
45
- product_name: The name of the product.
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 the license key is valid, False otherwise.
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
- def list_user_licenses(self, user_id: str) -> list:
61
- """List all licenses for a user.
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
- Returns:
67
- A list of licenses for the user.
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
@@ -589,7 +589,12 @@ class StudioApi:
589
589
  )
590
590
 
591
591
  def duplicate_studio(
592
- self, studio_id: str, teamspace_id: str, target_teamspace_id: str, machine: Machine = Machine.CPU
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:
@@ -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 _resolve_org, _resolve_user
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._auth = login.Auth()
42
- self._user = None
42
+ self._teamspace = None
43
43
 
44
- try:
45
- self._auth.authenticate()
46
- if user is None:
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._user = _resolve_user(self._user or user)
52
- self._org = _resolve_org(org)
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
- base_studio = self._base_studio_api.get_base_studio(name, self._org.id)
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 in organization {self._org.name}")
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
- self._org.id,
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, managed: bool = True, include_disabled: bool = False) -> List[BaseStudioInfo]:
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
- templates = self._base_studio_api.get_all_base_studios(self._org.id, managed).templates
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) + base_studio_cls.list(
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,
@@ -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, rich_to_str
28
- from lightning_sdk.constants import _LIGHTNING_DEBUG
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
- def _notify_exception(exception_type: Type[BaseException], value: BaseException, tb: TracebackType) -> None:
33
- """CLI won't show tracebacks, just print the exception message."""
34
- message = str(value.args[0]) if value.args else str(value) or "An unknown error occurred"
35
-
36
- error_text = Text()
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 = _notify_exception
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.
@@ -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)