lightning-sdk 2025.12.17__py3-none-any.whl → 2026.1.22__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 (99) hide show
  1. lightning_sdk/__version__.py +1 -1
  2. lightning_sdk/api/k8s_api.py +75 -29
  3. lightning_sdk/api/studio_api.py +195 -33
  4. lightning_sdk/api/teamspace_api.py +28 -9
  5. lightning_sdk/cli/cp/__init__.py +64 -0
  6. lightning_sdk/cli/entrypoint.py +2 -0
  7. lightning_sdk/cli/groups.py +22 -0
  8. lightning_sdk/cli/legacy/clusters_menu.py +2 -2
  9. lightning_sdk/cli/legacy/deploy/_auth.py +7 -6
  10. lightning_sdk/cli/legacy/run.py +13 -2
  11. lightning_sdk/cli/studio/__init__.py +4 -0
  12. lightning_sdk/cli/studio/cp.py +20 -64
  13. lightning_sdk/cli/studio/ls.py +57 -0
  14. lightning_sdk/cli/studio/rm.py +71 -0
  15. lightning_sdk/cli/utils/logging.py +2 -1
  16. lightning_sdk/cli/utils/studio_filesystem.py +65 -0
  17. lightning_sdk/cli/utils/teamspace_selection.py +5 -0
  18. lightning_sdk/exceptions.py +4 -0
  19. lightning_sdk/job/base.py +1 -1
  20. lightning_sdk/k8s_cluster.py +9 -10
  21. lightning_sdk/lightning_cloud/__version__.py +1 -1
  22. lightning_sdk/lightning_cloud/openapi/__init__.py +29 -11
  23. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -1
  24. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +113 -0
  25. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +268 -123
  26. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +246 -19
  27. lightning_sdk/lightning_cloud/openapi/api/lightningwork_service_api.py +116 -11
  28. lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +588 -2
  29. lightning_sdk/lightning_cloud/openapi/api/models_store_api.py +9 -1
  30. lightning_sdk/lightning_cloud/openapi/api/{kubernetes_virtual_machine_service_api.py → virtual_machine_service_api.py} +82 -82
  31. lightning_sdk/lightning_cloud/openapi/models/__init__.py +28 -10
  32. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_cluster_capacity_reservation_body.py +53 -1
  33. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_machine_body.py +53 -1
  34. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_org_cluster_capacity_reservation_body.py +409 -0
  35. lightning_sdk/lightning_cloud/openapi/models/cluster_service_report_machine_system_metrics_body.py +123 -0
  36. lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
  37. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_create_lit_logger_media_body.py +305 -0
  38. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_lit_logger_media_body.py +149 -0
  39. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_metrics_stream_body.py +53 -1
  40. lightning_sdk/lightning_cloud/openapi/models/v1_capacity_reservation_used_by.py +227 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
  43. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
  45. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +53 -27
  46. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +0 -1
  47. lightning_sdk/lightning_cloud/openapi/models/v1_create_lit_logger_media_response.py +149 -0
  48. lightning_sdk/lightning_cloud/openapi/models/v1_create_org_cluster_capacity_reservation_response.py +201 -0
  49. lightning_sdk/lightning_cloud/openapi/models/v1_create_sdk_command_history_request.py +29 -3
  50. lightning_sdk/lightning_cloud/openapi/models/{v1_ai_pod_v1.py → v1_cudo_direct_v1.py} +51 -51
  51. lightning_sdk/lightning_cloud/openapi/models/{v1_delete_kubernetes_virtual_machine_response.py → v1_delete_lit_logger_media_response.py} +6 -6
  52. lightning_sdk/lightning_cloud/openapi/models/{kubernetes_virtual_machine_service_update_kubernetes_virtual_machine_body.py → v1_delete_virtual_machine_response.py} +6 -6
  53. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -27
  54. lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
  55. lightning_sdk/lightning_cloud/openapi/models/v1_get_kubernetes_pod_logs_response.py +149 -0
  56. lightning_sdk/lightning_cloud/openapi/models/{v1_get_machine_response.py → v1_get_kubernetes_pod_response.py} +23 -23
  57. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
  58. lightning_sdk/lightning_cloud/openapi/models/v1_joinable_organization.py +27 -1
  59. lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_setting.py +149 -0
  60. lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_type.py +108 -0
  61. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_settings_v1.py +53 -1
  62. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +53 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod.py +27 -1
  64. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod_logs_page.py +227 -0
  65. lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_config.py +53 -1
  66. lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_response.py +43 -17
  67. lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_sort_order.py +104 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_list_lit_logger_media_response.py +149 -0
  69. lightning_sdk/lightning_cloud/openapi/models/v1_list_models_response.py +55 -3
  70. lightning_sdk/lightning_cloud/openapi/models/{v1_list_kubernetes_virtual_machines_response.py → v1_list_virtual_machines_response.py} +16 -16
  71. lightning_sdk/lightning_cloud/openapi/models/v1_lit_logger_media.py +513 -0
  72. lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -53
  73. lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +107 -3
  74. lightning_sdk/lightning_cloud/openapi/models/v1_media_type.py +104 -0
  75. lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +29 -3
  76. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
  77. lightning_sdk/lightning_cloud/openapi/models/v1_report_cloud_space_instance_idle_state_response.py +97 -0
  78. lightning_sdk/lightning_cloud/openapi/models/v1_report_machine_system_metrics_response.py +97 -0
  79. lightning_sdk/lightning_cloud/openapi/models/v1_tenant_credentials.py +201 -0
  80. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +157 -131
  81. lightning_sdk/lightning_cloud/openapi/models/{v1_kubernetes_virtual_machine.py → v1_virtual_machine.py} +94 -68
  82. lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_vm_configuration.py → v1_vm_configuration.py} +20 -20
  83. lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_provider_configuration.py → v1_vm_provider_configuration.py} +32 -32
  84. lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_create_virtual_machine_body.py +565 -0
  85. lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_update_virtual_machine_body.py +97 -0
  86. lightning_sdk/lightning_cloud/rest_client.py +0 -2
  87. lightning_sdk/machine.py +3 -3
  88. lightning_sdk/studio.py +14 -4
  89. lightning_sdk/utils/logging.py +2 -1
  90. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.22.dist-info}/METADATA +1 -5
  91. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.22.dist-info}/RECORD +95 -75
  92. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.22.dist-info}/WHEEL +1 -1
  93. lightning_sdk/lightning_cloud/cli/__main__.py +0 -29
  94. lightning_sdk/lightning_cloud/openapi/models/kubernetes_virtual_machine_service_create_kubernetes_virtual_machine_body.py +0 -513
  95. lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_vm_resources.py +0 -201
  96. lightning_sdk/lightning_cloud/source_code/logs_socket_api.py +0 -103
  97. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.22.dist-info}/LICENSE +0 -0
  98. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.22.dist-info}/entry_points.txt +0 -0
  99. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.22.dist-info}/top_level.txt +0 -0
