lightning-sdk 2025.9.29__py3-none-any.whl → 2025.10.8__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
@@ -9,7 +9,7 @@ from lightning_sdk.mmt import MMT
9
9
  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
- from lightning_sdk.studio import Studio
12
+ from lightning_sdk.studio import VM, Studio
13
13
  from lightning_sdk.teamspace import ConnectionType, FolderLocation, Teamspace
14
14
  from lightning_sdk.user import User
15
15
 
@@ -32,8 +32,9 @@ __all__ = [
32
32
  "Studio",
33
33
  "Teamspace",
34
34
  "User",
35
+ "VM",
35
36
  ]
36
37
 
37
- __version__ = "2025.09.29"
38
+ __version__ = "2025.10.08"
38
39
  _check_version_and_prompt_upgrade(__version__)
39
40
  _set_tqdm_envvars_noninteractive()
@@ -16,6 +16,8 @@ class BaseStudioInfo:
16
16
  name: str
17
17
  managed_id: str
18
18
  description: str
19
+ creator: str
20
+ enabled: bool
19
21
 
20
22
 
21
23
  class BaseStudio:
@@ -80,20 +82,29 @@ class BaseStudio:
80
82
  disabled=disabled,
81
83
  )
82
84
 
83
- def list(self, managed: bool = True) -> List[BaseStudioInfo]:
85
+ def list(self, managed: bool = True, include_disabled: bool = False) -> List[BaseStudioInfo]:
84
86
  """List all base studios in the organization.
85
87
 
88
+ Args:
89
+ managed: Whether to filter for managed base studios.
90
+ include_disabled: Whether to include disabled base studios in the results.
91
+
86
92
  Returns:
87
- List[V1CloudSpaceEnvironmentTemplate]: A list of base studio templates.
93
+ List[BaseStudioInfo]: A list of base studio templates.
88
94
  """
89
- result = []
90
- for template in self._base_studio_api.get_all_base_studios(self._org.id, managed).templates:
91
- result.append(
92
- BaseStudioInfo(
93
- id=template.id,
94
- name=template.name,
95
- managed_id=template.managed_id,
96
- description=template.description,
97
- ),
95
+ templates = self._base_studio_api.get_all_base_studios(self._org.id, managed).templates
96
+
97
+ return [
98
+ BaseStudioInfo(
99
+ id=template.id,
100
+ name=template.name,
101
+ managed_id=template.managed_id,
102
+ description=template.description,
103
+ creator="⚡ Lightning AI"
104
+ if template.managed_id
105
+ else UserApi()._get_user_by_id(template.user_id).username,
106
+ enabled=not template.disabled,
98
107
  )
99
- return result
108
+ for template in templates
109
+ if include_disabled or not template.disabled
110
+ ]
@@ -0,0 +1,10 @@
1
+ """Base Studio CLI commands."""
2
+
3
+ import click
4
+
5
+
6
+ def register_commands(group: click.Group) -> None:
7
+ """Register base studio commands with the given group."""
8
+ from lightning_sdk.cli.base_studio.list import list_base_studios
9
+
10
+ group.add_command(list_base_studios)
@@ -0,0 +1,45 @@
1
+ """Base Studio list command."""
2
+
3
+ import click
4
+ from rich.table import Table
5
+
6
+ from lightning_sdk.base_studio import BaseStudio
7
+ from lightning_sdk.cli.utils.richt_print import rich_to_str
8
+
9
+
10
+ @click.command("list")
11
+ @click.option("--include-disabled", help="Include disabled Base Studios in the list.", is_flag=True)
12
+ def list_base_studios(include_disabled: bool) -> None:
13
+ """List Base Studios in an org.
14
+
15
+ Example:
16
+ lightning base-studio list
17
+
18
+ """
19
+ return list_impl(include_disabled=include_disabled)
20
+
21
+
22
+ def list_impl(include_disabled: bool) -> None:
23
+ base_studio_cls = BaseStudio()
24
+ base_studios = base_studio_cls.list(include_disabled=include_disabled) + base_studio_cls.list(
25
+ managed=False, include_disabled=include_disabled
26
+ )
27
+
28
+ table = Table(
29
+ pad_edge=True,
30
+ )
31
+
32
+ table.add_column("Name")
33
+ table.add_column("Description")
34
+ table.add_column("Creator")
35
+ table.add_column("Enabled")
36
+
37
+ for base_studio in base_studios:
38
+ table.add_row(
39
+ base_studio.name.lower().replace(" ", "-"),
40
+ base_studio.description or "",
41
+ base_studio.creator,
42
+ "Yes" if base_studio.enabled else "No",
43
+ )
44
+
45
+ click.echo(rich_to_str(table), color=True)
@@ -17,6 +17,7 @@ from lightning_sdk.api.studio_api import _cloud_url
17
17
 
18
18
  # Import legacy groups directly from groups.py
19
19
  from lightning_sdk.cli.groups import (
20
+ base_studio,
20
21
  config,
21
22
  # job,
22
23
  # mmt,
@@ -85,6 +86,7 @@ main_cli.add_command(config)
85
86
  # main_cli.add_command(mmt)
86
87
  main_cli.add_command(studio)
87
88
  main_cli.add_command(vm)
89
+ main_cli.add_command(base_studio)
88
90
  if os.environ.get("LIGHTNING_EXPERIMENTAL_CLI_ONLY", "0") != "1":
89
91
  #### LEGACY COMMANDS ####
90
92
  # these commands are currently supported for backwards compatibility, but will potentially be removed in the future.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import click
4
4
 
5
+ from lightning_sdk.cli.base_studio import register_commands as register_base_studio_commands
5
6
  from lightning_sdk.cli.config import register_commands as register_config_commands
6
7
  from lightning_sdk.cli.job import register_commands as register_job_commands
7
8
  from lightning_sdk.cli.mmt import register_commands as register_mmt_commands
@@ -34,9 +35,15 @@ def vm() -> None:
34
35
  """Manage Lightning AI VMs."""
35
36
 
36
37
 
38
+ @click.group(name="base-studio")
39
+ def base_studio() -> None:
40
+ """Manage Lightning AI Base Studios."""
41
+
42
+
37
43
  # Register config commands with the main config group
38
44
  register_job_commands(job)
39
45
  register_mmt_commands(mmt)
40
46
  register_studio_commands(studio)
41
47
  register_config_commands(config)
42
48
  register_vm_commands(vm)
49
+ register_base_studio_commands(base_studio)
@@ -14,7 +14,7 @@ from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
14
14
  from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
15
15
  from lightning_sdk.models import download_model
16
16
  from lightning_sdk.studio import Studio
17
- from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
17
+ from lightning_sdk.utils.resolve import _get_authed_user
18
18
 
19
19
 
20
20
  def _expand_remote_path(path: str) -> str:
@@ -242,7 +242,7 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
242
242
  and (re.match(studio_name, st["name"]) or studio_name in st["name"]),
243
243
  possible_studios,
244
244
  )
245
- if not possible_studios:
245
+ if not len(possible_studios):
246
246
  raise ValueError(
247
247
  f"Could not find Studio like '{studio}', please consider update your filtering pattern."
248
248
  )
@@ -260,8 +260,7 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
260
260
  "Please contact Lightning AI directly to resolve this issue."
261
261
  ) from e
