lightning-sdk 2026.1.22__py3-none-any.whl → 2026.1.30__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 (61) hide show
  1. lightning_sdk/__version__.py +1 -1
  2. lightning_sdk/api/studio_api.py +38 -39
  3. lightning_sdk/api/teamspace_api.py +189 -72
  4. lightning_sdk/api/utils.py +69 -1
  5. lightning_sdk/cli/cp/__init__.py +14 -11
  6. lightning_sdk/cli/cp/teamspace_uploads.py +95 -0
  7. lightning_sdk/cli/legacy/download.py +29 -98
  8. lightning_sdk/cli/legacy/upload.py +24 -31
  9. lightning_sdk/cli/studio/cp.py +8 -5
  10. lightning_sdk/cli/studio/ls.py +1 -1
  11. lightning_sdk/cli/studio/rm.py +1 -1
  12. lightning_sdk/cli/utils/{studio_filesystem.py → filesystem.py} +49 -6
  13. lightning_sdk/exceptions.py +27 -0
  14. lightning_sdk/lightning_cloud/openapi/__init__.py +17 -12
  15. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  16. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +5 -1
  17. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +286 -468
  18. lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +579 -0
  19. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
  20. lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
  21. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +113 -0
  22. lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +113 -0
  23. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
  24. lightning_sdk/lightning_cloud/openapi/models/__init__.py +16 -12
  25. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → cluster_service_get_cluster_capacity_reservation_body.py} +6 -6
  26. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → container_registry_config_ecr.py} +49 -23
  27. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
  28. lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
  29. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
  30. lightning_sdk/lightning_cloud/openapi/models/jobs_service_duplicate_deployment_body.py +175 -0
  31. lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
  32. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
  33. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
  36. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
  37. lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_container_registry_scopes.py} +39 -39
  38. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_create_container_registry_response.py} +6 -6
  39. lightning_sdk/lightning_cloud/openapi/models/v1_delete_org_cluster_capacity_reservation_response.py +97 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
  42. lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → v1_get_cluster_capacity_reservation_response.py} +23 -23
  43. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +1 -27
  45. lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
  46. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → v1_mithril_direct_v1.py} +51 -51
  47. lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
  48. lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +27 -1
  49. lightning_sdk/lightning_cloud/openapi/models/v1_update_container_registry_response.py +97 -0
  50. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -105
  51. lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
  52. lightning_sdk/teamspace.py +28 -7
  53. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/METADATA +1 -1
  54. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/RECORD +59 -53
  55. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
  56. lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_details.py +0 -201
  57. /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
  58. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/LICENSE +0 -0
  59. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/WHEEL +0 -0
  60. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/entry_points.txt +0 -0
  61. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,95 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from rich.console import Console