@@ -50,6 +50,15 @@ def run() -> None:
50
50
  help="The organization owning the teamspace (if any). Defaults to the current organization.",
51
51
  )
52
52
  @click.option("--user", default=None, help="The user owning the teamspace (if any). Defaults to the current user.")
53
+ @click.option(
54
+ "--cloud-provider",
55
+ "--cloud_provider",
56
+ default=None,
57
+ help=(
58
+ "The provider to create the studio on. If set, must be in agreement with the provider from the "
59
+ "cloud_account (if specified)."
60
+ ),
61
+ )
53
62
  @click.option(
54
63
  "--cloud-account",
55
64
  "--cloud_account",
@@ -96,7 +105,7 @@ def run() -> None:
96
105
  )
97
106
  @click.option(
98
107
  "--entrypoint",
99
- default="sh -c",
108
+ default=None,
100
109
  show_default=True,
101
110
  help=(
102
111
  "The entrypoint of your docker container. "
@@ -141,6 +150,7 @@ def job(
141
150
  teamspace: Optional[str] = None,
142
151
  org: Optional[str] = None,
143
152
  user: Optional[str] = None,
153
+ cloud_provider: Optional[str] = None,
144
154
  cloud_account: Optional[str] = None,
145
155
  env: Sequence[str] = (),
146
156
  interruptible: bool = False,
@@ -184,6 +194,7 @@ def job(
184
194
  teamspace=resolved_teamspace,
185
195
  org=org,
186
196
  user=user,
197
+ cloud_provider=cloud_provider,
187
198
  cloud_account=cloud_account,
188
199
  env=env_dict,
189
200
  interruptible=interruptible,
@@ -289,7 +300,7 @@ def job(
289
300
  )
290
301
  @click.option(
291
302
  "--entrypoint",
292
- default="sh -c",
303
+ default=None,
293
304
  show_default=True,
294
305
  help=(
295
306
  "The entrypoint of your docker container. "
@@ -10,6 +10,8 @@ def register_commands(group: click.Group) -> None:
10
10
  from lightning_sdk.cli.studio.create import create_studio
11
11
  from lightning_sdk.cli.studio.delete import delete_studio
12
12
  from lightning_sdk.cli.studio.list import list_studios
13
+ from lightning_sdk.cli.studio.ls import ls_studio
14
+ from lightning_sdk.cli.studio.rm import rm_studio_file
13
15
  from lightning_sdk.cli.studio.ssh import ssh_studio
14
16
  from lightning_sdk.cli.studio.start import start_studio
15
17
  from lightning_sdk.cli.studio.stop import stop_studio
@@ -24,3 +26,5 @@ def register_commands(group: click.Group) -> None:
24
26
  group.add_command(switch_studio)
25
27
  group.add_command(connect_studio)
26
28
  group.add_command(cp_studio_file)
29
+ group.add_command(ls_studio)
30
+ group.add_command(rm_studio_file)
@@ -2,22 +2,20 @@
2
2
 
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import Optional, TypedDict
5
+ from typing import Optional
6
6
 
7
7
  import click
8
8
  from rich.console import Console
9
9
 
10
10
  from lightning_sdk.api.utils import _get_cloud_url
11
- from lightning_sdk.cli.utils.owner_selection import OwnerMenu
12
- from lightning_sdk.cli.utils.studio_selection import StudiosMenu
13
- from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
14
- from lightning_sdk.studio import Studio
11
+ from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
15
12
 
16
13
 
17
14
  @click.command("cp")
18
15
  @click.argument("source", nargs=1)
19
16
  @click.argument("destination", nargs=1)
20
- def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = None) -> None:
17
+ @click.option("-r", "--recursive", is_flag=True, help="Copy directories recursively")
18
+ def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = None, recursive: bool = False) -> None:
21
19
  """Copy a Studio file.
22
20
 
23
21
  SOURCE: Source file to copy from. For Studio files, use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
@@ -26,70 +24,29 @@ def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = Non
26
24
 
27
25
  Example:
28
26
  lightning studio cp source.txt lit://<owner>/<my-teamspace>/studios/<my-studio>/destination.txt
27
+ lightning studio cp -r source_folder/ lit://<owner>/<my-teamspace>/studios/<my-studio>/destination_folder/
29
28
 
30
29
  """
31
- return cp_impl(source=source, destination=destination)
30
+ return cp_impl(source=source, destination=destination, recursive=recursive)
32
31
 
33
32
 
34
- def cp_impl(source: str, destination: str) -> None:
33
+ def cp_impl(source: str, destination: str, recursive: bool = False) -> None:
35
34
  if "lit://" in source and "lit://" in destination:
36
35
  raise ValueError("Both source and destination cannot be Studio files.")
37
36
  elif "lit://" not in source and "lit://" not in destination:
38
37
  raise ValueError("Either source or destination must be a Studio file.")
39
38
  elif "lit://" in source:
40
39
  # Download from Studio to local
41
- cp_download(studio_path=source, local_path=destination)
40
+ cp_download(studio_path=source, local_path=destination, recursive=recursive)
42
41
  else:
43
42
  # Upload from local to Studio
44
- cp_upload(local_file_path=source, studio_file_path=destination)
45
-
46
-
47
- class StudioPathResult(TypedDict):
48
- owner: Optional[str]
49
- teamspace: Optional[str]
50
- studio: Optional[str]
51
- destination: Optional[str]
52
-
53
-
54
- def parse_studio_path(studio_path: str) -> StudioPathResult:
55
- path_string = studio_path.removeprefix("lit://")
56
- if not path_string:
57
- raise ValueError("Studio path cannot be empty after prefix")
58
-
59
- result: StudioPathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
60
-
61
- if "/studios/" in path_string:
62
- prefix_part, suffix_part = path_string.split("/studios/", 1)
63
-
64
- # org and teamspace
65
- if prefix_part:
66
- org_ts_components = prefix_part.split("/")
67
- if len(org_ts_components) == 2:
68
- result["owner"], result["teamspace"] = org_ts_components
69
- elif len(org_ts_components) == 1:
70
- result["teamspace"] = org_ts_components[0]
71
- else:
72
- raise ValueError(f"Invalid format: '{prefix_part}'")
73
-
74
- # studio and destination
75
- path_parts = suffix_part.split("/")
76
-
77
- else:
78
- # studio and destination
79
- path_parts = path_string.split("/")
80
-
81
- if not path_parts or len(path_parts) < 2:
82
- raise ValueError("Invalid: Missing studio name.")
83
-
84
- result["studio"] = path_parts[0]
85
- result["destination"] = "/".join(path_parts[1:])
86
-
87
- return result
43
+ cp_upload(local_file_path=source, studio_file_path=destination, recursive=recursive)
88
44
 
89
45
 
90
46
  def cp_upload(
91
47
  local_file_path: str,
92
48
  studio_file_path: str,
49
+ recursive: bool = False,
93
50
  ) -> None:
94
51
  console = Console()
95
52
  if not Path(local_file_path).exists():
@@ -103,6 +60,8 @@ def cp_upload(
103
60
  console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
104
61
 
105
62
  if Path(local_file_path).is_dir():
63
+ if not recursive:
64
+ raise ValueError(f"'{local_file_path}' is a directory. Use -r flag to copy directories recursively.")
106
65
  selected_studio.upload_folder(local_file_path, studio_path_result["destination"])
107
66
  else:
108
67
  if studio_file_path.endswith(("/", "\\")):
@@ -126,6 +85,7 @@ def cp_upload(
126
85
  def cp_download(
127
86
  studio_path: str,
128
87
  local_path: str,
88
+ recursive: bool = False,
129
89
  ) -> None:
130
90
  console = Console()
131
91
  studio_path_result = parse_studio_path(studio_path)
@@ -146,7 +106,14 @@ def cp_download(
146
106
 
147
107
  console.print(f"Downloading from {selected_studio.teamspace.name}/{selected_studio.name}")
148
108
  if path_info["type"] == "directory":
109
+ if not recursive:
110
+ raise ValueError(
111
+ f"'{studio_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
112
+ )
149
113
  folder_name = os.path.basename(studio_path_result["destination"].rstrip("/"))
114
+ if folder_name == "" and local_path in ("./", "."):
115
+ # handle root directory case (e.g. lit://lightning-ai/gpt-oss/studios/manual-lime-ylu2/)
116
+ folder_name = selected_studio.name
150
117
  target_path = os.path.join(local_path, folder_name)
151
118
 
152
119
  selected_studio.download_folder(studio_path_result["destination"], target_path)
@@ -161,14 +128,3 @@ def cp_download(
161
128
  os.makedirs(os.path.dirname(target_path), exist_ok=True)
162
129
  selected_studio.download_file(studio_path_result["destination"], target_path)
163
130
  console.print(f"See your file at {target_path}")
164
-
165
-
166
- def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
167
- owner_menu = OwnerMenu()
168
- resolved_owner = owner_menu(owner=owner)
169
-
170
- teamspace_menu = TeamspacesMenu(resolved_owner)
171
- resolved_teamspace = teamspace_menu(teamspace=teamspace)
172
-
173
- studio_menu = StudiosMenu(resolved_teamspace)
174
- return studio_menu(studio=studio_name)
@@ -0,0 +1,57 @@
1
+ """Studio ls command."""
2
+
3
+ import click
4
+
5
+ from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
6
+
7
+
8
+ @click.command("ls")
9
+ @click.argument("path", nargs=1)
10
+ def ls_studio(path: str) -> None:
11
+ """List contents of a directory in Studio.
12
+
13
+ PATH: Studio path in the format
14
+ lit://<owner>/<teamspace>/studios/<studio>/<directory-path>
15
+
16
+ Example:
17
+ lightning studio ls lit://<owner>/<my-teamspace>/studios/<my-studio>/data
18
+
19
+ """
20
+ return ls_impl(path=path)
21
+
22
+
23
+ def ls_impl(path: str) -> None:
24
+ if not path.startswith("lit://"):
25
+ raise ValueError("Path must be a Studio path starting with 'lit://'.")
26
+
27
+ studio_path_result = parse_studio_path(path)
28
+ selected_studio = resolve_studio(
29
+ studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
30
+ )
31
+
32
+ path_info = selected_studio._studio_api.get_path_info(
33
+ selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
34
+ )
35
+
36
+ if not path_info["exists"]:
37
+ raise FileNotFoundError(
38
+ f"The provided path does not exist in the studio: {studio_path_result['destination']} "
39
+ "Note that empty folders may not be detected as existing."
40
+ )
41
+
42
+ if path_info["type"] == "file":
43
+ # print the file name if it's a file (bash-like behavior)
44
+ print(studio_path_result["destination"])
45
+ return
46
+
47
+ tree = selected_studio._studio_api.get_tree(
48
+ selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
49
+ )
50
+
51
+ tree_items = tree.get("tree", [])
52
+
53
+ for item in tree_items:
54
+ name = item.get("path", "")
55
+ if item.get("type") == "tree":
56
+ name += "/"
57
+ print(name)
@@ -0,0 +1,71 @@
1
+ """Studio rm command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
7
+ from lightning_sdk.studio import Studio
8
+
9
+
10
+ @click.command("rm")
11
+ @click.argument("path", nargs=1)
12
+ @click.option("-r", "--recursive", is_flag=True, help="Remove directories recursively")
13
+ @click.option("-f", "--force", is_flag=True, help="Ignore nonexistent files, never prompt")
14
+ def rm_studio_file(path: str, recursive: bool = False, force: bool = False) -> None:
15
+ """Remove a Studio file or directory.
16
+
17
+ PATH: Studio path to remove. Use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
18
+
19
+ Example:
20
+ lightning studio rm lit://<owner>/<my-teamspace>/studios/<my-studio>/file.txt
21
+ lightning studio rm -r lit://<owner>/<my-teamspace>/studios/<my-studio>/folder/
22
+
23
+ """
24
+ return rm_impl(path=path, recursive=recursive, force=force)
25
+
26
+
27
+ def rm_impl(path: str, recursive: bool = False, force: bool = False) -> None:
28
+ if "lit://" not in path:
29
+ raise ValueError("Path must be a Studio path starting with 'lit://'.")
30
+
31
+ console = Console()
32
+ studio_path_result = parse_studio_path(path)
33
+
34
+ selected_studio = resolve_studio(
35
+ studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
36
+ )
37
+
38
+ # check if file/folder exists
39
+ path_info = selected_studio._studio_api.get_path_info(
40
+ selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
41
+ )
42
+
43
+ if not path_info["exists"]:
44
+ if force:
45
+ # silently ignore nonexistent files with -f flag
46
+ return
47
+ raise FileNotFoundError(
48
+ f"The provided path does not exist in the studio: '{studio_path_result['destination']}'. "
49
+ "Note that empty folders may not be detected as existing."
50
+ )
51
+
52
+ console.print(f"Removing from {selected_studio.teamspace.name}/{selected_studio.name}")
53
+
54
+ if path_info["type"] == "directory":
55
+ if not recursive:
56
+ raise ValueError(
57
+ f"'{studio_path_result['destination']}' is a directory. Use -r flag to remove directories recursively."
58
+ )
59
+ rm_folder(selected_studio=selected_studio, path=studio_path_result["destination"], console=console)
60
+ else:
61
+ rm_file(selected_studio=selected_studio, path=studio_path_result["destination"], console=console)
62
+
63
+
64
+ def rm_file(selected_studio: Studio, path: str, console: Console) -> None:
65
+ selected_studio._studio_api.remove_file(selected_studio._studio.id, selected_studio._teamspace.id, path)
66
+ console.print(f"Removed file: {path}")
67
+
68
+
69
+ def rm_folder(selected_studio: Studio, path: str, console: Console) -> None:
70
+ selected_studio._studio_api.remove_folder(selected_studio._studio.id, selected_studio._teamspace.id, path)
71
+ console.print(f"Removed directory: {path}")
@@ -30,10 +30,11 @@ def _log_command(message: str = "", duration: int = 0, error: Optional[str] = No
30
30
  body = V1CreateSDKCommandHistoryRequest(
31
31
  command=original_command,
32
32
  duration=duration,
33
- message=f"VERSION: {__version__} | {message}",
33
+ message=f"{message}",
34
34
  project_id=None,
35
35
  severity=V1SDKCommandHistorySeverity.INFO,
36
36
  type=V1SDKCommandHistoryType.CLI,
37
+ version=__version__,
37
38
  )
38
39
 
39
40
  if error:
@@ -0,0 +1,65 @@
1
+ from typing import Optional, TypedDict
2
+
3
+ from lightning_sdk.cli.utils.owner_selection import OwnerMenu
4
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
5
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
6
+ from lightning_sdk.studio import Studio
7
+
8
+
9
+ class StudioPathResult(TypedDict):
10
+ owner: Optional[str]
11
+ teamspace: Optional[str]
12
+ studio: Optional[str]
13
+ destination: Optional[str]
14
+
15
+
16
+ def parse_studio_path(studio_path: str) -> StudioPathResult:
17
+ path_string = studio_path.removeprefix("lit://")
18
+ if not path_string:
19
+ raise ValueError("Studio path cannot be empty after prefix")
20
+
21
+ result: StudioPathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
22
+
23
+ if "/studios/" in path_string:
24
+ prefix_part, suffix_part = path_string.split("/studios/", 1)
25
+
26
+ # org and teamspace
27
+ if prefix_part:
28
+ org_ts_components = prefix_part.split("/")
29
+ if len(org_ts_components) == 2:
30
+ result["owner"], result["teamspace"] = org_ts_components
31
+ elif len(org_ts_components) == 1:
32
+ result["teamspace"] = org_ts_components[0]
33
+ else:
34
+ raise ValueError(f"Invalid format: '{prefix_part}'")
35
+
36
+ # studio and destination
37
+ path_parts = suffix_part.split("/")
38
+
39
+ else:
40
+ # studio and destination
41
+ path_parts = path_string.split("/")
42
+
43
+ if not path_parts:
44
+ raise ValueError("Invalid: Missing studio name.")
45
+
46
+ if len(path_parts) == 1:
47
+ raise ValueError(
48
+ "Invalid: Invalid studio path. To refer to the studio root, add a trailing '/' (e.g., 'lit://<owner>/<my-teamspace>/studios/<my-studio>/')"
49
+ )
50
+
51
+ result["studio"] = path_parts[0]
52
+ result["destination"] = "/".join(path_parts[1:])
53
+
54
+ return result
55
+
56
+
57
+ def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
58
+ owner_menu = OwnerMenu()
59
+ resolved_owner = owner_menu(owner=owner)
60
+
61
+ teamspace_menu = TeamspacesMenu(resolved_owner)
62
+ resolved_teamspace = teamspace_menu(teamspace=teamspace)
63
+
64
+ studio_menu = StudiosMenu(resolved_teamspace)
65
+ return studio_menu(studio=studio_name)
@@ -51,6 +51,11 @@ class TeamspacesMenu:
51
51
  return TerminalMenu(possible_teamspaces, title=title, clear_menu_on_exit=True)
52
52
 
53
53
  def _get_possible_teamspaces(self, user: User) -> Dict[str, str]:
54
+ if self._owner is None:
55
+ from lightning_sdk.cli.utils.owner_selection import OwnerMenu
56
+
57
+ menu = OwnerMenu()
58
+ self._owner = menu()
54
59
  user_api = user._user_api
55
60
 
56
61
  memberships = user_api._get_all_teamspace_memberships(
@@ -1,2 +1,6 @@
1
1
  class OutOfCapacityError(RuntimeError):
2
2
  """Raised when the requested machine is not available in the selected cloud account."""
3
+
4
+
5
+ class NotSupportedError(RuntimeError):
6
+ """Raised when the requested machine is not supported in the selected cloud account."""
lightning_sdk/job/base.py CHANGED
@@ -201,7 +201,7 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
201
201
  raise RuntimeError(
202
202
  "image and studio are mutually exclusive as both define the environment to run the job in"
203
203
  )
204
- if cloud_account is None and in_studio():
204
+ if cloud_account is None and cloud_provider is None and in_studio():
205
205
  try:
206
206
  with skip_studio_setup():
207
207
  resolve_studio = Studio(teamspace=teamspace, user=user, org=org)
@@ -1,8 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from datetime import datetime
3
- from typing import List, Optional
4
-
5
- import pandas as pd
3
+ from typing import Any, Dict, List, Optional
6
4
 
7
5
  from lightning_sdk.api.k8s_api import K8sClusterApi
8
6
  from lightning_sdk.api.utils import to_iso_z
@@ -34,25 +32,26 @@ class K8sCluster:
34
32
  self._cloud_account = cloud_account
35
33
  self._k8s_cluster = K8sClusterApi(cloud_account=cloud_account)
36
34
 
37
- def _convert_to_k8s_usage_response(self, df: pd.DataFrame) -> K8sUsageResponse:
38
- """Converts a DataFrame to K8sUsageResponse.
35
+ def _convert_to_k8s_usage_response(self, data: List[Dict[str, Any]]) -> K8sUsageResponse:
36
+ """Converts a list of usage data to K8sUsageResponse.
39
37
 
40
38
  Args:
41
- df (pd.DataFrame): The DataFrame containing GPU usage data.
39
+ data (List[Dict[str, Any]]): The list of dictionaries containing GPU usage data.
42
40
 
43
41
  Returns:
44
42
  K8sUsageResponse: The converted response containing hourly usage and total usage.
45
43
  """
46
- if df.empty:
44
+ if not data:
47
45
  return K8sUsageResponse(hours=[], total_usage=0.0)
48
- # Convert each row of the DataFrame to HourlyUsage
46
+
47
+ # Convert each row to HourlyUsage
49
48
  hourly_usage_list: List[HourlyUsage] = [
50
49
  HourlyUsage(time=row["hour"], available_gpus=row["num_gpus"], billed_gpus=row["billed_gpus"])
51
- for _, row in df.iterrows()
50
+ for row in data
52
51
  ]
53
52
 
54
53
  # Calculate total usage (sum of billed GPUs)
55
- total_usage = df["billed_gpus"].sum()
54
+ total_usage = sum(row["billed_gpus"] for row in data)
56
55
 
57
56
  # Create and return the K8sUsageResponse
58
57
  return K8sUsageResponse(hours=hourly_usage_list, total_usage=total_usage)
@@ -1 +1 @@
1
- __version__ = "0.5.70"
1
+ __version__ = "0.6.0"