262
262
 
263
- with skip_studio_init():
264
- return Studio(**selected_studio)
263
+ return Studio(**selected_studio)
265
264
 
266
265
 
267
266
  @download.command(name="licenses")
@@ -20,7 +20,7 @@ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
20
20
  from lightning_sdk.constants import _LIGHTNING_DEBUG
21
21
  from lightning_sdk.models import upload_model as _upload_model
22
22
  from lightning_sdk.studio import Studio
23
- from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
23
+ from lightning_sdk.utils.resolve import _get_authed_user
24
24
 
25
25
  _STUDIO_UPLOAD_STATUS_PATH = "~/.lightning/studios/uploads"
26
26
 
@@ -285,8 +285,7 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
285
285
  "Please contact Lightning AI directly to resolve this issue."
286
286
  ) from e
287
287
 
288
- with skip_studio_init():
289
- return Studio(**selected_studio)
288
+ return Studio(**selected_studio)
290
289
 
291
290
 
292
291
  def _print_docker_push(lines: Generator, console: Console, progress: Progress, push_task: rich.progress.TaskID) -> None:
@@ -5,6 +5,7 @@ import click
5
5
 
6
6
  def register_commands(group: click.Group) -> None:
7
7
  """Register studio commands with the given group."""
8
+ from lightning_sdk.cli.studio.connect import connect_studio
8
9
  from lightning_sdk.cli.studio.create import create_studio
9
10
  from lightning_sdk.cli.studio.delete import delete_studio
10
11
  from lightning_sdk.cli.studio.list import list_studios
@@ -20,3 +21,4 @@ def register_commands(group: click.Group) -> None:
20
21
  group.add_command(start_studio)
21
22
  group.add_command(stop_studio)
22
23
  group.add_command(switch_studio)