5
+
6
+ from lightning_sdk.api.utils import _get_cloud_url
7
+ from lightning_sdk.cli.utils.filesystem import parse_teamspace_uploads_path, path_join, resolve_teamspace
8
+
9
+
10
+ def cp_upload(
11
+ local_file_path: str,
12
+ teamspace_path: str,
13
+ options: dict[str, any],
14
+ ) -> None:
15
+ console = Console()
16
+ recursive = options.get("recursive", False)
17
+ cloud_account = options.get("cloud_account", None)
18
+ if not Path(local_file_path).exists():
19
+ raise FileNotFoundError(f"The provided path does not exist: {local_file_path}")
20
+
21
+ teamspace_path_result = parse_teamspace_uploads_path(teamspace_path)
22
+
23
+ teamspace_path_result["destination"] = path_join("Uploads", teamspace_path_result["destination"])
24
+
25
+ selected_teamspace = resolve_teamspace(teamspace_path_result["teamspace"], teamspace_path_result["owner"])
26
+ console.print(f"Uploading to {selected_teamspace.owner.name}/{selected_teamspace.name}")
27
+
28
+ if Path(local_file_path).is_dir():
29
+ if not recursive:
30
+ raise ValueError(f"'{local_file_path}' is a directory. Use -r flag to copy directories recursively.")
31
+ selected_teamspace.upload_folder(
32
+ local_file_path, teamspace_path_result["destination"], cloud_account=cloud_account
33
+ )
34
+ else:
35
+ if teamspace_path.endswith(("/", "\\")):
36
+ # if destination ends with / or \, treat it as a directory
37
+ file_name = os.path.basename(local_file_path)
38
+ teamspace_path_result["destination"] = path_join(teamspace_path_result["destination"], file_name)
39
+ selected_teamspace.upload_file(
40
+ local_file_path, teamspace_path_result["destination"], cloud_account=cloud_account
41
+ )
42
+
43
+ studio_url = (
44
+ _get_cloud_url().replace(":443", "") + "/" + selected_teamspace.owner.name + "/" + selected_teamspace.name
45
+ )
46
+ console.print(f"See your file at {studio_url}")
47
+
48
+
49
+ def cp_download(
50
+ teamspace_path: str,
51
+ local_path: str,
52
+ options: dict[str, any],
53
+ ) -> None:
54
+ console = Console()
55
+ teamspace_path_result = parse_teamspace_uploads_path(teamspace_path)
56
+ recursive = options.get("recursive", False)
57
+
58
+ selected_teamspace = resolve_teamspace(teamspace_path_result["teamspace"], teamspace_path_result["owner"])
59
+
60
+ # check if file/folder exists
61
+ path_info = selected_teamspace._teamspace_api.get_path_info(
62
+ selected_teamspace._teamspace.id, path=teamspace_path_result["destination"]
63
+ )
64
+ if not path_info["exists"]:
65
+ raise FileNotFoundError(
66
+ f"The provided path does not exist in the teamspace drive: {teamspace_path_result['destination']} "
67
+ "Note that empty folders may not be detected as existing."
68
+ )
69
+
70
+ console.print(f"Downloading from {selected_teamspace.owner.name}/{selected_teamspace.name}")
71
+ if path_info["type"] == "directory":
72
+ if not recursive:
73
+ raise ValueError(
74
+ f"'{teamspace_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
75
+ )
76
+ folder_name = os.path.basename(teamspace_path_result["destination"].rstrip("/"))
77
+ if local_path in ("./", "."):
78
+ if folder_name == "":
79
+ folder_name = f"{selected_teamspace.name}_downloads"
80
+ target_path = os.path.join(local_path, folder_name)
81
+ else:
82
+ target_path = local_path
83
+
84
+ selected_teamspace.download_folder(teamspace_path_result["destination"], target_path)
85
+ console.print(f"See your folder at {target_path}")
86
+ else:
87
+ if os.path.isdir(local_path) or local_path.endswith(("/", "\\")):
88
+ # if local_path ends with / or \ or is a directory, treat it as a directory
89
+ file_name = os.path.basename(teamspace_path_result["destination"])
90
+ target_path = os.path.join(local_path, file_name)
91
+ else:
92
+ target_path = local_path
93
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
94
+ selected_teamspace.download_file(teamspace_path_result["destination"], target_path)
95
+ console.print(f"See your file at {target_path}")
@@ -12,6 +12,7 @@ from lightning_sdk.api.lit_container_api import LitContainerApi
12
12
  from lightning_sdk.cli.legacy.exceptions import StudioCliError
13
13
  from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
14
14
  from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
15
+ from lightning_sdk.exceptions import DeprecatedCommand, DeprecatedError
15
16
  from lightning_sdk.models import download_model
16
17
  from lightning_sdk.studio import Studio
17
18
  from lightning_sdk.utils.resolve import _get_authed_user
@@ -63,134 +64,64 @@ def model(name: str, download_dir: str = ".") -> None:
63
64
  )
64
65
 
65
66
 
