lightning-sdk 2025.9.16__py3-none-any.whl → 2025.9.29__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 (78) hide show
  1. lightning_sdk/__init__.py +4 -3
  2. lightning_sdk/api/cloud_account_api.py +12 -1
  3. lightning_sdk/api/job_api.py +12 -11
  4. lightning_sdk/api/mmt_api.py +1 -1
  5. lightning_sdk/api/studio_api.py +1 -1
  6. lightning_sdk/api/teamspace_api.py +18 -0
  7. lightning_sdk/api/user_api.py +8 -2
  8. lightning_sdk/cli/entrypoint.py +3 -1
  9. lightning_sdk/cli/groups.py +8 -1
  10. lightning_sdk/cli/legacy/entrypoint.py +1 -1
  11. lightning_sdk/cli/studio/create.py +19 -5
  12. lightning_sdk/cli/studio/delete.py +9 -5
  13. lightning_sdk/cli/studio/list.py +5 -1
  14. lightning_sdk/cli/studio/ssh.py +9 -3
  15. lightning_sdk/cli/studio/start.py +26 -3
  16. lightning_sdk/cli/studio/stop.py +7 -3
  17. lightning_sdk/cli/studio/switch.py +21 -5
  18. lightning_sdk/cli/utils/owner_selection.py +110 -0
  19. lightning_sdk/cli/utils/studio_selection.py +22 -15
  20. lightning_sdk/cli/utils/teamspace_selection.py +63 -62
  21. lightning_sdk/cli/vm/__init__.py +20 -0
  22. lightning_sdk/cli/vm/create.py +33 -0
  23. lightning_sdk/cli/vm/delete.py +25 -0
  24. lightning_sdk/cli/vm/list.py +30 -0
  25. lightning_sdk/cli/vm/ssh.py +31 -0
  26. lightning_sdk/cli/vm/start.py +60 -0
  27. lightning_sdk/cli/vm/stop.py +25 -0
  28. lightning_sdk/cli/vm/switch.py +38 -0
  29. lightning_sdk/lightning_cloud/openapi/__init__.py +20 -1
  30. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +2 -95
  31. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +24 -8
  32. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +420 -0
  33. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +121 -0
  34. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +655 -0
  35. lightning_sdk/lightning_cloud/openapi/models/__init__.py +20 -1
  36. lightning_sdk/lightning_cloud/openapi/models/create_machine_request_represents_the_request_to_create_a_machine.py +435 -0
  37. lightning_sdk/lightning_cloud/openapi/models/job_id_reportroutingtelemetry_body.py +123 -0
  38. lightning_sdk/lightning_cloud/openapi/models/project_id_storagetransfers_body.py +149 -0
  39. lightning_sdk/lightning_cloud/openapi/models/user_id_affiliatelinks_body.py +107 -3
  40. lightning_sdk/lightning_cloud/openapi/models/v1_abort_storage_transfer_response.py +97 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_assistant_session_daily_aggregated.py +27 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +2 -0
  43. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +53 -1
  45. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +1 -0
  46. lightning_sdk/lightning_cloud/openapi/models/v1_create_machine_response.py +123 -0
  47. lightning_sdk/lightning_cloud/openapi/models/v1_create_project_request.py +27 -1
  48. lightning_sdk/lightning_cloud/openapi/models/v1_delete_machine_response.py +97 -0
  49. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -1
  50. lightning_sdk/lightning_cloud/openapi/models/v1_get_machine_response.py +123 -0
  51. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
  52. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +27 -1
  53. lightning_sdk/lightning_cloud/openapi/models/v1_lightning_elastic_cluster_v1.py +97 -0
  54. lightning_sdk/lightning_cloud/openapi/models/{v1_get_model_total_usage_metrics_response.py → v1_list_machines_response.py} +37 -37
  55. lightning_sdk/lightning_cloud/openapi/models/v1_list_storage_transfers_response.py +123 -0
  56. lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +539 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +123 -0
  58. lightning_sdk/lightning_cloud/openapi/models/v1_pause_storage_transfer_response.py +97 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_request.py +123 -0
  60. lightning_sdk/lightning_cloud/openapi/models/v1_report_deployment_routing_telemetry_response.py +97 -0
  61. lightning_sdk/lightning_cloud/openapi/models/v1_resume_storage_transfer_response.py +97 -0
  62. lightning_sdk/lightning_cloud/openapi/models/v1_routing_telemetry.py +79 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_rule_resource.py +1 -0
  64. lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +149 -0
  65. lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier_type.py +105 -0
  66. lightning_sdk/lightning_cloud/openapi/models/v1_storage_transfer.py +435 -0
  67. lightning_sdk/lightning_cloud/openapi/models/v1_storage_transfer_status.py +108 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
  69. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +105 -79
  70. lightning_sdk/machine.py +16 -1
  71. lightning_sdk/studio.py +55 -11
  72. lightning_sdk/teamspace.py +65 -2
  73. {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.29.dist-info}/METADATA +1 -1
  74. {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.29.dist-info}/RECORD +78 -50
  75. {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.29.dist-info}/LICENSE +0 -0
  76. {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.29.dist-info}/WHEEL +0 -0
  77. {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.29.dist-info}/entry_points.txt +0 -0
  78. {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.29.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  import os
2
2
  from contextlib import suppress
3
- from typing import Dict, List, Optional
3
+ from typing import Dict, List, Optional, Union
4
4
 
5
5
  import click
6
6
  from simple_term_menu import TerminalMenu
7
7
 
8
8
  from lightning_sdk.cli.legacy.exceptions import StudioCliError
9
- from lightning_sdk.studio import Studio
9
+ from lightning_sdk.studio import VM, Studio
10
10
  from lightning_sdk.teamspace import Teamspace
11
11
  from lightning_sdk.utils.resolve import _get_authed_user
12
12
 
@@ -17,54 +17,58 @@ class StudiosMenu:
17
17
  It can be used to select a studio from a list of possible studios, or to resolve a studio from a name.
18
18
  """
19
19
 
20
- def __init__(self, teamspace: Teamspace) -> None:
20
+ def __init__(self, teamspace: Teamspace, vm: bool = False) -> None:
21
21
  """Initialize the StudiosMenu with a teamspace.
22
22
 
23
23
  Args:
24
24
  teamspace: The teamspace to list studios from
25
25
  """
26
26
  self.teamspace = teamspace
27
+ self.vm = vm
27
28
 
28
- def _get_studio_from_interactive_menu(self, possible_studios: Dict[str, Studio]) -> Studio:
29
+ def _get_studio_from_interactive_menu(self, possible_studios: Dict[str, Union[Studio, VM]]) -> Union[Studio, VM]:
29
30
  studio_names = sorted(possible_studios.keys())
30
- terminal_menu = self._prepare_terminal_menu_studios(studio_names)
31
+ terminal_menu = self._prepare_terminal_menu_studios(studio_names, vm=self.vm)
31
32
  terminal_menu.show()
32
33
 
33
34
  selected_name = studio_names[terminal_menu.chosen_menu_index]
34
35
  return possible_studios[selected_name]
35
36
 
36
- def _get_studio_from_name(self, studio: str, possible_studios: Dict[str, Studio]) -> Studio:
37
+ def _get_studio_from_name(self, studio: str, possible_studios: Dict[str, Union[Studio, VM]]) -> Union[Studio, VM]:
37
38
  if studio in possible_studios:
38
39
  return possible_studios[studio]
39
40
 
40
- click.echo(f"Could not find Studio {studio}, please select it from the list:")
41
+ click.echo(f"Could not find {'VM' if self.vm else 'Studio'} {studio}, please select it from the list:")
41
42
  return self._get_studio_from_interactive_menu(possible_studios)
42
43
 
43
44
  @staticmethod
44
- def _prepare_terminal_menu_studios(studio_names: List[str], title: Optional[str] = None) -> TerminalMenu:
45
+ def _prepare_terminal_menu_studios(
46
+ studio_names: List[str], title: Optional[str] = None, vm: bool = False
47
+ ) -> TerminalMenu:
45
48
  if title is None:
46
- title = "Please select a Studio out of the following:"
49
+ title = f"Please select a {'VM' if vm else 'Studio'} out of the following:"
47
50
 
48
51
  return TerminalMenu(studio_names, title=title, clear_menu_on_exit=True)
49
52
 
50
- def _get_possible_studios(self) -> Dict[str, Studio]:
53
+ def _get_possible_studios(self) -> Dict[str, Union[Studio, VM]]:
51
54
  """Get all available studios in the teamspace."""
52
55
  studios = {}
53
56
 
54
57
  user = _get_authed_user()
58
+ studios = self.teamspace.vms if self.vm else self.teamspace.studios
55
59
  for studio in self.teamspace.studios:
56
60
  if studio._studio.user_id == user.id:
57
61
  studios[studio.name] = studio
58
62
  return studios
59
63
 
60
- def __call__(self, studio: Optional[str] = None) -> Studio:
64
+ def __call__(self, studio: Optional[str] = None) -> Union[Studio, VM]:
61
65
  """Select a studio from the teamspace.
62
66
 
63
67
  Args:
64
68
  studio: Optional studio name to select. If not provided, will show interactive menu.
65
69
 
66
70
  Returns:
67
- Selected Studio object
71
+ Selected Studio/VM object
68
72
 
69
73
  Raises:
70
74
  StudioCliError: If studio selection fails
@@ -73,15 +77,18 @@ class StudiosMenu:
73
77
  # try to resolve the studio from the name, environment or config
74
78
  resolved_studio = None
75
79
 
80
+ selected_cls = VM if self.vm else Studio
81
+
76
82
  with suppress(Exception):
77
- resolved_studio = Studio(name=studio, teamspace=self.teamspace, create_ok=False)
83
+ resolved_studio = selected_cls(name=studio, teamspace=self.teamspace, create_ok=False)
78
84
 
79
85
  if resolved_studio is not None:
80
86
  return resolved_studio
81
87
 
82
88
  if os.environ.get("LIGHTNING_NON_INTERACTIVE", "0") == "1" and studio is None:
83
89
  raise ValueError(
84
- "Studio selection is not supported in non-interactive mode. Please provide a studio name."
90
+ f"{'VM' if self.vm else 'Studio'} selection is not supported in non-interactive mode. "
91
+ "Please provide a studio name."
85
92
  )
86
93
 
87
94
  click.echo(f"Listing studios in teamspace {self.teamspace.owner.name}/{self.teamspace.name}...")
@@ -101,6 +108,6 @@ class StudiosMenu:
101
108
 
102
109
  except Exception as e:
103
110
  raise StudioCliError(
104
- "Could not resolve a Studio. "
111
+ f"Could not resolve a {'VM' if self.vm else 'Studio'}. "
105
112
  "Please pass it as an argument or contact Lightning AI directly to resolve this issue."
106
113
  ) from e
@@ -1,15 +1,15 @@
1
1
  import os
2
+ from contextlib import suppress
2
3
  from typing import Dict, List, Optional
3
4
 
4
5
  import click
5
6
  from simple_term_menu import TerminalMenu
6
7
 
7
- from lightning_sdk.api import OrgApi
8
8
  from lightning_sdk.cli.legacy.exceptions import StudioCliError
9
9
  from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
10
- from lightning_sdk.teamspace import Teamspace
11
- from lightning_sdk.user import User
12
- from lightning_sdk.utils.resolve import _get_authed_user
10
+ from lightning_sdk.teamspace import Organization, Teamspace
11
+ from lightning_sdk.user import Owner, User
12
+ from lightning_sdk.utils.resolve import ApiException, _get_authed_user, _resolve_teamspace
13
13
 
14
14
 
15
15
  class TeamspacesMenu:
@@ -18,51 +18,45 @@ class TeamspacesMenu:
18
18
  It can be used to select a teamspace from a list of possible teamspaces, or to resolve a teamspace from a name.
19
19
  """
20
20
 
21
- def _get_teamspace_from_interactive_menu(self, possible_teamspaces: Dict[str, Dict[str, str]]) -> Dict[str, str]:
21
+ def __init__(self, owner: Optional[Owner] = None) -> None:
22
+ self._owner: Owner = owner
23
+
24
+ def _get_teamspace_from_interactive_menu(self, possible_teamspaces: Dict[str, str]) -> str:
22
25
  teamspace_ids = sorted(possible_teamspaces.keys())
23
- terminal_menu = self._prepare_terminal_menu_teamspaces([possible_teamspaces[k] for k in teamspace_ids])
26
+ terminal_menu = self._prepare_terminal_menu_teamspaces(
27
+ [possible_teamspaces[k] for k in teamspace_ids], self._owner
28
+ )
24
29
  terminal_menu.show()
25
30
 
26
31
  selected_id = teamspace_ids[terminal_menu.chosen_menu_index]
27
32
  return possible_teamspaces[selected_id]
28
33
 
29
- def _get_teamspace_from_name(
30
- self, teamspace: str, possible_teamspaces: Dict[str, Dict[str, str]]
31
- ) -> Dict[str, str]:
32
- try:
33
- owner, name = teamspace.split("/", maxsplit=1)
34
- except ValueError as e:
35
- raise ValueError(
36
- f"Invalid teamspace format: '{teamspace}'. "
37
- "Teamspace should be specified as '{teamspace_owner}/{teamspace_name}' "
38
- "(e.g., 'my-org/my-teamspace')."
39
- ) from e
40
-
34
+ def _get_teamspace_from_name(self, teamspace: str, possible_teamspaces: Dict[str, str]) -> str:
41
35
  for _, ts in possible_teamspaces.items():
42
- if ts["name"] == name and (ts["user"] == owner or ts["org"] == owner):
36
+ if ts == teamspace:
43
37
  return ts
44
38
 
45
- click.echo(f"Could not find Teamspace {teamspace}, please select it from the list:")
39
+ click.echo(f"Could not find Teamspace {self._owner.name}/{teamspace}, please select it from the list:")
46
40
  return self._get_teamspace_from_interactive_menu(possible_teamspaces)
47
41
 
48
42
  @staticmethod
49
43
  def _prepare_terminal_menu_teamspaces(
50
- possible_teamspaces: List[Dict[str, str]], title: Optional[str] = None
44
+ possible_teamspaces: List[str],
45
+ owner: Owner,
46
+ title: Optional[str] = None,
51
47
  ) -> TerminalMenu:
52
48
  if title is None:
53
- title = "Please select a Teamspace out of the following:"
49
+ title = f"Please select a Teamspace (owned by {owner.name}) out of the following:"
54
50
 
55
- return TerminalMenu(
56
- [f"{t['user'] or t['org']}/{t['name']}" for t in possible_teamspaces], title=title, clear_menu_on_exit=True
57
- )
51
+ return TerminalMenu(possible_teamspaces, title=title, clear_menu_on_exit=True)
58
52
 
59
53
  @staticmethod
60
- def _get_possible_teamspaces(user: User) -> Dict[str, Dict[str, str]]:
61
- org_api = OrgApi()
54
+ def _get_possible_teamspaces(user: User, owner: Owner) -> Dict[str, str]:
62
55
  user_api = user._user_api
63
56
 
64
- user_api._get_organizations_for_authed_user()
65
- memberships = user_api._get_all_teamspace_memberships(user_id=user.id)
57
+ memberships = user_api._get_all_teamspace_memberships(
58
+ user_id=user.id, org_id=owner.id if isinstance(owner, Organization) else None
59
+ )
66
60
 
67
61
  teamspaces = {}
68
62
  # get all teamspace memberships
@@ -70,56 +64,63 @@ class TeamspacesMenu:
70
64
  teamspace_id = membership.project_id
71
65
  teamspace_name = membership.name
72
66
 
73
- # get organization if necessary
74
- if membership.owner_type == "organization":
75
- org_name = org_api._get_org_by_id(membership.owner_id).name
76
- user_name = None
77
- else:
78
- org_name = None
79
-
80
- # don't do a request if not necessary
81
- if membership.owner_id == user.id:
82
- user_name = user.name
83
- else:
84
- user_name = user_api._get_user_by_id(membership.owner_id).username
85
-
86
- teamspaces[teamspace_id] = {"user": user_name, "org": org_name, "name": teamspace_name}
67
+ if membership.owner_id == owner.id:
68
+ teamspaces[teamspace_id] = teamspace_name
87
69
 
88
70
  return teamspaces
89
71
 
90
72
  def __call__(self, teamspace: Optional[str] = None) -> Teamspace:
91
- try:
92
- # try to resolve the teamspace from the name, environment or config
93
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
73
+ resolved_teamspace = None
74
+ resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
75
+ if resolved_teamspace is not None:
76
+ return resolved_teamspace
94
77
 
95
- if resolved_teamspace is not None:
96
- return resolved_teamspace
78
+ if self._owner is None:
79
+ # resolve owner if required
80
+ from lightning_sdk.cli.utils.owner_selection import OwnerMenu
97
81
 
98
- if os.environ.get("LIGHTNING_NON_INTERACTIVE", "0") == "1":
99
- raise ValueError(
100
- "Teamspace selection is not supported in non-interactive mode. "
101
- "Please provide a teamspace name in the format of 'owner/teamspace'."
102
- )
82
+ menu = OwnerMenu()
83
+ self._owner = menu()
84
+
85
+ if isinstance(self._owner, Organization):
86
+ org = self._owner
87
+ user = None
88
+ elif isinstance(self._owner, User):
89
+ org = None
90
+ user = self._owner
91
+
92
+ with suppress(ApiException, ValueError, RuntimeError):
93
+ resolved_teamspace = _resolve_teamspace(teamspace=teamspace, org=org, user=user)
103
94
 
104
- # if the teamspace is not resolved, try to get the teamspace from the interactive menu
105
- # this could mean that either no teamspace was provided or the provided teamspace is not valid
106
- user = _get_authed_user()
95
+ if resolved_teamspace is not None:
96
+ return resolved_teamspace
107
97
 
108
- possible_teamspaces = self._get_possible_teamspaces(user)
98
+ if os.environ.get("LIGHTNING_NON_INTERACTIVE", "0") == "1":
99
+ raise ValueError(
100
+ "Teamspace selection is not supported in non-interactive mode. "
101
+ "Please provide a teamspace name in the format of 'owner/teamspace'."
102
+ )
103
+
104
+ # if the teamspace is not resolved, try to get the teamspace from the interactive menu
105
+ # this could mean that either no teamspace was provided or the provided teamspace is not valid
106
+ try:
107
+ auth_user = _get_authed_user()
108
+
109
+ possible_teamspaces = self._get_possible_teamspaces(auth_user, self._owner)
109
110
  if teamspace is None:
110
- teamspace = resolve_teamspace_owner_name_format(teamspace)
111
- teamspace_dict = self._get_teamspace_from_interactive_menu(possible_teamspaces=possible_teamspaces)
111
+ teamspace_name = self._get_teamspace_from_interactive_menu(possible_teamspaces=possible_teamspaces)
112
112
  else:
113
- teamspace_dict = self._get_teamspace_from_name(
113
+ teamspace_name = self._get_teamspace_from_name(
114
114
  teamspace=teamspace, possible_teamspaces=possible_teamspaces
115
115
  )
116
116
 
117
- return Teamspace(**teamspace_dict)
117
+ return Teamspace(teamspace_name, org=org, user=user)
118
+
118
119
  except KeyboardInterrupt:
119
120
  raise KeyboardInterrupt from None
120
121
 
121
122
  except Exception as e:
122
123
  raise StudioCliError(
123
- f"Could not find the given Teamspace {teamspace}. "
124
+ f"Could not find the given Teamspace owned by {self._owner.name}. "
124
125
  "Please contact Lightning AI directly to resolve this issue."
125
126
  ) from e
@@ -0,0 +1,20 @@
1
+ import click
2
+
3
+
4
+ def register_commands(group: click.Group) -> None:
5
+ """Register studio commands with the given group."""
6
+ from lightning_sdk.cli.vm.create import create_vm
7
+ from lightning_sdk.cli.vm.delete import delete_vm
8
+ from lightning_sdk.cli.vm.list import list_vms
9
+ from lightning_sdk.cli.vm.ssh import ssh_vm
10
+ from lightning_sdk.cli.vm.start import start_vm
11
+ from lightning_sdk.cli.vm.stop import stop_vm
12
+ from lightning_sdk.cli.vm.switch import switch_vm
13
+
14
+ group.add_command(create_vm)
15
+ group.add_command(delete_vm)
16
+ group.add_command(list_vms)
17
+ group.add_command(ssh_vm)
18
+ group.add_command(start_vm)
19
+ group.add_command(stop_vm)
20
+ group.add_command(switch_vm)
@@ -0,0 +1,33 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.create import create_impl
6
+ from lightning_sdk.machine import CloudProvider
7
+
8
+
9
+ @click.command("create")
10
+ @click.option("--name", help="The name of the VM to create. If not provided, a random name will be generated.")
11
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
12
+ @click.option(
13
+ "--cloud-provider",
14
+ help="The cloud provider to start the VM on. Defaults to teamspace default.",
15
+ type=click.Choice(m.name for m in list(CloudProvider)),
16
+ )
17
+ @click.option(
18
+ "--cloud-account",
19
+ help="The cloud account to create the VM on. Defaults to teamspace default.",
20
+ type=click.STRING,
21
+ )
22
+ def create_vm(
23
+ name: Optional[str] = None,
24
+ teamspace: Optional[str] = None,
25
+ cloud_provider: Optional[str] = None,
26
+ cloud_account: Optional[str] = None,
27
+ ) -> None:
28
+ """Create a new VM.
29
+
30
+ Example:
31
+ lightning vm create
32
+ """
33
+ create_impl(name=name, teamspace=teamspace, cloud_provider=cloud_provider, cloud_account=cloud_account, vm=True)
@@ -0,0 +1,25 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.delete import delete_impl
6
+
7
+
8
+ @click.command("delete")
9
+ @click.option(
10
+ "--name",
11
+ help=(
12
+ "The name of the VM to delete. "
13
+ "If not provided, will try to infer from environment, "
14
+ "use the default value from the config or prompt for interactive selection."
15
+ ),
16
+ )
17
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
18
+ def delete_vm(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
19
+ """Delete a VM.
20
+
21
+ Example:
22
+ lightning vm delete --name my-vm
23
+
24
+ """
25
+ return delete_impl(name=name, teamspace=teamspace, vm=True)
@@ -0,0 +1,30 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.list import list_impl
6
+
7
+
8
+ @click.command("list")
9
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
10
+ @click.option(
11
+ "--all",
12
+ is_flag=True,
13
+ flag_value=True,
14
+ default=False,
15
+ help="List all VMs, not just the ones belonging to the authed user",
16
+ )
17
+ @click.option(
18
+ "--sort-by",
19
+ default=None,
20
+ type=click.Choice(["name", "teamspace", "status", "machine", "cloud-account"], case_sensitive=False),
21
+ help="the attribute to sort the VMs by.",
22
+ )
23
+ def list_vms(teamspace: Optional[str] = None, all: bool = False, sort_by: Optional[str] = None) -> None: # noqa: A002
24
+ """List VMs in a teamspace.
25
+
26
+ Example:
27
+ lightning vm list --teamspace owner/teamspace
28
+
29
+ """
30
+ return list_impl(teamspace=teamspace, all=all, sort_by=sort_by, vm=True)
@@ -0,0 +1,31 @@
1
+ from typing import List, Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.ssh import ssh_impl
6
+
7
+
8
+ @click.command("ssh")
9
+ @click.option(
10
+ "--name",
11
+ help=(
12
+ "The name of the VM to ssh into. "
13
+ "If not provided, will try to infer from environment, "
14
+ "use the default value from the config or prompt for interactive selection."
15
+ ),
16
+ )
17
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)", type=click.STRING)
18
+ @click.option(
19
+ "--option",
20
+ "-o",
21
+ help="Additional options to pass to the SSH command. Can be specified multiple times.",
22
+ multiple=True,
23
+ type=click.STRING,
24
+ )
25
+ def ssh_vm(name: Optional[str] = None, teamspace: Optional[str] = None, option: Optional[List[str]] = None) -> None:
26
+ """SSH into a VM.
27
+
28
+ Example:
29
+ lightning vm ssh --name my-vm
30
+ """
31
+ return ssh_impl(name=name, teamspace=teamspace, option=option, vm=False)
@@ -0,0 +1,60 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.start import start_impl
6
+ from lightning_sdk.machine import CloudProvider, Machine
7
+
8
+
9
+ @click.command("start")
10
+ @click.option(
11
+ "--name",
12
+ help=(
13
+ "The name of the VM to start. "
14
+ "If not provided, will try to infer from environment, "
15
+ "use the default value from the config or prompt for interactive selection."
16
+ ),
17
+ )
18
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
19
+ @click.option("--create", is_flag=True, help="Create the VM if it doesn't exist")
20
+ @click.option(
21
+ "--machine",
22
+ help="The machine type to start the VM on. Defaults to CPU-4",
23
+ type=click.Choice(m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli),
24
+ )
25
+ @click.option("--interruptible", is_flag=True, help="Start the VM on an interruptible instance.")
26
+ @click.option(
27
+ "--cloud-provider",
28
+ help=("The cloud provider to start the VM on. Defaults to teamspace default. Only used if --create is specified."),
29
+ type=click.Choice(m.name for m in list(CloudProvider)),
30
+ )
31
+ @click.option(
32
+ "--cloud-account",
33
+ help="The cloud account to start the VM on. Defaults to teamspace default. Only used if --create is specified.",
34
+ type=click.STRING,
35
+ )
36
+ def start_vm(
37
+ name: Optional[str] = None,
38
+ teamspace: Optional[str] = None,
39
+ create: bool = False,
40
+ machine: str = "CPU",
41
+ interruptible: bool = False,
42
+ cloud_provider: Optional[str] = None,
43
+ cloud_account: Optional[str] = None,
44
+ ) -> None:
45
+ """Start a VM.
46
+
47
+ Example:
48
+ lightning vm start --name my-vm
49
+
50
+ """
51
+ return start_impl(
52
+ name=name,
53
+ teamspace=teamspace,
54
+ create=create,
55
+ machine=machine,
56
+ interruptible=interruptible,
57
+ cloud_provider=cloud_provider,
58
+ cloud_account=cloud_account,
59
+ vm=True,
60
+ )
@@ -0,0 +1,25 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.stop import stop_impl
6
+
7
+
8
+ @click.command("stop")
9
+ @click.option(
10
+ "--name",
11
+ help=(
12
+ "The name of the VM to stop. "
13
+ "If not provided, will try to infer from environment, "
14
+ "use the default value from the config or prompt for interactive selection."
15
+ ),
16
+ )
17
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
18
+ def stop_vm(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
19
+ """Stop a VM.
20
+
21
+ Example:
22
+ lightning vm stop --name my-vm
23
+
24
+ """
25
+ return stop_impl(name=name, teamspace=teamspace, vm=True)
@@ -0,0 +1,38 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.studio.switch import switch_impl
6
+ from lightning_sdk.machine import Machine
7
+
8
+
9
+ @click.command("switch")
10
+ @click.option(
11
+ "--name",
12
+ help=(
13
+ "The name of the VM to switch to a different machine. "
14
+ "If not provided, will try to infer from environment, "
15
+ "use the default value from the config or prompt for interactive selection."
16
+ ),
17
+ )
18
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
19
+ @click.option(
20
+ "--machine",
21
+ help="The machine type to switch the studio to.",
22
+ type=click.Choice(m.name for m in Machine.__dict__.values() if isinstance(m, Machine)),
23
+ )
24
+ @click.option("--interruptible", is_flag=True, help="Switch the studio to an interruptible instance.")
25
+ def switch_vm(
26
+ name: Optional[str] = None,
27
+ teamspace: Optional[str] = None,
28
+ machine: Optional[str] = None,
29
+ interruptible: bool = False,
30
+ ) -> None:
31
+ """Switch a VM to a different machine type."""
32
+ return switch_impl(
33
+ name=name,
34
+ teamspace=teamspace,
35
+ machine=machine,
36
+ interruptible=interruptible,
37
+ vm=True,
38
+ )