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 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.16"
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.list_global_cloud_accounts(teamspace_id=teamspace_id)
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!")
@@ -104,22 +104,23 @@ class JobApiV1:
104
104
  org_id=org_id,
105
105
  )
106
106
 
107
- identifier = None
107
+ identifiers = []
108
108
 
109
109
  if user_requested_compute_config and user_requested_compute_config.name:
110
- identifier = user_requested_compute_config.name
110
+ identifiers.append(user_requested_compute_config.name)
111
111
  else:
112
- identifier = spec.compute_config.instance_type
112
+ identifiers.append(spec.compute_config.instance_type)
113
113
 
114
114
  for accelerator in accelerators:
115
- if identifier in (
116
- accelerator.slug,
117
- accelerator.slug_multi_cloud,
118
- accelerator.instance_id,
119
- ):
120
- return Machine.from_str(accelerator.slug_multi_cloud)
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(identifier)
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.from_str(accelerator.slug_multi_cloud)
435
+ return Machine._from_accelerator(accelerator)
435
436
 
436
437
  return Machine.from_str(spec.instance_name or spec.instance_type)
437
438
 
@@ -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.from_str(accelerator.slug_multi_cloud)
268
+ return Machine._from_accelerator(accelerator)
269
269
 
270
270
  return Machine.from_str(spec.instance_name or spec.instance_type)
271
271
 
@@ -426,7 +426,7 @@ class StudioApi:
426
426
  accelerator.slug_multi_cloud,
427
427
  accelerator.instance_id,
428
428
  ):
429
- return Machine.from_str(accelerator.slug_multi_cloud)
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)
@@ -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
- return self._client.projects_service_list_memberships(filter_by_user_id=True).memberships
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 _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
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
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 2025.9.16
3
+ Version: 2025.9.23
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License
@@ -1,12 +1,12 @@
1
1
  docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
2
- lightning_sdk/__init__.py,sha256=1wyKGyM8HKl1zyLHQ97cOY7_jZefa5V89YaxP3-0s08,1183
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=FUZMGnvIXeXT-14WjA3ke98bG33tMN7v8K3kOLepAnk,8887
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=fjsbMw_fmAHLldAGT03R8Byp1znR6ZYAh9PXUAzzP8w,24016
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=pPeNUcl_7qE0BsrLfmvG0dH0rCJfHiSFexBs4UxWXBM,8732
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=7spFPyotlSQ0Krx6WymslnpSQaaeOmFkydvhtg6p0Ic,16410
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=hIBsGiJ2qn5UjcHDxP5WUyKGT_AIFfpSHrQVwg0afBw,10699
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=Aji8lke2jGeef-1lh6wV7EQ15iuiw1Cn_IuBNnEb8SA,39492
34
- lightning_sdk/api/teamspace_api.py,sha256=XPyUtpL5iyt9BpXw5WM-_hT2R3OzuC1GcumnI2ktVhQ,19333
35
- lightning_sdk/api/user_api.py,sha256=FQFgdZOopEvtFJcJT1mRRuStqKKYmg4iuVUYDSQfDVM,4514
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=ZWo_aZuEuLzBWRgQEykypEM1sE_deDKLDcCyLgFWaaQ,5188
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.16.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
1181
- lightning_sdk-2025.9.16.dist-info/METADATA,sha256=GR7YQsSApMYI943xTmY0qASB5yv4cUrFub3xlY0gwnI,4130
1182
- lightning_sdk-2025.9.16.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1183
- lightning_sdk-2025.9.16.dist-info/entry_points.txt,sha256=OoZa4Fc8NMs6GSN0cdA1J8e6couzAcL82CbM1yo4f_M,122
1184
- lightning_sdk-2025.9.16.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
1185
- lightning_sdk-2025.9.16.dist-info/RECORD,,
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,,