66
- @download.command(name="folder")
67
- @click.argument("path")
67
+ @download.command(
68
+ name="folder",
69
+ cls=DeprecatedCommand,
70
+ message="Studio downloads via 'lightning download folder' are deprecated. Use 'lightning studio cp -r' instead.",
71
+ )
72
+ @click.argument("path", required=False, nargs=-1)
68
73
  @click.option(
69
74
  "--studio",
70
75
  default=None,
71
- help=(
72
- "The name of the studio to download from. "
73
- "Will show a menu with user's owned studios for selection if not specified. "
74
- "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
75
- "The teamspace and studio names can be regular expressions to match, "
76
- "a menu filtered studios will be shown for final selection."
77
- ),
76
+ hidden=True,
78
77
  )
79
78
  @click.option(
80
79
  "--teamspace",
81
80
  default=None,
82
- help="The teamspace the drive folder is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
81
+ hidden=True,
83
82
  )
84
83
  @click.option(
85
84
  "--local-path",
86
85
  "--local_path",
87
- default=".",
88
- type=click.Path(file_okay=False, dir_okay=True),
89
- help="The path to the directory you want to download the folder to.",
86
+ default=None,
87
+ hidden=True,
90
88
  )
91
89
  def folder(
92
90
  path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = "."
93
91
  ) -> None:
94
- """Download a folder from a Studio or a Teamspace drive folder.
95
-
96
- Example:
97
- lightning download folder PATH
98
-
99
- PATH: The relative path within the Studio or drive folder you want to download.
100
- Defaults to the entire Studio or drive folder.
101
- """
102
- local_path = Path(local_path)
103
- if not local_path.is_dir():
104
- raise NotADirectoryError(f"'{local_path}' is not a directory")
105
-
106
- if studio and teamspace:
107
- raise ValueError("Either --studio or --teamspace must be provided, not both")
108
-
109
- if studio:
110
- path = _expand_remote_path(path)
111
- resolved_downloader = _resolve_studio(studio)
112
- elif teamspace:
113
- menu = TeamspacesMenu()
114
- resolved_downloader = menu(teamspace)
115
- else:
116
- raise ValueError("Either --studio or --teamspace must be provided")
117
-
118
- if not path:
119
- local_path /= resolved_downloader.name
120
- path = ""
121
-
122
- try:
123
- if not path and teamspace:
124
- raise FileNotFoundError()
125
- resolved_downloader.download_folder(remote_path=path, target_path=str(local_path))
126
- except Exception as e:
127
- raise StudioCliError(
128
- f"Could not download the folder from the given Studio {studio} or Teamspace {teamspace}. "
129
- "Please contact Lightning AI directly to resolve this issue."
130
- ) from e
92
+ """[DEPRECATED] Use 'lightning studio cp -r' instead."""
93
+ raise DeprecatedError(
94
+ "Studio downloads via 'lightning download folder' are deprecated. Use 'lightning studio cp -r' instead."
95
+ )
131
96
 
132
97
 
133
- @download.command(name="file")
134
- @click.argument("path")
98
+ @download.command(
99
+ name="file",
100
+ cls=DeprecatedCommand,
101
+ message="Studio downloads via 'lightning download file' are deprecated. Use 'lightning studio cp' instead.",
102
+ )
103
+ @click.argument("path", required=False, nargs=-1)
135
104
  @click.option(
136
105
  "--studio",
137
106
  default=None,
138
- help=(
139
- "The name of the studio to download from. "
140
- "Will show a menu with user's owned studios for selection if not specified. "
141
- "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
142
- "The teamspace and studio names can be regular expressions to match, "
143
- "a menu filtered studios will be shown for final selection."
144
- ),
107
+ hidden=True,
145
108
  )
146
109
  @click.option(
147
110
  "--teamspace",
148
111
  default=None,
149
- help="The teamspace the file is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
112
+ hidden=True,
150
113
  )