24
+ group.add_command(connect_studio)
@@ -0,0 +1,189 @@
1
+ """Studio connect command."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from typing import Dict, Optional, Set
6
+
7
+ import click
8
+
9
+ from lightning_sdk.base_studio import BaseStudio
10
+ from lightning_sdk.cli.utils.richt_print import studio_name_link
11
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config, save_teamspace_to_config
12
+ from lightning_sdk.cli.utils.ssh_connection import configure_ssh_internal
13
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
14
+ from lightning_sdk.lightning_cloud.openapi.rest import ApiException
15
+ from lightning_sdk.machine import CloudProvider, Machine
16
+ from lightning_sdk.studio import Studio
17
+ from lightning_sdk.utils.names import random_unique_name
18
+
19
+ DEFAULT_MACHINE = "CPU"
20
+
21
+
22
+ def _split_gpus_spec(gpus: str) -> tuple[str, int]:
23
+ machine_name, machine_val = gpus.split(":", 1)
24
+ machine_name = machine_name.strip()
25
+ machine_val = machine_val.strip()
26
+
27
+ if not machine_val.isdigit() or int(machine_val) <= 0:
28
+ raise ValueError(f"Invalid GPU count '{machine_val}'. Must be a positive integer.")
29
+
30
+ machine_num = int(machine_val)
31
+ return machine_name, machine_num
32
+
33
+
34
+ def _construct_available_gpus(machine_options: Dict[str, str]) -> Set[str]:
35
+ # returns available gpus:count
36
+ available_gpus = set()
37
+ for v in machine_options.values():
38
+ if "_X_" in v:
39
+ gpu_type_num = v.replace("_X_", ":")
40
+ available_gpus.add(gpu_type_num)
41
+ else:
42
+ available_gpus.add(v)
43
+ return available_gpus
44
+
45
+
46
+ def _get_machine_from_gpus(gpus: str) -> Machine:
47
+ machine_name = gpus
48
+ machine_num = 1
49
+
50
+ if ":" in gpus:
51
+ machine_name, machine_num = _split_gpus_spec(gpus)
52
+
53
+ machine_options = {
54
+ m.name.lower(): m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli
55
+ }
56
+
57
+ if machine_num == 1:
58
+ # e.g. gpus=L4 or gpus=L4:1
59
+ gpu_key = machine_name.lower()
60
+ try:
61
+ return machine_options[gpu_key]
62
+ except KeyError:
63
+ available = ", ".join(_construct_available_gpus(machine_options))
64
+ raise ValueError(f"Invalid GPU type '{machine_name}'. Available options: {available}") from None
65
+
66
+ # Else: e.g. gpus=L4:4
67
+ gpu_key = f"{machine_name.lower()}_x_{machine_num}"
68
+ try:
69
+ return machine_options[gpu_key]
70
+ except KeyError:
71
+ available = ", ".join(_construct_available_gpus(machine_options))
72
+ raise ValueError(f"Invalid GPU configuration '{gpus}'. Available options: {available}") from None
73
+
74
+
75
+ def _get_base_studio_id(studio_type: Optional[str]) -> Optional[str]:
76
+ base_studios = BaseStudio()
77
+ base_studios = base_studios.list()
78
+ template_id = None
79
+
80
+ if base_studios and len(base_studios):
81
+ # if not specified by user, use the first existing template studio
82
+ template_id = base_studios[0].id
83
+ # else, try to match the provided studio_type to base studio name
84
+ if studio_type:
85
+ normalized_studio_type = studio_type.lower().replace(" ", "-")
86
+ match = next(
87
+ (s for s in base_studios if s.name.lower().replace(" ", "-") == normalized_studio_type),
88
+ None,
89
+ )
90
+ if match:
91
+ template_id = match.id
92
+
93
+ return template_id
94
+
95
+
96
+ @click.command("connect")
97
+ @click.argument("name", required=False)
98
+ @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
99
+ @click.option(
100
+ "--cloud-provider",
101
+ help="The cloud provider to start the studio on. Defaults to teamspace default.",
102
+ type=click.Choice(m.name for m in list(CloudProvider)),
103
+ )
104
+ @click.option(
105
+ "--cloud-account",
106
+ help="The cloud account to create the studio on. Defaults to teamspace default.",
107
+ type=click.STRING,
108
+ )
109
+ @click.option(
110
+ "--machine",
111
+ help="The machine type to start the studio on. Defaults to CPU-4",
112
+ type=click.Choice(m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli),
113
+ )
114
+ @click.option(
115
+ "--gpus",
116
+ help="The number and type of GPUs to start the studio on (format: TYPE:COUNT, e.g. L4:4)",
117
+ type=click.STRING,
118
+ )
119
+ @click.option(
120
+ "--studio-type",
121
+ help="The base studio template name to use for creating the studio. "
122
+ "Must be lowercase and hyphenated (use '-' instead of spaces). "
123
+ "Run 'lightning base-studio list' to see all available templates. "
124
+ "Defaults to the first available template.",
125
+ type=click.STRING,
126
+ )
127
+ def connect_studio(
128
+ name: Optional[str] = None,
129
+ teamspace: Optional[str] = None,
130
+ cloud_provider: Optional[str] = None,
131
+ cloud_account: Optional[str] = None,
132
+ machine: Optional[str] = None,
133
+ gpus: Optional[str] = None,
134
+ studio_type: Optional[str] = None,
135
+ ) -> None:
136
+ """Connect to a Studio.
137
+
138
+ Example:
139
+ lightning studio connect
140
+ """
141
+ menu = TeamspacesMenu()
142
+
143
+ resolved_teamspace = menu(teamspace)
144
+ save_teamspace_to_config(resolved_teamspace, overwrite=False)
145
+
146
+ if cloud_provider is not None:
147
+ cloud_provider = CloudProvider(cloud_provider)
148
+
149
+ name = name or random_unique_name()
150
+
151
+ # check for available base studios
152
+ template_id = _get_base_studio_id(studio_type)
153
+
154
+ try:
155
+ studio = Studio(
156
+ name=name,
157
+ teamspace=resolved_teamspace,
158
+ create_ok=True,
159
+ cloud_provider=cloud_provider,
160
+ cloud_account=cloud_account,
161
+ studio_type=template_id,
162
+ )
163
+ except (RuntimeError, ValueError, ApiException):
164
+ raise ValueError(f"Could not create Studio: '{name}'") from None
165
+
166
+ click.echo(f"Connecting to Studio '{studio_name_link(studio)}' ...")
167
+
168
+ Studio.show_progress = True
169
+
170
+ if machine and gpus:
171
+ raise click.UsageError("Options --machine and --gpu are mutually exclusive. Provide only one.")
172
+ elif gpus:
173
+ machine = _get_machine_from_gpus(gpus.strip())
174
+ elif not machine:
175
+ machine = DEFAULT_MACHINE
176
+
177
+ save_studio_to_config(studio)
178
+ # by default, interruptible is False
179
+ studio.start(machine=machine, interruptible=False)
180
+
181
+ ssh_private_key_path = configure_ssh_internal()
182
+
183
+ ssh_option = "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR"
184
+ try:
185
+ ssh_command = f"ssh -i {ssh_private_key_path} {ssh_option} s_{studio._studio.id}@ssh.lightning.ai"
186
+ subprocess.run(ssh_command.split())
187
+ except Exception as ex:
188
+ print(f"Failed to establish SSH connection: {ex}")
189
+ sys.exit(1)
@@ -1,19 +1,14 @@
1
1
  """Studio SSH command."""
2
2
 
3
- import os
4
- import platform
5
3
  import subprocess
6
- import uuid
7
- from pathlib import Path
8
4
  from typing import List, Optional
9
5
 
10
6
  import click
11
7
 
12
8
  from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
9
+ from lightning_sdk.cli.utils.ssh_connection import configure_ssh_internal
13
10
  from lightning_sdk.cli.utils.studio_selection import StudiosMenu
14
11
  from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
15
- from lightning_sdk.lightning_cloud.login import Auth
16
- from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH
17
12
 
18
13
 
19
14
  @click.command("ssh")
@@ -43,9 +38,7 @@ def ssh_studio(name: Optional[str] = None, teamspace: Optional[str] = None, opti
43
38
 
44
39
 
45
40
  def ssh_impl(name: Optional[str], teamspace: Optional[str], option: Optional[List[str]], vm: bool) -> None:
46
- auth = Auth()
47
- auth.authenticate()
48
- ssh_private_key_path = _download_ssh_keys(auth.api_key, force_download=False)
41
+ ssh_private_key_path = configure_ssh_internal()
49
42
 
50
43
  menu = TeamspacesMenu()
51
44
  resolved_teamspace = menu(teamspace=teamspace)
@@ -63,53 +56,9 @@ def ssh_impl(name: Optional[str], teamspace: Optional[str], option: Optional[Lis
63
56
  subprocess.run(ssh_command.split())
64
57
  except Exception:
65
58
  # redownload the keys to be sure they are up to date
66
- _download_ssh_keys(auth.api_key, force_download=True)
59
+ ssh_private_key_path = configure_ssh_internal(force_download=True)
67
60
  try:
68
61
  subprocess.run(ssh_command.split())
69
62
  except Exception:
70
63
  # TODO: make this a generic CLI error
71
64
  raise RuntimeError("Failed to establish SSH connection") from None
72
-
73
-
74
- def _download_ssh_keys(
75
- api_key: str,
76
- force_download: bool = False,
77
- ssh_key_name: str = "lightning_rsa",
78
- ) -> None:
79
- """Download the SSH key for a User."""
80
- ssh_private_key_path = os.path.join(os.path.expanduser(os.path.dirname(_DEFAULT_CONFIG_FILE_PATH)), ssh_key_name)
81
-
82
- os.makedirs(os.path.dirname(ssh_private_key_path), exist_ok=True)
83
-
84
- if not os.path.isfile(ssh_private_key_path) or force_download:
85
- key_id = str(uuid.uuid4())
86
- _download_file(
87
- f"https://lightning.ai/setup/ssh-gen?t={api_key}&id={key_id}&machineName={platform.node()}",
88
- ssh_private_key_path,
89
- overwrite=True,
90
- chmod=0o600,
91
- )
92
- _download_file(
93
- f"https://lightning.ai/setup/ssh-public?t={api_key}&id={key_id}",
94
- ssh_private_key_path + ".pub",
95
- overwrite=True,
96
- )
97
-
98
- return ssh_private_key_path
99
-
100
-
101
- def _download_file(url: str, local_path: Path, overwrite: bool = True, chmod: Optional[int] = None) -> None:
102
- """Download a file from a URL."""
103
- import requests
104
-
105
- if os.path.isfile(local_path) and not overwrite:
106
- raise FileExistsError(f"The file {local_path} already exists and overwrite is set to False.")
107
-
108
- response = requests.get(url, stream=True)
109
- response.raise_for_status()
110
-
111
- with open(local_path, "wb") as file:
112
- for chunk in response.iter_content(chunk_size=8192):
113
- file.write(chunk)
114
- if chmod is not None:
115
- os.chmod(local_path, 0o600)
@@ -0,0 +1,59 @@
1
+ import os
2
+ import platform
3
+ import uuid
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from lightning_sdk.lightning_cloud.login import Auth
8
+ from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH
9
+
10
+
11
+ def configure_ssh_internal(force_download: bool = False) -> str:
12
+ """Internal function to configure SSH without Click decorators."""
13
+ auth = Auth()
14
+ auth.authenticate()
15
+ return download_ssh_keys(auth.api_key, force_download=force_download)
16
+
17
+
18
+ def download_ssh_keys(
19
+ api_key: str | None,
20
+ force_download: bool = False,
21
+ ssh_key_name: str = "lightning_rsa",
22
+ ) -> str:
23
+ """Download the SSH key for a User."""
24
+ ssh_private_key_path = os.path.join(os.path.expanduser(os.path.dirname(_DEFAULT_CONFIG_FILE_PATH)), ssh_key_name)
25
+
26
+ os.makedirs(os.path.dirname(ssh_private_key_path), exist_ok=True)
27
+
28
+ if not os.path.isfile(ssh_private_key_path) or force_download:
29
+ key_id = str(uuid.uuid4())
30
+ download_file(
31
+ f"https://lightning.ai/setup/ssh-gen?t={api_key}&id={key_id}&machineName={platform.node()}",
32
+ Path(ssh_private_key_path),
33
+ overwrite=True,
34
+ chmod=0o600,
35
+ )
36
+ download_file(
37
+ f"https://lightning.ai/setup/ssh-public?t={api_key}&id={key_id}",
38
+ Path(ssh_private_key_path + ".pub"),
39
+ overwrite=True,
40
+ )
41
+
42
+ return ssh_private_key_path
43
+
44
+
45
+ def download_file(url: str, local_path: Path, overwrite: bool = True, chmod: Optional[int] = None) -> None:
46
+ """Download a file from a URL."""
47
+ import requests
48
+
49
+ if os.path.isfile(local_path) and not overwrite:
50
+ raise FileExistsError(f"The file {local_path} already exists and overwrite is set to False.")
51
+
52
+ response = requests.get(url, stream=True)
53
+ response.raise_for_status()
54
+
55
+ with open(local_path, "wb") as file:
56
+ for chunk in response.iter_content(chunk_size=8192):
57
+ file.write(chunk)
58
+ if chmod is not None:
59
+ os.chmod(local_path, 0o600)
lightning_sdk/studio.py CHANGED
@@ -8,6 +8,7 @@ from tqdm.auto import tqdm
8
8
 
9
9
  from lightning_sdk.api.cloud_account_api import CloudAccountApi
10
10
  from lightning_sdk.api.studio_api import StudioApi
11
+ from lightning_sdk.base_studio import BaseStudio
11
12
  from lightning_sdk.constants import _LIGHTNING_DEBUG
12
13
  from lightning_sdk.lightning_cloud.openapi import V1ClusterType
13
14
  from lightning_sdk.machine import CloudProvider, Machine
@@ -51,6 +52,8 @@ class Studio:
51
52
  If not specified, falls backto the teamspace default cloud account.
52
53
  create_ok: whether the studio will be created if it does not yet exist. Defaults to True
53
54
  provider: the provider of the machine, the studio should be created on.
55
+ studio_type: Type of studio to create. Only effective during initial creation;
56
+ ignored for existing studios.
54
57
 
55
58
  Note:
56
59
  Since a teamspace can either be owned by an org or by a user directly,
@@ -78,6 +81,7 @@ class Studio:
78
81
  source: Optional[str] = None,
79
82
  disable_secrets: bool = False,
80
83
  provider: Optional[Union[CloudProvider, str]] = None, # deprecated in favor of cloud_provider
84
+ studio_type: Optional[str] = None, # for base studio templates
81
85
  ) -> None:
82
86
  self._studio_api = StudioApi()
83
87
  self._cloud_account_api = CloudAccountApi()
@@ -113,6 +117,25 @@ class Studio:
113
117
  default_cloud_account=self._teamspace.default_cloud_account,
114
118
  )
115
119
 
120
+ self._studio_type = None
121
+ if studio_type:
122
+ self._base_studio = BaseStudio()
123
+ self._available_base_studios = self._base_studio.list()
124
+ for bst in self._available_base_studios:
125
+ if (
126
+ bst.id == studio_type
127
+ or bst.name == studio_type
128
+ or bst.name.lower().replace(" ", "-") == studio_type
129
+ ):
130
+ self._studio_type = bst.id
131
+
132
+ if not self._studio_type:
133
+ raise ValueError(
134
+ f"Could not find studio type with ID or name '{studio_type}'. "
135
+ f"Available studio types: "
136
+ f"{[bst.name.lower().replace(' ', '-') for bst in self._available_base_studios]}"
137
+ )
138
+
116
139
  # Resolve studio name if not provided: explicit → env (LIGHTNING_CLOUD_SPACE_ID) → config defaults
117
140
  if name is None and not getattr(self._skip_init, "value", False):
118
141
  studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
@@ -150,6 +173,7 @@ class Studio:
150
173
  cloud_account=_cloud_account,
151
174
  source=source,
152
175
  disable_secrets=self._disable_secrets,
176
+ cloud_space_environment_template_id=self._studio_type,
153
177
  )
154
178
  else:
155
179
  raise e
@@ -264,10 +288,11 @@ class Studio:
264
288
  else:
265
289
  interruptible = self.teamspace.start_studios_on_interruptible
266
290
 
291
+ new_machine = machine
292
+ if not isinstance(machine, Machine):
293
+ new_machine = Machine.from_str(machine)
294
+
267
295
  if status == Status.Running:
268
- new_machine = machine
269
- if not isinstance(machine, Machine):
270
- new_machine = Machine.from_str(machine)
271
296
  if new_machine != self.machine:
272
297
  raise RuntimeError(
273
298
  f"Requested to start {self._cls_name} on {new_machine}, "
@@ -289,7 +314,11 @@ class Studio:
289
314
  with StudioProgressTracker("start", show_progress=True) as progress:
290
315
  # Start the studio without blocking
291
316
  self._studio_api.start_studio_async(
292
- self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
317
+ self._studio.id,
318
+ self._teamspace.id,
319
+ new_machine,
320
+ interruptible=interruptible,
321
+ max_runtime=max_runtime,
293
322
  )
294
323
 
295
324
  # Track progress through completion
@@ -299,7 +328,7 @@ class Studio:
299
328
  else:
300
329
  # Use the blocking version if no progress is needed
301
330
  self._studio_api.start_studio(
302
- self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
331
+ self._studio.id, self._teamspace.id, new_machine, interruptible=interruptible, max_runtime=max_runtime
303
332
  )
304
333
 
305
334
  self._setup()
@@ -17,30 +17,31 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_instance_st
17
17
  class StartupPhase(Enum):
18
18
  """Studio startup phase messages."""
19
19
 
20
- ALLOCATING_MACHINE = "Allocating machine from cloud provider..."
21
- STUDIO_STARTING = "Studio is starting up..."
22
- SETTING_UP_ENVIRONMENT = "Setting up Studio environment..."
23
- RESTORING_STATE = "Restoring Studio state..."
24
- FINALIZING_SETUP = "Finalizing Studio setup..."
25
- COMPLETED = "Studio started successfully"
20
+ STARTING_STUDIO = "Starting Studio..."
21
+ GETTING_MACHINE = "Getting a machine..."
26
22
 
23
+ SWITCHING_STUDIO = "Switching Studio..."
27
24
 
28
- def get_switching_progress_message(percentage: int, is_base_studio: bool, is_new_cloud_space: bool) -> str:
25
+ SETTING_UP_MACHINE = "Setting up machine..."
26
+ RESTORING_STUDIO = "Restoring Studio..."
27
+ PREPARING_STUDIO = "Preparing Studio..."
28
+ RESTORING_BASE_STUDIO = "Restoring Base Studio..."
29
+ SETTING_UP_BASE_STUDIO = "Setting up Base Studio..."
30
+ DONE = "Done"
31
+
32
+
33
+ def get_switching_progress_message(percentage: int, is_base_studio: bool) -> str:
29
34
  """Get progress message for switching studios."""
30
35
  percentage = max(0, min(100, round(percentage)))
31
36
 
32
37
  if percentage > 98:
33
- message = "Done"
38
+ message = StartupPhase.DONE.value
34
39
  elif percentage > 80:
35
- if is_new_cloud_space:
36
- message = "Setting up Base Studio..." if is_base_studio else "Preparing Studio..."
37
- else:
38
- message = "Restoring Studio..."
40
+ message = StartupPhase.RESTORING_BASE_STUDIO.value if is_base_studio else StartupPhase.RESTORING_STUDIO.value
39
41
  elif percentage > 60:
40
- message = "Setting up machine from cloud provider"
42
+ message = StartupPhase.SETTING_UP_MACHINE.value
41
43
  else:
42
- message = "Allocating machine from cloud provider"
43
-
44
+ message = StartupPhase.SWITCHING_STUDIO.value
44
45
  return f"({percentage}%) {message}"
45
46
 
46
47
 
@@ -110,15 +111,13 @@ class StudioProgressTracker:
110
111
  if self.progress:
111
112
  self.progress.stop()
112
113
 
113
- def update_progress(
114
- self, percentage: int, message: str = "", is_base_studio: bool = False, is_new_cloud_space: bool = False
115
- ) -> None:
114
+ def update_progress(self, percentage: int, message: str = "", is_base_studio: bool = False) -> None:
116
115
  """Update progress bar with current percentage and message."""
117
116
  if not self.progress or self.task_id is None:
118
117
  return
119
118
 
120
119
  if self.operation_type == "switch":
121
- display_message = get_switching_progress_message(percentage, is_base_studio, is_new_cloud_space)
120
+ display_message = get_switching_progress_message(percentage, is_base_studio)
122
121
  else:
123
122
  display_message = message or f"{self.operation_type.capitalize()}ing Studio..."
124
123
 
@@ -153,7 +152,7 @@ class StudioProgressTracker:
153
152
  message_stability_delay = 3.0 # Seconds to wait before changing message
154
153
 
155
154
  # Show initial progress immediately
156
- self.update_progress(5, StartupPhase.ALLOCATING_MACHINE.value)
155
+ self.update_progress(5, StartupPhase.STARTING_STUDIO.value)
157
156
 
158
157
  while True:
159
158
  try:
@@ -163,7 +162,7 @@ class StudioProgressTracker:
163
162
  # Default fallback progress based on time
164
163
  time_based_progress = min(95, int((elapsed / timeout) * 100))
165
164
  current_progress = max(last_progress, time_based_progress)
166
- current_message = StartupPhase.ALLOCATING_MACHINE.value
165
+ current_message = StartupPhase.STARTING_STUDIO.value
167
166
 
168
167
  # Check if we have detailed status information
169
168
  if hasattr(status, "in_use") and status.in_use:
@@ -178,7 +177,7 @@ class StudioProgressTracker:
178
177
  hasattr(startup_status, "top_up_restore_finished")
179
178
  and startup_status.top_up_restore_finished
180
179
  ):
181
- self.complete(StartupPhase.COMPLETED.value)
180
+ self.complete(StartupPhase.DONE.value)
182
181
  break
183
182
 
184
183
  # Check other phases in descending priority
@@ -186,14 +185,14 @@ class StudioProgressTracker:
186
185
  hasattr(startup_status, "initial_restore_finished")
187
186
  and startup_status.initial_restore_finished
188
187
  ):
189
- current_progress = max(current_progress, 85)
190
- current_message = StartupPhase.FINALIZING_SETUP.value
188
+ current_progress = max(current_progress, 80)
189
+ current_message = StartupPhase.PREPARING_STUDIO.value
191
190
  elif hasattr(startup_status, "container_ready") and startup_status.container_ready:
192
- current_progress = max(current_progress, 70)
193
- current_message = StartupPhase.RESTORING_STATE.value
194
- elif hasattr(startup_status, "machine_ready") and startup_status.machine_ready:
195
191
  current_progress = max(current_progress, 60)
196
- current_message = StartupPhase.SETTING_UP_ENVIRONMENT.value
192
+ current_message = StartupPhase.SETTING_UP_MACHINE.value
193
+ elif hasattr(startup_status, "machine_ready") and startup_status.machine_ready:
194
+ current_progress = max(current_progress, 30)
195
+ current_message = StartupPhase.GETTING_MACHINE.value
197
196
 
198
197
  # Check general phase information
199
198
  if hasattr(in_use, "phase") and in_use.phase:
@@ -201,7 +200,7 @@ class StudioProgressTracker:
201
200
 
202
201
  if phase == "CLOUD_SPACE_INSTANCE_STATE_RUNNING":
203
202
  current_progress = max(current_progress, 80)
204
- current_message = StartupPhase.STUDIO_STARTING.value
203
+ current_message = StartupPhase.SETTING_UP_MACHINE.value
205
204
  elif phase == "CLOUD_SPACE_INSTANCE_STATE_PENDING":
206
205
  # Track time in pending phase for smoother progress
207
206
  if "pending" not in phase_start_times:
@@ -211,7 +210,7 @@ class StudioProgressTracker:
211
210
  # Progress more smoothly through pending phase (10-60%)
212
211
  pending_progress = 10 + min(50, int((pending_elapsed / 60) * 50))
213
212
  current_progress = max(current_progress, pending_progress)
214
- current_message = StartupPhase.ALLOCATING_MACHINE.value
213
+ current_message = StartupPhase.GETTING_MACHINE.value
215
214
 
216
215
  # Check for requested machine status (pre-allocation)
217
216
  elif hasattr(status, "requested") and status.requested:
@@ -222,7 +221,7 @@ class StudioProgressTracker:
222
221
  # Progress through allocation phase (5-30%)
223
222
  allocation_progress = 5 + min(25, int((allocation_elapsed / 30) * 25))
224
223
  current_progress = max(current_progress, allocation_progress)
225
- current_message = StartupPhase.ALLOCATING_MACHINE.value
224
+ current_message = StartupPhase.GETTING_MACHINE.value
226
225
 
227
226
  # Ensure progress never decreases and moves smoothly
228
227
  if current_progress > last_progress:
@@ -262,12 +261,12 @@ class StudioProgressTracker:
262
261
 
263
262
  # Only update message if enough time has passed
264
263
  current_time = time.time()
265
- should_update_message = StartupPhase.ALLOCATING_MACHINE.value != self._last_message and (
264
+ should_update_message = StartupPhase.GETTING_MACHINE.value != self._last_message and (
266
265
  current_time - last_message_time >= message_stability_delay or last_message_time == 0
267
266
  )
268
267
 
269
268
  if should_update_message:
270
- self.update_progress(fallback_progress, StartupPhase.ALLOCATING_MACHINE.value)
269
+ self.update_progress(fallback_progress, StartupPhase.GETTING_MACHINE.value)
271
270
  last_message_time = current_time
272
271
  else:
273
272
  # Update progress but keep existing message
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 2025.9.29
3
+ Version: 2025.10.8
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,8 +1,8 @@
1
1
  docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
2
- lightning_sdk/__init__.py,sha256=HBcv4oavnBLmLhOoGKn7B71XVE1xFmSPUw0GM-UjU4w,1221
2
+ lightning_sdk/__init__.py,sha256=MLcIAWdrZoDrz0HJ7KhaLRbFrSg8Qs_XFS8p2vr2cwQ,1235
3
3
  lightning_sdk/agents.py,sha256=ly6Ma1j0ZgGPFyvPvMN28JWiB9dATIstFa5XM8pMi6I,1577
4
4
  lightning_sdk/ai_hub.py,sha256=iI1vNhgcz_Ff1c3rN1ogN7dK-r-HXRj6NMtS2cA14UA,6925
5
- lightning_sdk/base_studio.py,sha256=_Pwwl37R9GRd7t-f2kO5aQXiLNrP4sUtUNht2ZkP8LE,3678
5
+ lightning_sdk/base_studio.py,sha256=FIjHUiZtRf42m5YeqYYaJxcdgOtJVAJLZU42gzmldO8,4109
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
@@ -14,7 +14,7 @@ lightning_sdk/plugin.py,sha256=f3P5-xZY6x-MX0Fs2z_Q2erSxPSiHZARO0BVkCezHw4,15192
14
14
  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
- lightning_sdk/studio.py,sha256=_mPNeH_IBoWC-bwEiGXCeWizl1D2YXdWMUVUH3QyFsw,33145
17
+ lightning_sdk/studio.py,sha256=pij-pT-Cd11j5-LMAHxORLRP9a-Gcow87ZkTPcluu38,34350
18
18
  lightning_sdk/teamspace.py,sha256=iYaEaZH8N8Yj4XXj6M4zIH0tsjPVrAf_BzAAv6Jt9VI,26556
19
19
  lightning_sdk/user.py,sha256=TSYh38rxoi7qKOfrK2JYh_Nknya2Kbz2ngDIY85fFOY,1778
20
20
  lightning_sdk/api/__init__.py,sha256=xrp_RNECJGQtL5rZHF69WOzEuEIbWSLtjWAJAz4R5K4,500
@@ -35,8 +35,10 @@ lightning_sdk/api/teamspace_api.py,sha256=UZ6Ka6TL7zggs5qhQB4wl47VRusVTMpth1-toG
35
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
- lightning_sdk/cli/entrypoint.py,sha256=kvzeMKNuMoGSfsvg8v9QsBuMHR4U3pcWqBEHfvQQzhM,4595
39
- lightning_sdk/cli/groups.py,sha256=OPH6PHN82w5ERX00wzmstqU0NjEHmViVLAmy8-y8jQk,1131
38
+ lightning_sdk/cli/entrypoint.py,sha256=wMAyzfKRqZ6EZ73SxpuJDk3u8J3o-I5vF0VnESJh2QE,4646
39
+ lightning_sdk/cli/groups.py,sha256=3l9_U-8T613RJ2fHZy_-iJbtpq4fH8auWH7eFGSdZmg,1373
40
+ lightning_sdk/cli/base_studio/__init__.py,sha256=npBEmXWNVuiawm98TFVNHZf038kD4aW3gzVHsjbUPNI,272
41
+ lightning_sdk/cli/base_studio/list.py,sha256=DCXzSIY4IB0lHEstP0PA-8e7VqQQSUXL6OVv8VvwBKM,1244
40
42
  lightning_sdk/cli/config/__init__.py,sha256=_aJ7uZZOdjomZ-ABmz8Cu3fLmnSujnQK4hKAEKM4aF4,395
41
43
  lightning_sdk/cli/config/get.py,sha256=JXGRNVGxELS6Es2RESgStzTKnjg5rr-M6yOmx2ty9A8,1606
42
44
  lightning_sdk/cli/config/set.py,sha256=9p8q8nUWGWNcAcflQ3hS_tj-59jY7cmkzccuiZl92h4,3117
@@ -50,7 +52,7 @@ lightning_sdk/cli/legacy/connect.py,sha256=0qJ0yvykoPBkmCvvEbWKppt7wlUmRq3LNNGEs
50
52
  lightning_sdk/cli/legacy/create.py,sha256=ftp7cP8dqtO3h35mZUkpUjFW1tHwKxPQoUOPsabcy0M,3682
51
53
  lightning_sdk/cli/legacy/delete.py,sha256=IC04CNqJFIEfgl6Tlt4uEl3t81yZCb8VYLRwy3B8xiA,3816
52
54
  lightning_sdk/cli/legacy/docker_cli.py,sha256=EozLC4qnvHhgukmnu35itfI5n7v-abCpwKPkPy3eOV4,997
53
- lightning_sdk/cli/legacy/download.py,sha256=7ZOkqnAIEww12LM_cDO8hBhVbMYkChl82-vXHixcNwg,11357
55
+ lightning_sdk/cli/legacy/download.py,sha256=_Yaw1slwjE7ygDomjEJliXLTE-Gfj1d3zkH3daYVASk,11311
54
56
  lightning_sdk/cli/legacy/entrypoint.py,sha256=dk_BKvNzQMYREdAGTTdHF8nZWp_EuvCii-WOz4YFwrk,3784
55
57
  lightning_sdk/cli/legacy/exceptions.py,sha256=QUF3OMAMZwBikvlusimSHSBjb6ywvHpfAumJBEaodSw,169
56
58
  lightning_sdk/cli/legacy/generate.py,sha256=3tLaqALiuWG3AEIQtNHChMNpaJJU9zV5VYGshiEmKug,1582
@@ -66,17 +68,18 @@ lightning_sdk/cli/legacy/stop.py,sha256=jSiuxIunDnL3A3DQ1m8GZx01Qt4Zx5vIA3u7U81C
66
68
  lightning_sdk/cli/legacy/studios_menu.py,sha256=TA9rO6_fFHGMz0Nt4rJ6iV80X5pZE4xShrSiyXoU-oQ,4129
67
69
  lightning_sdk/cli/legacy/switch.py,sha256=G3y9BmP-0d_OKJub5fMZ0ddnGZkOFBtregMkx_KWxcw,1945
68
70
  lightning_sdk/cli/legacy/teamspace_menu.py,sha256=u5_lyB1aCD8KJIsOuxi0bvPi75McU5EbluMLyD_NBUw,4104
69
- lightning_sdk/cli/legacy/upload.py,sha256=7FTUnA4CvE7FaNTmNKhjmr-7Ly72QGvDQVqvp1EeQpY,13773
71
+ lightning_sdk/cli/legacy/upload.py,sha256=fFJct9F_1vLqTMLtSjuv4BN3AbUpeGebwv3ZXH0NiqI,13722
70
72
  lightning_sdk/cli/legacy/deploy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
73
  lightning_sdk/cli/legacy/deploy/_auth.py,sha256=wpwhkF2tmURlhEBaZ3QgOnHQELy3XLPt2d4LWb7JosY,7700
72
74
  lightning_sdk/cli/legacy/deploy/devbox.py,sha256=SdOtYdGefzldn-WCa6sz31zysyf2nP5D7O2x1DE9z_E,6839
73
75
  lightning_sdk/cli/legacy/deploy/serve.py,sha256=bxN04mHlArjHtxR_QkCM0S_RPmL3WIPsgnPgFadSXwI,15478
74
76
  lightning_sdk/cli/mmt/__init__.py,sha256=CLgr-ZHHLS6Db_JGqpxbn4G2pYrKi4Qn-uhi8e0kFNc,145
75
- lightning_sdk/cli/studio/__init__.py,sha256=Reb5a0iHJfNMxmnP30DAmYqpJ-YDQoJcgyVrD2cYDNo,823
77
+ lightning_sdk/cli/studio/__init__.py,sha256=jqUuEniYlf3fwJni-MGLII3oprpsBSQEFqzBJwPIxMk,925
78
+ lightning_sdk/cli/studio/connect.py,sha256=yb4Ga4lLjrg5C0sLPwAoEkkTpT7rv165AwqBOsYBIWc,6488
76
79
  lightning_sdk/cli/studio/create.py,sha256=kTxLtvTFjo-SgkdzOIngdXu_6yYLHwVbkfMhHjpIFkI,2505
77
80
  lightning_sdk/cli/studio/delete.py,sha256=Gi1KLCrSgjTiyoQe9NRvF4ZpsNyeM07KHnBxfdJg2FE,1483
78
81
  lightning_sdk/cli/studio/list.py,sha256=ieLiHRIfAb7E5e_r48BeTC03ib8RY6maeF7nkmrqaek,3301
79
- lightning_sdk/cli/studio/ssh.py,sha256=5CRw62p9rUb4BIL1WZP-Ehzcgfq_hVkY5ixTurxUKKo,3866
82
+ lightning_sdk/cli/studio/ssh.py,sha256=zHpVUe2QiFvM91Pj5kzNCnacavwrptxNsPpxyxpKLno,2194
80
83
  lightning_sdk/cli/studio/start.py,sha256=GHfV_1I1XwV8UHOWBP2WVfd1B2BHyw1lQ9HMv6Eomes,3236
81
84
  lightning_sdk/cli/studio/stop.py,sha256=hVXIyrv68rvrwIQgyEPUKt3Th8S1iipOVflopPnoVOY,1398
82
85
  lightning_sdk/cli/studio/switch.py,sha256=x6F1biDtfR0dk1t7X3NSkmfj5jkzGJe6-nvj3SonQlQ,2069
@@ -87,6 +90,7 @@ lightning_sdk/cli/utils/owner_selection.py,sha256=PbKbFoGblxi60lF_kZxnlqxjCmI9r9
87
90
  lightning_sdk/cli/utils/resolve.py,sha256=RWQ8MgXEJCdsoY50y8Pa9B-A6NA_SvxOqL5pVKfvSr8,917
88
91
  lightning_sdk/cli/utils/richt_print.py,sha256=psSY65nLAQbxK_K0w93Qq9857aUUbm77FLS1sc3Oecg,1262
89
92
  lightning_sdk/cli/utils/save_to_config.py,sha256=mMjjfA4Yv1e7MtE03iADTXcTSAuUL0Ws9OZObx6ufo0,1164
93
+ lightning_sdk/cli/utils/ssh_connection.py,sha256=cfppkWOLRLVSE-LhkENRANgdPNpT3U6beg8Lyx1xoxo,1984
90
94
  lightning_sdk/cli/utils/studio_selection.py,sha256=z0SMd82sOt_mfTiQZgVY27MxLkMAjM__SWhlOlfXJT0,4347
91
95
  lightning_sdk/cli/utils/teamspace_selection.py,sha256=n-FFfWLTV63SxoK6Fi2VQikBIfKYqMNTew9b9v-1UQk,4945
92
96
  lightning_sdk/cli/vm/__init__.py,sha256=tAJpluJVDOF3fUQyRwko5ZYIpqglczMyRv9Kz4OVaIk,711
@@ -1203,11 +1207,11 @@ lightning_sdk/utils/config.py,sha256=LfCAbko_f-1TgFoYD1KT68tZHEIgn3M2y1HUqTMruUo
1203
1207
  lightning_sdk/utils/dynamic.py,sha256=glUTO1JC9APtQ6Gr9SO02a3zr56-sPAXM5C3NrTpgyQ,1959
1204
1208
  lightning_sdk/utils/enum.py,sha256=h2JRzqoBcSlUdanFHmkj_j5DleBHAu1esQYUsdNI-hU,4106
1205
1209
  lightning_sdk/utils/names.py,sha256=1EuXbIh7wldkDp1FG10oz9vIOyWrpGWeFFVy-DQBgzA,18162
1206
- lightning_sdk/utils/progress.py,sha256=IXfEcUF-rL5jIw0Hir6eSxN7VBZfR--1O2LaEhGAU70,12698
1210
+ lightning_sdk/utils/progress.py,sha256=bLWw39fzq29PMWoFXaPIVfoS3Ug245950oWOFJ2ZaiU,12596
1207
1211
  lightning_sdk/utils/resolve.py,sha256=4TyEnIgIrvkSvYk5i5PmcIogD_5Y9pBhiphRLfLMttc,10477
1208
- lightning_sdk-2025.9.29.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
1209
- lightning_sdk-2025.9.29.dist-info/METADATA,sha256=CYT1gpwGdPiXJFMDKm0cmhiilbuIEgxvrFT0ndOwhuc,4130
1210
- lightning_sdk-2025.9.29.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1211
- lightning_sdk-2025.9.29.dist-info/entry_points.txt,sha256=OoZa4Fc8NMs6GSN0cdA1J8e6couzAcL82CbM1yo4f_M,122
1212
- lightning_sdk-2025.9.29.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
1213
- lightning_sdk-2025.9.29.dist-info/RECORD,,
1212
+ lightning_sdk-2025.10.8.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
1213
+ lightning_sdk-2025.10.8.dist-info/METADATA,sha256=N8b-kMsJedFH9daVWxNta30bWOraumpFMRESf8W-Sh4,4130
1214
+ lightning_sdk-2025.10.8.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1215
+ lightning_sdk-2025.10.8.dist-info/entry_points.txt,sha256=OoZa4Fc8NMs6GSN0cdA1J8e6couzAcL82CbM1yo4f_M,122
1216
+ lightning_sdk-2025.10.8.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
1217
+ lightning_sdk-2025.10.8.dist-info/RECORD,,