lightning-sdk 2025.9.16__py3-none-any.whl → 2025.9.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightning_sdk/__init__.py +4 -3
- lightning_sdk/api/cloud_account_api.py +12 -1
- lightning_sdk/api/job_api.py +12 -11
- lightning_sdk/api/mmt_api.py +1 -1
- lightning_sdk/api/studio_api.py +1 -1
- lightning_sdk/api/teamspace_api.py +18 -0
- lightning_sdk/api/user_api.py +8 -2
- lightning_sdk/cli/utils/owner_selection.py +110 -0
- lightning_sdk/cli/utils/teamspace_selection.py +63 -62
- lightning_sdk/machine.py +16 -1
- lightning_sdk/teamspace.py +54 -0
- {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.23.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.23.dist-info}/RECORD +17 -16
- {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.23.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.23.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.23.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.9.16.dist-info → lightning_sdk-2025.9.23.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
|
@@ -10,14 +10,16 @@ from lightning_sdk.organization import Organization
|
|
|
10
10
|
from lightning_sdk.plugin import JobsPlugin, MultiMachineTrainingPlugin, Plugin, SlurmJobsPlugin
|
|
11
11
|
from lightning_sdk.status import Status
|
|
12
12
|
from lightning_sdk.studio import Studio
|
|
13
|
-
from lightning_sdk.teamspace import FolderLocation, Teamspace
|
|
13
|
+
from lightning_sdk.teamspace import ConnectionType, FolderLocation, Teamspace
|
|
14
14
|
from lightning_sdk.user import User
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"AIHub",
|
|
18
18
|
"Agent",
|
|
19
19
|
"CloudProvider",
|
|
20
|
+
"ConnectionType",
|
|
20
21
|
"Deployment",
|
|
22
|
+
"FolderLocation",
|
|
21
23
|
"Job",
|
|
22
24
|
"JobsPlugin",
|
|
23
25
|
"Machine",
|
|
@@ -29,10 +31,9 @@ __all__ = [
|
|
|
29
31
|
"Status",
|
|
30
32
|
"Studio",
|
|
31
33
|
"Teamspace",
|
|
32
|
-
"FolderLocation",
|
|
33
34
|
"User",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
|
-
__version__ = "2025.09.
|
|
37
|
+
__version__ = "2025.09.23"
|
|
37
38
|
_check_version_and_prompt_upgrade(__version__)
|
|
38
39
|
_set_tqdm_envvars_noninteractive()
|
|
@@ -13,6 +13,7 @@ from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from lightning_sdk.machine import CloudProvider
|
|
16
|
+
from lightning_sdk.teamspace import ConnectionType
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class CloudAccountApi:
|
|
@@ -133,7 +134,7 @@ class CloudAccountApi:
|
|
|
133
134
|
|
|
134
135
|
def get_cloud_account_provider_mapping(self, teamspace_id: str) -> Dict["CloudProvider", V1ExternalCluster]:
|
|
135
136
|
"""Gets the cloud account <-> provider mapping."""
|
|
136
|
-
res = self.
|
|
137
|
+
res = self.list_cloud_accounts(teamspace_id=teamspace_id)
|
|
137
138
|
cloud_accounts = {cloud_account.id: cloud_account for cloud_account in res}
|
|
138
139
|
providers = {cloud_account.id: self._get_cloud_account_provider(cloud_account) for cloud_account in res}
|
|
139
140
|
|
|
@@ -214,3 +215,13 @@ class CloudAccountApi:
|
|
|
214
215
|
return default_cloud_account
|
|
215
216
|
|
|
216
217
|
return None
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def get_cloud_provider_for_connection_type(connection_type: "ConnectionType") -> "CloudProvider":
|
|
221
|
+
from lightning_sdk.machine import CloudProvider
|
|
222
|
+
from lightning_sdk.teamspace import ConnectionType
|
|
223
|
+
|
|
224
|
+
if connection_type == ConnectionType.EFS:
|
|
225
|
+
return CloudProvider.AWS
|
|
226
|
+
|
|
227
|
+
raise ValueError(f"ConnectionType {ConnectionType} currently not supported!")
|
lightning_sdk/api/job_api.py
CHANGED
|
@@ -104,22 +104,23 @@ class JobApiV1:
|
|
|
104
104
|
org_id=org_id,
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
identifiers = []
|
|
108
108
|
|
|
109
109
|
if user_requested_compute_config and user_requested_compute_config.name:
|
|
110
|
-
|
|
110
|
+
identifiers.append(user_requested_compute_config.name)
|
|
111
111
|
else:
|
|
112
|
-
|
|
112
|
+
identifiers.append(spec.compute_config.instance_type)
|
|
113
113
|
|
|
114
114
|
for accelerator in accelerators:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
for ident in identifiers:
|
|
116
|
+
if ident in (
|
|
117
|
+
accelerator.slug,
|
|
118
|
+
accelerator.slug_multi_cloud,
|
|
119
|
+
accelerator.instance_id,
|
|
120
|
+
):
|
|
121
|
+
return Machine._from_accelerator(accelerator)
|
|
121
122
|
|
|
122
|
-
return Machine.from_str(
|
|
123
|
+
return Machine.from_str(identifiers[0])
|
|
123
124
|
|
|
124
125
|
def _get_machines_for_cloud_account(
|
|
125
126
|
self, teamspace_id: str, cloud_account_id: str, org_id: str
|
|
@@ -431,7 +432,7 @@ class JobApiV2:
|
|
|
431
432
|
if (spec.instance_name and spec.instance_name in possible_identifiers) or (
|
|
432
433
|
spec.instance_type and spec.instance_type in possible_identifiers
|
|
433
434
|
):
|
|
434
|
-
return Machine.
|
|
435
|
+
return Machine._from_accelerator(accelerator)
|
|
435
436
|
|
|
436
437
|
return Machine.from_str(spec.instance_name or spec.instance_type)
|
|
437
438
|
|
lightning_sdk/api/mmt_api.py
CHANGED
|
@@ -265,7 +265,7 @@ class MMTApiV2:
|
|
|
265
265
|
if (spec.instance_name and spec.instance_name in possible_identifiers) or (
|
|
266
266
|
spec.instance_type and spec.instance_type in possible_identifiers
|
|
267
267
|
):
|
|
268
|
-
return Machine.
|
|
268
|
+
return Machine._from_accelerator(accelerator)
|
|
269
269
|
|
|
270
270
|
return Machine.from_str(spec.instance_name or spec.instance_type)
|
|
271
271
|
|
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -426,7 +426,7 @@ class StudioApi:
|
|
|
426
426
|
accelerator.slug_multi_cloud,
|
|
427
427
|
accelerator.instance_id,
|
|
428
428
|
):
|
|
429
|
-
return Machine.
|
|
429
|
+
return Machine._from_accelerator(accelerator)
|
|
430
430
|
|
|
431
431
|
return Machine.from_str(response.compute_config.name)
|
|
432
432
|
|
|
@@ -28,6 +28,7 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
28
28
|
V1Assistant,
|
|
29
29
|
V1CloudSpace,
|
|
30
30
|
V1ClusterAccelerator,
|
|
31
|
+
V1EfsConfig,
|
|
31
32
|
V1Endpoint,
|
|
32
33
|
V1ExternalCluster,
|
|
33
34
|
V1GCSFolderDataConnection,
|
|
@@ -515,3 +516,20 @@ class TeamspaceApi:
|
|
|
515
516
|
create_request.gcs_folder = V1GCSFolderDataConnection()
|
|
516
517
|
|
|
517
518
|
self._client.data_connection_service_create_data_connection(create_request, teamspace_id)
|
|
519
|
+
|
|
520
|
+
def new_connection(
|
|
521
|
+
self, teamspace_id: str, name: str, source: str, cluster: V1ExternalCluster, writable: bool, region: str
|
|
522
|
+
) -> None:
|
|
523
|
+
create_request = Create(
|
|
524
|
+
name=name,
|
|
525
|
+
create_resources=False,
|
|
526
|
+
force=True,
|
|
527
|
+
writable=writable,
|
|
528
|
+
cluster_id=cluster.id,
|
|
529
|
+
access_cluster_ids=[cluster.id],
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# TODO: Add support for other connection types
|
|
533
|
+
create_request.efs = V1EfsConfig(file_system_id=source, region=region)
|
|
534
|
+
|
|
535
|
+
self._client.data_connection_service_create_data_connection(create_request, teamspace_id)
|
lightning_sdk/api/user_api.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from typing import Dict, List, Union
|
|
2
|
+
from typing import Dict, List, Optional, Union
|
|
3
3
|
|
|
4
4
|
from lightning_sdk.lightning_cloud.login import Auth
|
|
5
5
|
from lightning_sdk.lightning_cloud.openapi import (
|
|
@@ -61,8 +61,14 @@ class UserApi:
|
|
|
61
61
|
def _get_all_teamspace_memberships(
|
|
62
62
|
self,
|
|
63
63
|
user_id: str, # todo: this is unused, but still required
|
|
64
|
+
org_id: Optional[str] = None,
|
|
64
65
|
) -> List[V1Membership]:
|
|
65
|
-
|
|
66
|
+
kwargs: Dict[str, Union[bool, str]] = {"filter_by_user_id": True}
|
|
67
|
+
|
|
68
|
+
if org_id is not None:
|
|
69
|
+
kwargs["organization_id"] = org_id
|
|
70
|
+
|
|
71
|
+
return self._client.projects_service_list_memberships(**kwargs).memberships
|
|
66
72
|
|
|
67
73
|
def _get_authed_user_name(self) -> str:
|
|
68
74
|
"""Gets the currently logged-in user."""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from typing import Dict, List, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from simple_term_menu import TerminalMenu
|
|
7
|
+
|
|
8
|
+
from lightning_sdk.cli.legacy.exceptions import StudioCliError
|
|
9
|
+
from lightning_sdk.organization import Organization
|
|
10
|
+
from lightning_sdk.owner import Owner
|
|
11
|
+
from lightning_sdk.user import User
|
|
12
|
+
from lightning_sdk.utils.resolve import ApiException, _get_authed_user, _resolve_org, _resolve_user
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _OwnerMenuType(TypedDict):
|
|
16
|
+
name: str
|
|
17
|
+
is_org: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OwnerMenu:
|
|
21
|
+
"""This class is used to select a teamspace owner (org/user) from a list of possible owners.
|
|
22
|
+
|
|
23
|
+
It can be used to select an owner from a list of possible owners, or to resolve an owner from a name.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def _get_owner_from_interactive_menu(self, possible_owners: Dict[str, _OwnerMenuType]) -> _OwnerMenuType:
|
|
27
|
+
owner_ids = sorted(possible_owners.keys())
|
|
28
|
+
terminal_menu = self._prepare_terminal_menu_owners([possible_owners[k] for k in owner_ids])
|
|
29
|
+
terminal_menu.show()
|
|
30
|
+
|
|
31
|
+
selected_id = owner_ids[terminal_menu.chosen_menu_index]
|
|
32
|
+
return possible_owners[selected_id]
|
|
33
|
+
|
|
34
|
+
def _get_owner_from_name(self, owner: str, possible_owners: Dict[str, _OwnerMenuType]) -> _OwnerMenuType:
|
|
35
|
+
for _, ts in possible_owners.items():
|
|
36
|
+
if ts["name"]:
|
|
37
|
+
return ts
|
|
38
|
+
|
|
39
|
+
click.echo(f"Could not find Owner {owner}, please select it from the list:")
|
|
40
|
+
return self._get_owner_from_interactive_menu(possible_owners)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _prepare_terminal_menu_owners(
|
|
44
|
+
possible_owners: List[_OwnerMenuType], title: Optional[str] = None
|
|
45
|
+
) -> TerminalMenu:
|
|
46
|
+
if title is None:
|
|
47
|
+
title = "Please select a Teamspace-Owner out of the following:"
|
|
48
|
+
|
|
49
|
+
return TerminalMenu(
|
|
50
|
+
[f"{to['name']} ({'Organization' if to['is_org'] else 'User'})" for to in possible_owners],
|
|
51
|
+
title=title,
|
|
52
|
+
clear_menu_on_exit=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _get_possible_owners(user: User) -> Dict[str, _OwnerMenuType]:
|
|
57
|
+
user_api = user._user_api
|
|
58
|
+
|
|
59
|
+
orgs = user_api._get_organizations_for_authed_user()
|
|
60
|
+
owners: Dict[str, _OwnerMenuType] = {user.id: {"name": user.name, "is_org": False}}
|
|
61
|
+
|
|
62
|
+
for org in orgs:
|
|
63
|
+
owners[org.id] = {"name": org.name, "is_org": True}
|
|
64
|
+
|
|
65
|
+
return owners
|
|
66
|
+
|
|
67
|
+
def __call__(self, owner: Optional[str] = None) -> Owner:
|
|
68
|
+
try:
|
|
69
|
+
# try to resolve the teamspace from the name, environment or config
|
|
70
|
+
resolved_owner = None
|
|
71
|
+
with suppress(ApiException, ValueError, RuntimeError):
|
|
72
|
+
resolved_owner = _resolve_org(owner)
|
|
73
|
+
|
|
74
|
+
if resolved_owner is not None:
|
|
75
|
+
return resolved_owner
|
|
76
|
+
|
|
77
|
+
with suppress(ApiException, ValueError, RuntimeError):
|
|
78
|
+
resolved_owner = _resolve_user(owner)
|
|
79
|
+
|
|
80
|
+
if resolved_owner is not None:
|
|
81
|
+
return resolved_owner
|
|
82
|
+
|
|
83
|
+
if os.environ.get("LIGHTNING_NON_INTERACTIVE", "0") == "1":
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"Owner selection is not supported in non-interactive mode. Please provide an owner name."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# if the owner is not resolved, try to get the owner from the interactive menu
|
|
89
|
+
# this could mean that either no owner was provided or the provided owner is not valid
|
|
90
|
+
user = _get_authed_user()
|
|
91
|
+
|
|
92
|
+
possible_owners = self._get_possible_owners(user)
|
|
93
|
+
if owner is None:
|
|
94
|
+
owner_dict = self._get_owner_from_interactive_menu(possible_owners=possible_owners)
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
owner_dict = self._get_owner_from_name(owner=owner, possible_owners=possible_owners)
|
|
98
|
+
|
|
99
|
+
if owner_dict.get("is_org", False):
|
|
100
|
+
return Organization(owner_dict.get("name", None))
|
|
101
|
+
|
|
102
|
+
return User(owner_dict.get("name", None))
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
raise KeyboardInterrupt from None
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise StudioCliError(
|
|
108
|
+
f"Could not find the given Teamspace-Owner {owner}. "
|
|
109
|
+
"Please contact Lightning AI directly to resolve this issue."
|
|
110
|
+
) 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
|
|
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(
|
|
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
|
|
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[
|
|
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,
|
|
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.
|
|
65
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
78
|
+
if self._owner is None:
|
|
79
|
+
# resolve owner if required
|
|
80
|
+
from lightning_sdk.cli.utils.owner_selection import OwnerMenu
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
user = _get_authed_user()
|
|
95
|
+
if resolved_teamspace is not None:
|
|
96
|
+
return resolved_teamspace
|
|
107
97
|
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
teamspace_name = self._get_teamspace_from_name(
|
|
114
114
|
teamspace=teamspace, possible_teamspaces=possible_teamspaces
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
-
return Teamspace(
|
|
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 {
|
|
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
|
lightning_sdk/machine.py
CHANGED
|
@@ -116,7 +116,7 @@ class Machine:
|
|
|
116
116
|
)
|
|
117
117
|
for m in possible_values:
|
|
118
118
|
for machine_id in [machine, *additional_machine_ids]:
|
|
119
|
-
if machine_id in (
|
|
119
|
+
if machine_id and machine_id in (
|
|
120
120
|
getattr(m, "name", None),
|
|
121
121
|
getattr(m, "instance_type", None),
|
|
122
122
|
getattr(m, "slug", None),
|
|
@@ -127,6 +127,21 @@ class Machine:
|
|
|
127
127
|
return cls(machine, *additional_machine_ids)
|
|
128
128
|
return cls(machine, machine, machine)
|
|
129
129
|
|
|
130
|
+
@classmethod
|
|
131
|
+
def _from_accelerator(cls, accelerator: Any) -> "Machine":
|
|
132
|
+
if accelerator.accelerator_type == "GPU":
|
|
133
|
+
accelerator_resources_count = accelerator.resources.gpu
|
|
134
|
+
else:
|
|
135
|
+
accelerator_resources_count = accelerator.resources.cpu
|
|
136
|
+
|
|
137
|
+
return Machine.from_str(
|
|
138
|
+
accelerator.slug_multi_cloud,
|
|
139
|
+
accelerator.slug,
|
|
140
|
+
accelerator.instance_id,
|
|
141
|
+
accelerator.secondary_instance_id,
|
|
142
|
+
f"lit-{accelerator.family.lower()}-{accelerator_resources_count}",
|
|
143
|
+
)
|
|
144
|
+
|
|
130
145
|
|
|
131
146
|
# CPU machines
|
|
132
147
|
# default CPU machines
|
lightning_sdk/teamspace.py
CHANGED
|
@@ -41,6 +41,17 @@ class FolderLocation(Enum):
|
|
|
41
41
|
return self.value
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class ConnectionType(Enum):
|
|
45
|
+
EFS = "EFS"
|
|
46
|
+
S3 = "S3"
|
|
47
|
+
GCS = "GCS"
|
|
48
|
+
FILESTORE = "FILESTORE"
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
"""Converts the FolderLocation to a str."""
|
|
52
|
+
return self.value
|
|
53
|
+
|
|
54
|
+
|
|
44
55
|
class Teamspace:
|
|
45
56
|
"""A teamspace is a collection of Studios, Clusters, Members and an associated Budget.
|
|
46
57
|
|
|
@@ -574,6 +585,49 @@ class Teamspace:
|
|
|
574
585
|
|
|
575
586
|
return
|
|
576
587
|
|
|
588
|
+
def new_connection(
|
|
589
|
+
self,
|
|
590
|
+
name: str,
|
|
591
|
+
source: str,
|
|
592
|
+
connection_type: ConnectionType,
|
|
593
|
+
writable: bool = True,
|
|
594
|
+
cloud_account: Optional[str] = None,
|
|
595
|
+
region: Optional[str] = None,
|
|
596
|
+
) -> None:
|
|
597
|
+
"""Add an existing data source to this Teamspace.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
name: the name under which your data will be available in this Teamspace
|
|
601
|
+
source: the source spec of your data. Format depends on the type of data to connect.
|
|
602
|
+
For EFS, this should be the filsystem id
|
|
603
|
+
connection_type: the kind of data to connect to this Teamspace
|
|
604
|
+
writable: whether to support write-back to this data source. If False, the data is connected as read-only
|
|
605
|
+
cloud_account: which cloud-account to connect to the data source to.
|
|
606
|
+
If not specified, will retrieve the cloud-account that matches the required provider type
|
|
607
|
+
starting with private cloud accounts and falling back to public if necessary.
|
|
608
|
+
region: which provider region this data is in. Required for some connection types only.
|
|
609
|
+
"""
|
|
610
|
+
provider_for_connection = self._cloud_account_api.get_cloud_provider_for_connection_type(connection_type)
|
|
611
|
+
|
|
612
|
+
if connection_type == ConnectionType.EFS and region is None:
|
|
613
|
+
raise ValueError("Region must be specified")
|
|
614
|
+
|
|
615
|
+
cloud_account = self._cloud_account_api.resolve_cloud_account(
|
|
616
|
+
self.id, cloud_account, provider_for_connection, None
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
cloud_accounts = self._cloud_account_api.list_cloud_accounts(self.id)
|
|
620
|
+
resolved_cloud_accounts = [
|
|
621
|
+
external_cloud for external_cloud in cloud_accounts if external_cloud.id == cloud_account
|
|
622
|
+
]
|
|
623
|
+
|
|
624
|
+
if len(resolved_cloud_accounts) == 0:
|
|
625
|
+
raise ValueError(f"Cloud account not found: {cloud_account}")
|
|
626
|
+
|
|
627
|
+
resolved_cloud_account = resolved_cloud_accounts[0]
|
|
628
|
+
|
|
629
|
+
self._teamspace_api.new_connection(self.id, name, source, resolved_cloud_account, writable, region or "")
|
|
630
|
+
|
|
577
631
|
|
|
578
632
|
def _list_files(path: Union[str, Path]) -> Tuple[List[Path], List[str]]:
|
|
579
633
|
"""List all folders in a directory and return them as a list and relative path."""
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
|
|
2
|
-
lightning_sdk/__init__.py,sha256=
|
|
2
|
+
lightning_sdk/__init__.py,sha256=i1LQ7zzjJrjKEzaMjFBS0esHYmtvvMuXySVY_i34Upg,1221
|
|
3
3
|
lightning_sdk/agents.py,sha256=ly6Ma1j0ZgGPFyvPvMN28JWiB9dATIstFa5XM8pMi6I,1577
|
|
4
4
|
lightning_sdk/ai_hub.py,sha256=iI1vNhgcz_Ff1c3rN1ogN7dK-r-HXRj6NMtS2cA14UA,6925
|
|
5
5
|
lightning_sdk/base_studio.py,sha256=_Pwwl37R9GRd7t-f2kO5aQXiLNrP4sUtUNht2ZkP8LE,3678
|
|
6
6
|
lightning_sdk/constants.py,sha256=ztl1PTUBULnqTf3DyKUSJaV_O20hNtUYT6XvAYIrmIk,749
|
|
7
7
|
lightning_sdk/helpers.py,sha256=KWMWnORHItIIA3PGn71YPs-7RjzGi8IXa2kQ5Qo4U8M,2459
|
|
8
8
|
lightning_sdk/lit_container.py,sha256=8ys49TXI9MO89jLTA7MwDrKrssTRARAIF9OVmolDLq0,5273
|
|
9
|
-
lightning_sdk/machine.py,sha256=
|
|
9
|
+
lightning_sdk/machine.py,sha256=WXBAkO42GdvL3rYykVTl5UUhpQpnbLThngzymijBInk,9462
|
|
10
10
|
lightning_sdk/models.py,sha256=ur1My3oivAe0p2tAYtOAGM_1tPBht3Oh-xLun-oUK08,7861
|
|
11
11
|
lightning_sdk/organization.py,sha256=jizjG4oW_oxkP2Qz4vL5cle4Cg12xB_vf4eT-bnvLP0,1352
|
|
12
12
|
lightning_sdk/owner.py,sha256=t5svD2it4C9pbSpVuG9WJL46CYi37JXNziwnXxhiU5U,1361
|
|
@@ -15,24 +15,24 @@ lightning_sdk/sandbox.py,sha256=_NvnWotEXW2rBiVFZZ4krKXxVjuAqfNh04qELSM0-Pg,5786
|
|
|
15
15
|
lightning_sdk/serve.py,sha256=uW7zLhQ3X90ifetpxzTb8FNxifv5vIs7qZlgfEjVKzk,11794
|
|
16
16
|
lightning_sdk/status.py,sha256=lLGAuSvXBoXQFEEsEYwdCi0RcSNatUn5OPjJVjDtoM0,386
|
|
17
17
|
lightning_sdk/studio.py,sha256=3BE44Ya-2HXmDkF58lVQmd7KVzzzPktqX1SV7K3nkfo,31438
|
|
18
|
-
lightning_sdk/teamspace.py,sha256=
|
|
18
|
+
lightning_sdk/teamspace.py,sha256=eMatrnUr6E-ok96HIHRhghT2x_yEj_ToA6chSgYddGk,26265
|
|
19
19
|
lightning_sdk/user.py,sha256=TSYh38rxoi7qKOfrK2JYh_Nknya2Kbz2ngDIY85fFOY,1778
|
|
20
20
|
lightning_sdk/api/__init__.py,sha256=xrp_RNECJGQtL5rZHF69WOzEuEIbWSLtjWAJAz4R5K4,500
|
|
21
21
|
lightning_sdk/api/agents_api.py,sha256=G47TbFo9kYqnBMqdw2RW-lfS1VAUBSXDmzs6fpIEMUs,4059
|
|
22
22
|
lightning_sdk/api/ai_hub_api.py,sha256=azqDZ-PzasVAcoQHno7k7OO_xFOHQ4NDozxF8jEh83Y,7864
|
|
23
23
|
lightning_sdk/api/base_studio_api.py,sha256=3R8tucZX2e9yKHBcY2rWFRP4dxqLrC6H75vdBDkH0ck,3617
|
|
24
|
-
lightning_sdk/api/cloud_account_api.py,sha256=
|
|
24
|
+
lightning_sdk/api/cloud_account_api.py,sha256=B9-Ixy6OESAH_5wn6gKJLpcazj_48eiFLRTpunuyzjc,9191
|
|
25
25
|
lightning_sdk/api/deployment_api.py,sha256=v2AfoTDkQ-1CBh75FOjFkRpf6yc3U_edDy43uYSn19I,24852
|
|
26
|
-
lightning_sdk/api/job_api.py,sha256=
|
|
26
|
+
lightning_sdk/api/job_api.py,sha256=4uaqLiEGRmvFk2chiOltbgAfEC7seeNTmsBENsaft88,16468
|
|
27
27
|
lightning_sdk/api/license_api.py,sha256=XV3RhefyPQDYjwY9AaBZe4rByZTEAnsvLDxcdm9q0Wo,2438
|
|
28
28
|
lightning_sdk/api/lit_container_api.py,sha256=jCJVwd-3MNjejL9FyvH89pzt-SeG3G8qCJD16iTMJAQ,11454
|
|
29
29
|
lightning_sdk/api/llm_api.py,sha256=8ouowlX-Ld0hRvueIS6fQqaEJLQMYskvhKV5-Z_A6gE,11886
|
|
30
|
-
lightning_sdk/api/mmt_api.py,sha256=
|
|
30
|
+
lightning_sdk/api/mmt_api.py,sha256=adVGNFpinnsE3lkY4QZhpWxq4ZQjhyC1XTmOhpBWXvk,10691
|
|
31
31
|
lightning_sdk/api/org_api.py,sha256=Ze3z_ATVrukobujV5YdC42DKj45Vuwl7X52q_Vr-o3U,803
|
|
32
32
|
lightning_sdk/api/pipeline_api.py,sha256=rJYp_FN7uUjC5xbc6K67l2eRSmVuOkijd5i8Nm5BF7I,4621
|
|
33
|
-
lightning_sdk/api/studio_api.py,sha256=
|
|
34
|
-
lightning_sdk/api/teamspace_api.py,sha256=
|
|
35
|
-
lightning_sdk/api/user_api.py,sha256=
|
|
33
|
+
lightning_sdk/api/studio_api.py,sha256=ZwyaRcjPiE2O5gZ3Ko5zuB_QtzzDLZI9vUF6k7Z9Hl0,39484
|
|
34
|
+
lightning_sdk/api/teamspace_api.py,sha256=UZ6Ka6TL7zggs5qhQB4wl47VRusVTMpth1-toGVWbjQ,19974
|
|
35
|
+
lightning_sdk/api/user_api.py,sha256=o9gZmtvqNfj4kdSpo2fyyRuFAP7bM5w7mx0Oj9__Ads,4702
|
|
36
36
|
lightning_sdk/api/utils.py,sha256=1NJgW4HCfzMqgnAqbMA7RRr2v3iM3KTuPIUQK5klDeQ,27127
|
|
37
37
|
lightning_sdk/cli/__init__.py,sha256=lksw08t_ZIuHOH47LCIqSVHeZ8cUXI2aJWHYhyujYHM,32
|
|
38
38
|
lightning_sdk/cli/entrypoint.py,sha256=85icG0fmUqXuFErkD2v_kOlqnr18veD8oxe7gPBWHE8,4556
|
|
@@ -83,11 +83,12 @@ lightning_sdk/cli/studio/switch.py,sha256=qhWhLvjk-BbwU8Br3rXPQIMVQLTUTq21f0v2P-
|
|
|
83
83
|
lightning_sdk/cli/utils/__init__.py,sha256=0gHdWY3bqrIyiFiEh_uSBuxWpOykCjqUc8fPEV0z3no,186
|
|
84
84
|
lightning_sdk/cli/utils/cloud_account_map.py,sha256=7oM7ns4ZJfkxxZhl-GMkxEkkinKbVkskx5dLcyEQiz4,414
|
|
85
85
|
lightning_sdk/cli/utils/coloring.py,sha256=YEb1LiYb6CekVfCRb83-npAmNgd-7c1LzXuNDo4GLSc,2415
|
|
86
|
+
lightning_sdk/cli/utils/owner_selection.py,sha256=PbKbFoGblxi60lF_kZxnlqxjCmI9r9b9iDZClHkJcpQ,4216
|
|
86
87
|
lightning_sdk/cli/utils/resolve.py,sha256=RWQ8MgXEJCdsoY50y8Pa9B-A6NA_SvxOqL5pVKfvSr8,917
|
|
87
88
|
lightning_sdk/cli/utils/richt_print.py,sha256=psSY65nLAQbxK_K0w93Qq9857aUUbm77FLS1sc3Oecg,1262
|
|
88
89
|
lightning_sdk/cli/utils/save_to_config.py,sha256=mMjjfA4Yv1e7MtE03iADTXcTSAuUL0Ws9OZObx6ufo0,1164
|
|
89
90
|
lightning_sdk/cli/utils/studio_selection.py,sha256=tjYTm4sY3NSQaa-6Ob9R0UQ60wIXiTNT2406BUJvJbU,3927
|
|
90
|
-
lightning_sdk/cli/utils/teamspace_selection.py,sha256=
|
|
91
|
+
lightning_sdk/cli/utils/teamspace_selection.py,sha256=n-FFfWLTV63SxoK6Fi2VQikBIfKYqMNTew9b9v-1UQk,4945
|
|
91
92
|
lightning_sdk/deployment/__init__.py,sha256=dXsa4psDzFYFklsq3JC-2V_L4FQjGZnQAf-ZiVlqG9c,545
|
|
92
93
|
lightning_sdk/deployment/deployment.py,sha256=7Sk4ORKmgMDk7Owpk_q-89ws8-Y9orX0jRZF99tNv1E,21775
|
|
93
94
|
lightning_sdk/job/__init__.py,sha256=1MxjQ6rHkyUHCypSW9RuXuVMVH11WiqhIXcU2LCFMwE,64
|
|
@@ -1177,9 +1178,9 @@ lightning_sdk/utils/enum.py,sha256=h2JRzqoBcSlUdanFHmkj_j5DleBHAu1esQYUsdNI-hU,4
|
|
|
1177
1178
|
lightning_sdk/utils/names.py,sha256=1EuXbIh7wldkDp1FG10oz9vIOyWrpGWeFFVy-DQBgzA,18162
|
|
1178
1179
|
lightning_sdk/utils/progress.py,sha256=IXfEcUF-rL5jIw0Hir6eSxN7VBZfR--1O2LaEhGAU70,12698
|
|
1179
1180
|
lightning_sdk/utils/resolve.py,sha256=4TyEnIgIrvkSvYk5i5PmcIogD_5Y9pBhiphRLfLMttc,10477
|
|
1180
|
-
lightning_sdk-2025.9.
|
|
1181
|
-
lightning_sdk-2025.9.
|
|
1182
|
-
lightning_sdk-2025.9.
|
|
1183
|
-
lightning_sdk-2025.9.
|
|
1184
|
-
lightning_sdk-2025.9.
|
|
1185
|
-
lightning_sdk-2025.9.
|
|
1181
|
+
lightning_sdk-2025.9.23.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
|
|
1182
|
+
lightning_sdk-2025.9.23.dist-info/METADATA,sha256=vBeooGy4iWhjSE6eAD-ZHrX1v5_0CXTqb5UeSsCb8WQ,4130
|
|
1183
|
+
lightning_sdk-2025.9.23.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
1184
|
+
lightning_sdk-2025.9.23.dist-info/entry_points.txt,sha256=OoZa4Fc8NMs6GSN0cdA1J8e6couzAcL82CbM1yo4f_M,122
|
|
1185
|
+
lightning_sdk-2025.9.23.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
|
|
1186
|
+
lightning_sdk-2025.9.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|