151
114
  @click.option(
152
115
  "--local-path",
153
116
  "--local_path",
154
- default=".",
155
- type=click.Path(file_okay=False, dir_okay=True),
156
- help="The path to the directory you want to download the file to.",
117
+ default=None,
118
+ hidden=True,
157
119
  )
158
120
  def file(path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = ".") -> None:
159
- """Download a file from a Studio or Teamspace drive file.
160
-
161
- Example:
162
- lightning download file PATH
163
-
164
- PATH: The relative path to the file within the Studio or Teamspace drive file you want to download.
165
- """
166
- local_path = Path(local_path)
167
- if not local_path.is_dir():
168
- raise NotADirectoryError(f"'{local_path}' is not a directory")
169
-
170
- if studio and teamspace:
171
- raise ValueError("Either --studio or --teamspace must be provided, not both")
172
-
173
- if studio:
174
- resolved_downloader = _resolve_studio(studio)
175
- elif teamspace:
176
- menu = TeamspacesMenu()
177
- resolved_downloader = menu(teamspace)
178
- else:
179
- raise ValueError("Either --studio or --teamspace must be provided")
180
-
181
- if not path:
182
- local_path /= resolved_downloader.name
183
- path = ""
184
-
185
- try:
186
- if not path:
187
- raise FileNotFoundError()
188
- resolved_downloader.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
189
- except Exception as e:
190
- raise StudioCliError(
191
- f"Could not download the file from the given Studio {studio} or Teamspace {teamspace}. "
192
- "Please contact Lightning AI directly to resolve this issue."
193
- ) from e
121
+ """[DEPRECATED] Use 'lightning studio cp' instead."""
122
+ raise DeprecatedError(
123
+ "Studio downloads via 'lightning download file' are deprecated. Use 'lightning studio cp' instead."
124
+ )
194
125
 
195
126
 
196
127
  @download.command(name="container")
@@ -18,6 +18,7 @@ from lightning_sdk.cli.legacy.exceptions import StudioCliError
18
18
  from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
19
19
  from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
20
20
  from lightning_sdk.constants import _LIGHTNING_DEBUG
21
+ from lightning_sdk.exceptions import DeprecatedCommand, DeprecatedError
21
22
  from lightning_sdk.models import upload_model as _upload_model
22
23
  from lightning_sdk.studio import Studio
23
24
  from lightning_sdk.utils.resolve import _get_authed_user
@@ -54,56 +55,48 @@ def model(name: str, path: str = ".", cloud_account: Optional[str] = None) -> No
54
55
  _upload_model(name, path, cloud_account=cloud_account)
55
56
 
56
57
 
57
- @upload.command("folder")
58
- @click.argument("path", type=click.Path(exists=True))
59
- @click.option(
60
- "--studio",
61
- default=None,
62
- help=(
63
- "The name of the studio to upload to. "
64
- "Will show a menu for selection if not specified. "
65
- "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>"
66
- ),
58
+ @upload.command(
59
+ name="folder",
60
+ cls=DeprecatedCommand,
61
+ message="Studio uploads via 'lightning upload folder' are deprecated. Use 'lightning studio cp -r' instead.",
67
62
  )
63
+ @click.argument("path", type=click.Path(), required=False, nargs=-1)
64
+ @click.option("--studio", default=None, hidden=True)
68
65
  @click.option(
69
66
  "--remote-path",
70
67
  "--remote_path",
71
68
  default=None,
72
- help=(
73
- "The path where the uploaded file should appear on your Studio. "
74
- "Has to be within your Studio's home directory and will be relative to that. "
75
- "If not specified, will use the name of the folder you want to upload and place it in your home directory."
76
- ),
69
+ hidden=True,
77
70
  )
78
71
  def folder(path: str, studio: Optional[str], remote_path: Optional[str]) -> None:
79
- """Upload a folder to a Studio."""
80
- _folder(path=path, studio=studio, remote_path=remote_path)
72
+ """[DEPRECATED] Use 'lightning studio cp -r' instead."""
73
+ raise DeprecatedError(
74
+ "Studio uploads via 'lightning upload folder' are deprecated. Use 'lightning studio cp -r' instead."
75
+ )
81
76
 
82
77
 
83
- @upload.command("file")
84
- @click.argument("path", type=click.Path(exists=True))
78
+ @upload.command(
79
+ name="file",
80
+ cls=DeprecatedCommand,
81
+ message="Studio uploads via 'lightning upload file' are deprecated. Use 'lightning studio cp' instead.",
82
+ )
83
+ @click.argument("path", type=click.Path(), required=False, nargs=-1)
85
84
  @click.option(
86
85
  "--studio",
87
86
  default=None,
88
- help=(
89
- "The name of the studio to upload to. "
90
- "Will show a menu for selection if not specified. "
91
- "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>"
92
- ),
87
+ hidden=True,
93
88
  )
94
89
  @click.option(
95
90
  "--remote-path",
96
91
  "--remote_path",
97
92
  default=None,
98
- help=(
99
- "The path where the uploaded file should appear on your Studio. "
100
- "Has to be within your Studio's home directory and will be relative to that. "
101
- "If not specified, will use the name of the file you want to upload and place it in your home directory."
102
- ),
93
+ hidden=True,
103
94
  )
104
95
  def file(path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
105
- """Upload a file to a Studio."""
106
- _file(path=path, studio=studio, remote_path=remote_path)
96
+ """[DEPRECATED] Use 'lightning studio cp' instead."""
97
+ raise DeprecatedError(
98
+ "Studio uploads via 'lightning upload file' are deprecated. Use 'lightning studio cp' instead."
99
+ )
107
100
 
108
101
 
109
102
  @upload.command("container")
@@ -8,7 +8,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.studio_filesystem import parse_studio_path, resolve_studio
11
+ from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
12
12
 
13
13
 
14
14
  @click.command("cp")
@@ -111,10 +111,13 @@ def cp_download(
111
111
  f"'{studio_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
112
112
  )
113
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
117
- target_path = os.path.join(local_path, folder_name)
114
+ if local_path in ("./", "."):
115
+ if folder_name == "":
116
+ # handle root directory case (e.g. lit://lightning-ai/gpt-oss/studios/manual-lime-ylu2/)
117
+ folder_name = selected_studio.name
118
+ target_path = os.path.join(local_path, folder_name)
119
+ else:
120
+ target_path = local_path
118
121
 
119
122
  selected_studio.download_folder(studio_path_result["destination"], target_path)
120
123
  console.print(f"See your folder at {target_path}")
@@ -2,7 +2,7 @@
2
2
 
3
3
  import click
4
4
 
5
- from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
5
+ from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
6
6
 
7
7
 
8
8
  @click.command("ls")
@@ -3,7 +3,7 @@
3
3
  import click
4
4
  from rich.console import Console
5
5
 
6
- from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
6
+ from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
7
7
  from lightning_sdk.studio import Studio
8
8
 
9
9
 
@@ -1,24 +1,30 @@
1
- from typing import Optional, TypedDict
1
+ import os
2
+ from typing import Any, Optional, TypedDict
2
3
 
3
4
  from lightning_sdk.cli.utils.owner_selection import OwnerMenu
4
5
  from lightning_sdk.cli.utils.studio_selection import StudiosMenu
5
6
  from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
6
7
  from lightning_sdk.studio import Studio
8
+ from lightning_sdk.teamspace import Teamspace
7
9
 
8
10
 
9
- class StudioPathResult(TypedDict):
11
+ class PathResult(TypedDict):
10
12
  owner: Optional[str]
11
13
  teamspace: Optional[str]
12
14
  studio: Optional[str]
13
15
  destination: Optional[str]
14
16
 
15
17
 
16
- def parse_studio_path(studio_path: str) -> StudioPathResult:
18
+ def path_join(*args: Any) -> str:
19
+ return os.path.join(*args).replace("\\", "/")
20
+
21
+
22
+ def parse_studio_path(studio_path: str) -> PathResult:
17
23
  path_string = studio_path.removeprefix("lit://")
18
24
  if not path_string:
19
25
  raise ValueError("Studio path cannot be empty after prefix")
20
26
 
21
- result: StudioPathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
27
+ result: PathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
22
28
 
23
29
  if "/studios/" in path_string:
24
30
  prefix_part, suffix_part = path_string.split("/studios/", 1)
@@ -54,12 +60,49 @@ def parse_studio_path(studio_path: str) -> StudioPathResult:
54
60
  return result
55
61
 
56
62
 
57
- def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
63
+ def parse_teamspace_uploads_path(teamspace_path: str) -> PathResult:
64
+ path_string = teamspace_path.removeprefix("lit://")
65
+ if not path_string:
66
+ raise ValueError("Teamspace path cannot be empty after prefix")
67
+
68
+ result: PathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
69
+
70
+ if "/uploads/" in path_string:
71
+ prefix_part, suffix_part = path_string.split("/uploads/", 1)
72
+
73
+ # org and teamspace
74
+ if prefix_part:
75
+ org_ts_components = prefix_part.split("/")
76
+ if len(org_ts_components) == 2:
77
+ result["owner"], result["teamspace"] = org_ts_components
78
+ elif len(org_ts_components) == 1:
79
+ result["teamspace"] = org_ts_components[0]
80
+ else:
81
+ raise ValueError(f"Invalid format: '{prefix_part}'")
82
+
83
+ # studio and destination
84
+ path_parts = suffix_part.split("/")
85
+
86
+ else:
87
+ raise ValueError("Invalid teamspace uploads path: missing '/uploads/' segment")
88
+
89
+ if not path_parts:
90
+ raise ValueError("Invalid: Missing teamspace name.")
91
+
92
+ result["destination"] = suffix_part
93
+
94
+ return result
95
+
96
+
97
+ def resolve_teamspace(teamspace: Optional[str], owner: Optional[str]) -> Teamspace:
58
98
  owner_menu = OwnerMenu()
59
99
  resolved_owner = owner_menu(owner=owner)
60
100
 
61
101
  teamspace_menu = TeamspacesMenu(resolved_owner)
62
- resolved_teamspace = teamspace_menu(teamspace=teamspace)
102
+ return teamspace_menu(teamspace=teamspace)
103
+
63
104
 
105
+ def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
106
+ resolved_teamspace = resolve_teamspace(teamspace, owner)
64
107
  studio_menu = StudiosMenu(resolved_teamspace)
65
108
  return studio_menu(studio=studio_name)
@@ -1,6 +1,33 @@
1
+ from typing import Any
2
+
3
+ import click
4
+
5
+
1
6
  class OutOfCapacityError(RuntimeError):
2
7
  """Raised when the requested machine is not available in the selected cloud account."""
3
8
 
4
9
 
5
10
  class NotSupportedError(RuntimeError):
6
11
  """Raised when the requested machine is not supported in the selected cloud account."""
12
+
13
+
14
+ class DeprecatedError(RuntimeError):
15
+ """Raised when a deprecated feature is used."""
16
+
17
+
18
+ class DeprecatedCommand(click.Command):
19
+ """Custom exception for deprecated commands."""
20
+
21
+ def __init__(self, *args: Any, message: str, **kwargs: Any) -> None:
22
+ super().__init__(*args, **kwargs)
23
+ self.deprecated_message = message
24
+
25
+ def get_help(self, ctx: click.Context) -> str:
26
+ if self.deprecated_message:
27
+ raise DeprecatedError(self.deprecated_message)
28
+ return super().get_help(ctx)
29
+
30
+ def invoke(self, ctx: click.Context) -> Any:
31
+ if self.deprecated_message:
32
+ raise DeprecatedError(self.deprecated_message)
33
+ return super().invoke(ctx)