lightning-sdk 0.2.6__py3-none-any.whl → 0.2.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.
Files changed (42) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/ai_hub_api.py +1 -0
  3. lightning_sdk/api/lit_container_api.py +83 -30
  4. lightning_sdk/api/teamspace_api.py +8 -9
  5. lightning_sdk/api/utils.py +0 -1
  6. lightning_sdk/cli/docker.py +1 -1
  7. lightning_sdk/cli/download.py +10 -2
  8. lightning_sdk/cli/serve.py +51 -41
  9. lightning_sdk/cli/upload.py +41 -6
  10. lightning_sdk/lightning_cloud/openapi/__init__.py +8 -0
  11. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +202 -0
  12. lightning_sdk/lightning_cloud/openapi/models/__init__.py +8 -0
  13. lightning_sdk/lightning_cloud/openapi/models/alerts_config_billing.py +175 -0
  14. lightning_sdk/lightning_cloud/openapi/models/alerts_config_studios.py +149 -0
  15. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +53 -1
  16. lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +27 -1
  17. lightning_sdk/lightning_cloud/openapi/models/server_id_alerts_body.py +27 -1
  18. lightning_sdk/lightning_cloud/openapi/models/v1_alert_method.py +102 -0
  19. lightning_sdk/lightning_cloud/openapi/models/v1_alerts_config.py +149 -0
  20. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_cold_start_metrics.py +617 -0
  21. lightning_sdk/lightning_cloud/openapi/models/v1_conversation_response_chunk.py +29 -3
  22. lightning_sdk/lightning_cloud/openapi/models/v1_create_project_request.py +27 -1
  23. lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +29 -3
  24. lightning_sdk/lightning_cloud/openapi/models/v1_list_cloud_space_cold_start_metrics_response.py +123 -0
  25. lightning_sdk/lightning_cloud/openapi/models/v1_message.py +29 -3
  26. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +53 -1
  27. lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +27 -1
  28. lightning_sdk/lightning_cloud/openapi/models/v1_report_cloud_space_instance_stop_at_response.py +97 -0
  29. lightning_sdk/lightning_cloud/openapi/models/v1_server_alert.py +27 -1
  30. lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_phase.py +104 -0
  31. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -1
  32. lightning_sdk/lightning_cloud/rest_client.py +42 -44
  33. lightning_sdk/lit_container.py +19 -4
  34. lightning_sdk/models.py +1 -1
  35. lightning_sdk/serve.py +107 -41
  36. lightning_sdk/teamspace.py +32 -18
  37. {lightning_sdk-0.2.6.dist-info → lightning_sdk-0.2.8.dist-info}/METADATA +1 -1
  38. {lightning_sdk-0.2.6.dist-info → lightning_sdk-0.2.8.dist-info}/RECORD +42 -34
  39. {lightning_sdk-0.2.6.dist-info → lightning_sdk-0.2.8.dist-info}/LICENSE +0 -0
  40. {lightning_sdk-0.2.6.dist-info → lightning_sdk-0.2.8.dist-info}/WHEEL +0 -0
  41. {lightning_sdk-0.2.6.dist-info → lightning_sdk-0.2.8.dist-info}/entry_points.txt +0 -0
  42. {lightning_sdk-0.2.6.dist-info → lightning_sdk-0.2.8.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py CHANGED
@@ -31,6 +31,6 @@ __all__ = [
31
31
  "User",
32
32
  ]
33
33
 
34
- __version__ = "0.2.6"
34
+ __version__ = "0.2.8"
35
35
  _check_version_and_prompt_upgrade(__version__)
36
36
  _set_tqdm_envvars_noninteractive()
@@ -165,6 +165,7 @@ class AIHubApi:
165
165
  name=name,
166
166
  replicas=1,
167
167
  spec=template.spec_v2.job,
168
+ parent_template_id=template_id,
168
169
  ),
169
170
  )
170
171
 
@@ -1,12 +1,12 @@
1
1
  import inspect
2
2
  import time
3
- from typing import Any, Callable, Dict, Generator, Iterator, List
3
+ from typing import Any, Callable, Dict, Generator, Iterator, List, Optional
4
4
 
5
5
  import docker
6
6
  import requests
7
+ from rich.console import Console
7
8
 
8
9
  from lightning_sdk.api.utils import _get_registry_url
9
- from lightning_sdk.lightning_cloud.env import LIGHTNING_CLOUD_URL
10
10
  from lightning_sdk.lightning_cloud.openapi.models import V1DeleteLitRepositoryResponse
11
11
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
12
12
  from lightning_sdk.teamspace import Teamspace
@@ -21,6 +21,21 @@ class DockerPushError(Exception):
21
21
  pass
22
22
 
23
23
 
24
+ class DockerNotRunningError(Exception):
25
+ def __init__(self, message: str = "Failed to connect to Docker") -> None:
26
+ self.message = message
27
+ super().__init__(self.message)
28
+
29
+ def print_help(self) -> None:
30
+ console = Console()
31
+ console.print("[bold red]Docker Error: Failed to connect to Docker. Is it running?[/bold red]")
32
+ console.print("[yellow]Troubleshooting:[/yellow]")
33
+ console.print("1. Check if Docker daemon is running")
34
+ console.print("2. Verify your user has proper permissions")
35
+ console.print("3. Try restarting Docker service")
36
+ console.print("4. Read more here: https://docs.docker.com/engine/daemon/start/")
37
+
38
+
24
39
  def retry_on_lcr_auth_failure(func: Callable) -> Callable:
25
40
  def generator_wrapper(self: "LitContainerApi", *args: Any, **kwargs: Any) -> Callable:
26
41
  try:
@@ -54,9 +69,7 @@ class LitContainerApi:
54
69
  self._docker_client.ping()
55
70
  self._docker_auth_config = {}
56
71
  except docker.errors.DockerException:
57
- raise RuntimeError(
58
- "Failed to connect to Docker, follow these steps to start it: https://docs.docker.com/engine/daemon/start/"
59
- ) from None
72
+ raise DockerNotRunningError() from None
60
73
 
61
74
  def authenticate(self, reauth: bool = False) -> bool:
62
75
  resp = None
@@ -85,45 +98,73 @@ class LitContainerApi:
85
98
  print(f"Authentication error: {e} resp: {resp}")
86
99
  return False
87
100
 
88
- def list_containers(self, project_id: str) -> List:
89
- project = self._client.lit_registry_service_get_lit_project_registry(project_id)
101
+ def list_containers(self, project_id: str, cloud_account: Optional[str] = None) -> List:
102
+ """Lists containers of the project ID.
103
+
104
+ :param project_id: The non-human readable project ID used internally to identify projects.
105
+ :param cloud_account: The cluster ID of the cloud account. If None, will use the default cluster.
106
+ :return:
107
+ """
108
+ project = self._client.lit_registry_service_get_lit_project_registry(
109
+ project_id, cluster_id="" if cloud_account is None else cloud_account
110
+ ) # cloud account on the CLI is cluster_id
90
111
  return project.repositories
91
112
 
92
113
  def delete_container(self, project_id: str, container: str) -> V1DeleteLitRepositoryResponse:
114
+ """Deletes the container from LitCR. Garbage collection will come in and do the final sweep of the blob.
115
+
116
+ :param project_id: The non-human readable project ID used internally to identify projects.
117
+ :param container: The name of the docker container a user wants to push up, ie, nginx, vllm, etc
118
+ :return:
119
+ """
93
120
  try:
94
121
  return self._client.lit_registry_service_delete_lit_repository(project_id, container)
95
122
  except Exception as e:
96
123
  raise ValueError(f"Could not delete container {container} from project {project_id}: {e!s}") from e
97
124
 
98
125
  @retry_on_lcr_auth_failure
99
- def upload_container(self, container: str, teamspace: Teamspace, tag: str) -> Generator[dict, None, None]:
126
+ def upload_container(
127
+ self, container: str, teamspace: Teamspace, tag: str, cloud_account: str, platform: str
128
+ ) -> Generator[dict, None, None]:
129
+ """Upload container will push the container to LitCR.
130
+
131
+ It uses docker push API to interact with docker daemon which will then push the container to a storage
132
+ location defined by teamspace + cloud_account. Will retry pushes if not authenticated or if push errors happen
133
+
134
+ :param container: The name of the docker container a user wants to push up, ie, nginx, vllm, etc
135
+ :param teamspace: The teamspace he container is going to appear in. This is <OWNER_ID>/<TEAMSPACE_NAME>
136
+ :param tag: The container tag, default will latest.
137
+ :param cloud_account: If empty will be pushed to Lightning SaaS storage. Otherwise, this will be cluster_id.
138
+ Named cloud-account in the CLI options.
139
+ :param platform: If empty will be linux/amd64. This is important because our entire deployment infra runs on
140
+ linux/amd64. Will show user a warning otherwise.
141
+ :return: Generator[dict, None, dict]
142
+ """
100
143
  try:
101
- self._docker_client.images.get(container)
144
+ self._docker_client.images.get(f"{container}:{tag}")
102
145
  except docker.errors.ImageNotFound:
103
146
  try:
104
- self._docker_client.images.pull(container, tag)
105
- self._docker_client.images.get(container)
147
+ self._docker_client.images.pull(repository=container, tag=tag, platform=platform)
148
+ self._docker_client.images.get(f"{container}:{tag}")
149
+ except docker.errors.ImageNotFound as e:
150
+ raise ValueError(f"Container {container}:{tag} does not exist") from e
106
151
  except docker.errors.APIError as e:
107
152
  raise ValueError(f"Could not pull container {container}") from e
108
- except docker.errors.ImageNotFound as e:
109
- raise ValueError(f"Container {container} does not exist") from e
110
153
  except Exception as e:
111
- raise ValueError(f"Unable to upload {container}") from e
154
+ raise ValueError(f"Unable to upload {container}:{tag}") from e
112
155
 
113
156
  registry_url = _get_registry_url()
114
157
  container_basename = container.split("/")[-1]
115
- repository = f"{registry_url}/lit-container/{teamspace.owner.name}/{teamspace.name}/{container_basename}"
116
- tagged = self._docker_client.api.tag(container, repository, tag)
158
+ repository = (
159
+ f"{registry_url}/lit-container{f'-{cloud_account}' if cloud_account is not None else ''}/"
160
+ f"{teamspace.owner.name}/{teamspace.name}/{container_basename}"
161
+ )
162
+ tagged = self._docker_client.api.tag(f"{container}:{tag}", repository, tag)
117
163
  if not tagged:
118
- raise ValueError(f"Could not tag container {container} with {repository}:{tag}")
119
- yield from self._push_with_retry(repository)
120
- yield {
121
- "finish": True,
122
- "url": f"{LIGHTNING_CLOUD_URL}/{teamspace.owner.name}/{teamspace.name}/containers/{container_basename}",
123
- "repository": repository,
124
- }
125
-
126
- def _push_with_retry(self, repository: str, max_retries: int = 3) -> Iterator[Dict[str, Any]]:
164
+ raise ValueError(f"Could not tag container {container}:{tag} with {repository}:{tag}")
165
+ yield from self._push_with_retry(repository, tag=tag)
166
+
167
+ def _push_with_retry(self, repository: str, tag: str, max_retries: int = 3) -> Iterator[Dict[str, Any]]:
127
168
  def is_auth_error(error_msg: str) -> bool:
128
169
  auth_errors = ["unauthorized", "authentication required", "unauth"]
129
170
  return any(err in error_msg.lower() for err in auth_errors)
@@ -141,7 +182,7 @@ class LitContainerApi:
141
182
  time.sleep(2)
142
183
 
143
184
  lines = self._docker_client.api.push(
144
- repository, stream=True, decode=True, auth_config=self._docker_auth_config
185
+ repository, tag=tag, stream=True, decode=True, auth_config=self._docker_auth_config
145
186
  )
146
187
  for line in lines:
147
188
  if isinstance(line, dict) and "error" in line:
@@ -149,7 +190,7 @@ class LitContainerApi:
149
190
  if is_auth_error(error) or is_timeout_error(error):
150
191
  if attempt < max_retries - 1:
151
192
  break
152
- raise DockerPushError(f"Max retries reached: {error}")
193
+ raise DockerPushError(f"Max retries reached while pushing: {error}")
153
194
  raise DockerPushError(f"Push error: {error}")
154
195
  yield line
155
196
  else:
@@ -158,14 +199,26 @@ class LitContainerApi:
158
199
  except docker.errors.APIError as e:
159
200
  if (is_auth_error(str(e)) or is_timeout_error(str(e))) and attempt < max_retries - 1:
160
201
  continue
161
- raise DockerPushError(f"Push failed: {e}") from e
202
+ raise DockerPushError("Pushing the container failed") from e
162
203
 
163
204
  raise DockerPushError("Max push retries reached")
164
205
 
165
206
  @retry_on_lcr_auth_failure
166
- def download_container(self, container: str, teamspace: Teamspace, tag: str) -> Generator[str, None, None]:
207
+ def download_container(
208
+ self, container: str, teamspace: Teamspace, tag: str, cloud_account: Optional[str] = None
209
+ ) -> Generator[str, None, None]:
210
+ """Will download container from LitCR. Optionally from a BYOC cluster.
211
+
212
+ :param container: The name of the container to download
213
+ :param teamspace: The teamspace containing the container
214
+ :param tag: The tag of the container to download
215
+ :return: Generator yielding download status
216
+ """
167
217
  registry_url = _get_registry_url()
168
- repository = f"{registry_url}/lit-container/{teamspace.owner.name}/{teamspace.name}/{container}"
218
+ repository = (
219
+ f"{registry_url}/lit-container{f'-{cloud_account}' if cloud_account is not None else ''}/"
220
+ f"{teamspace.owner.name}/{teamspace.name}/{container}"
221
+ )
169
222
  try:
170
223
  self._docker_client.images.pull(repository, tag=tag, auth_config=self._docker_auth_config)
171
224
  except requests.exceptions.HTTPError as e:
@@ -215,16 +215,15 @@ class TeamspaceApi:
215
215
  version: str,
216
216
  local_path: Path,
217
217
  remote_path: str,
218
- cloud_account: str,
219
218
  teamspace_id: str,
220
219
  progress_bar: bool = True,
221
220
  ) -> None:
221
+ """Upload a file to the model store."""
222
222
  uploader = _ModelFileUploader(
223
223
  client=self._client,
224
224
  model_id=model_id,
225
225
  version=version,
226
226
  teamspace_id=teamspace_id,
227
- cloud_account=cloud_account,
228
227
  file_path=str(local_path),
229
228
  remote_path=str(remote_path),
230
229
  progress_bar=progress_bar,
@@ -235,20 +234,20 @@ class TeamspaceApi:
235
234
  self,
236
235
  model_id: str,
237
236
  version: str,
238
- root_path: Path,
239
- filepaths: List[Path],
240
- cloud_account: str,
237
+ file_paths: List[Path],
238
+ remote_paths: List[str],
241
239
  teamspace_id: str,
242
240
  progress_bar: bool = True,
243
241
  ) -> None:
244
- main_pbar = tqdm(total=len(filepaths), desc="Uploading files...", position=0) if progress_bar else None
245
- for filepath in filepaths:
242
+ """Upload files to the model store."""
243
+ main_pbar = tqdm(total=len(file_paths), desc="Uploading files...", position=0) if progress_bar else None
244
+ assert len(file_paths) == len(remote_paths), "File paths and remote paths must have the same length"
245
+ for filepath, remote_path in zip(file_paths, remote_paths):
246
246
  self.upload_model_file(
247
247
  model_id=model_id,
248
248
  version=version,
249
249
  local_path=filepath,
250
- remote_path=str(filepath.relative_to(root_path)),
251
- cloud_account=cloud_account,
250
+ remote_path=remote_path,
252
251
  teamspace_id=teamspace_id,
253
252
  progress_bar=progress_bar, # TODO: Global progress bar
254
253
  )
@@ -202,7 +202,6 @@ class _ModelFileUploader:
202
202
  model_id: str,
203
203
  version: str,
204
204
  teamspace_id: str,
205
- cloud_account: str,
206
205
  file_path: str,
207
206
  remote_path: str,
208
207
  progress_bar: bool,
@@ -19,4 +19,4 @@ def api(server_filename: str, port: int = 8000, gpu: bool = False, tag: str = "l
19
19
 
20
20
 
21
21
  def _api(server_filename: str, port: int = 8000, gpu: bool = False, tag: str = "litserve-model") -> str:
22
- return _LitServeDeployer().dockerize_api(server_filename=server_filename, port=port, gpu=gpu, tag=tag)
22
+ return _LitServeDeployer(None, None).dockerize_api(server_filename=server_filename, port=port, gpu=gpu, tag=tag)
@@ -143,7 +143,15 @@ def file(path: str = "", studio: Optional[str] = None, local_path: str = ".") ->
143
143
  @click.argument("container")
144
144
  @click.option("--teamspace", default=None, help="The name of the teamspace to download the container from")
145
145
  @click.option("--tag", default="latest", show_default=True, help="The tag of the container to download.")
146
- def download_container(container: str, teamspace: Optional[str] = None, tag: str = "latest") -> None:
146
+ @click.option(
147
+ "--cloud-account",
148
+ "--cloud_account", # The UI will present the above variant, using this as a secondary to be consistent w/ models
149
+ default=None,
150
+ help="The name of the cloud account to download the Container from.",
151
+ )
152
+ def download_container(
153
+ container: str, teamspace: Optional[str] = None, tag: str = "latest", cloud_account: Optional[str] = None
154
+ ) -> None:
147
155
  """Download a docker container from a teamspace.
148
156
 
149
157
  Example:
@@ -156,7 +164,7 @@ def download_container(container: str, teamspace: Optional[str] = None, tag: str
156
164
  resolved_teamspace = menu._resolve_teamspace(teamspace)
157
165
  with console.status("Downloading container..."):
158
166
  api = LitContainerApi()
159
- api.download_container(container, resolved_teamspace, tag)
167
+ api.download_container(container, resolved_teamspace, tag, cloud_account)
160
168
  console.print("Container downloaded successfully", style="green")
161
169
 
162
170
 
@@ -8,28 +8,35 @@ import click
8
8
  from rich.console import Console
9
9
  from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
10
10
  from rich.prompt import Confirm
11
- from rich.syntax import Syntax
12
11
 
13
12
  from lightning_sdk import Machine, Teamspace
14
- from lightning_sdk.api.deployment_api import DeploymentApi
15
13
  from lightning_sdk.api.lit_container_api import LitContainerApi
16
14
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
17
- from lightning_sdk.serve import _LitServeDeployer
15
+ from lightning_sdk.serve import _LitServeDeployer, authenticate
18
16
 
19
17
  _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
20
18
 
21
19
 
22
- @click.group("serve")
20
+ class _ServeGroup(click.Group):
21
+ def parse_args(self, ctx: click.Context, args: list) -> click.Group:
22
+ # Check if first arg is a file path and not a command name
23
+ if args and os.path.exists(args[0]) and args[0] not in self.commands:
24
+ # Insert the 'api' command before the file path
25
+ args.insert(0, "api")
26
+ return super().parse_args(ctx, args)
27
+
28
+
29
+ @click.group("serve", cls=_ServeGroup)
23
30
  def serve() -> None:
24
31
  """Serve a LitServe model.
25
32
 
26
33
  Example:
27
- lightning serve api server.py # serve locally
34
+ lightning serve server.py # serve locally
28
35
 
29
36
  Example:
30
- lightning serve api server.py --cloud --name litserve-api # deploy to the cloud
37
+ lightning serve server.py --cloud # deploy to the cloud
31
38
 
32
- You can deploy the API to the cloud by running `lightning serve api server.py --cloud`.
39
+ You can deploy the API to the cloud by running `lightning serve server.py --cloud`.
33
40
  This will build a docker container for the server.py script and deploy it to the Lightning AI platform.
34
41
  """
35
42
 
@@ -44,11 +51,11 @@ def serve() -> None:
44
51
  help="Generate a client for the model",
45
52
  )
46
53
  @click.option(
47
- "--cloud",
54
+ "--local",
48
55
  is_flag=True,
49
56
  default=False,
50
57
  flag_value=True,
51
- help="Deploy the model to the Lightning AI platform",
58
+ help="Run the model locally",
52
59
  )
53
60
  @click.option("--name", default=None, help="Name of the deployed API (e.g., 'classification-api', 'Llama-api')")
54
61
  @click.option(
@@ -109,7 +116,7 @@ def serve() -> None:
109
116
  def api(
110
117
  script_path: str,
111
118
  easy: bool,
112
- cloud: bool,
119
+ local: bool,
113
120
  name: Optional[str],
114
121
  non_interactive: bool,
115
122
  machine: str,
@@ -128,7 +135,7 @@ def api(
128
135
  return api_impl(
129
136
  script_path=script_path,
130
137
  easy=easy,
131
- cloud=cloud,
138
+ local=local,
132
139
  repository=name,
133
140
  non_interactive=non_interactive,
134
141
  machine=machine,
@@ -148,7 +155,7 @@ def api(
148
155
  def api_impl(
149
156
  script_path: Union[str, Path],
150
157
  easy: bool = False,
151
- cloud: bool = False,
158
+ local: bool = False,
152
159
  repository: [str] = None,
153
160
  tag: Optional[str] = None,
154
161
  non_interactive: bool = False,
@@ -178,7 +185,7 @@ def api_impl(
178
185
  timestr = datetime.now().strftime("%b-%d-%H_%M")
179
186
  repository = f"litserve-{timestr}".lower()
180
187
 
181
- if cloud:
188
+ if not local:
182
189
  repository = repository or "litserve-model"
183
190
  machine = Machine.from_str(machine)
184
191
  return _handle_cloud(
@@ -231,34 +238,21 @@ def _handle_cloud(
231
238
  ) -> None:
232
239
  deployment_name = os.path.basename(repository)
233
240
  tag = tag if tag else "latest"
241
+
242
+ if non_interactive:
243
+ console.print("[italic]non-interactive[/italic] mode enabled, skipping confirmation prompts", style="blue")
244
+
245
+ # Authenticate with LitServe affiliate
246
+ authenticate()
234
247
  if teamspace is None:
235
248
  menu = _TeamspacesMenu()
236
249
  resolved_teamspace = menu._resolve_teamspace(teamspace)
237
250
  else:
238
251
  resolved_teamspace = Teamspace(name=teamspace, org=org, user=user)
239
252
 
240
- if DeploymentApi().get_deployment_by_name(deployment_name, resolved_teamspace.id):
241
- syntax = Syntax(
242
- "from lightning_sdk import Deployment\n\n"
243
- f"deployment = Deployment('{deployment_name}', teamspace={resolved_teamspace.name})\n"
244
- "deployment.update(...)",
245
- "python",
246
- line_numbers=True,
247
- )
248
-
249
- console.print(
250
- f"\nDeployment with name [b]{deployment_name}[/b] already running. "
251
- "To update the deployment, use the Deployment API:\n",
252
- syntax,
253
- )
254
- return
255
-
256
253
  port = port or 8000
257
- ls_deployer = _LitServeDeployer()
254
+ ls_deployer = _LitServeDeployer(name=deployment_name, teamspace=resolved_teamspace)
258
255
  path = ls_deployer.dockerize_api(script_path, port=port, gpu=not machine.is_cpu(), tag=tag, print_success=False)
259
- console.clear()
260
- if non_interactive:
261
- console.print("[italic]non-interactive[/italic] mode enabled, skipping confirmation prompts", style="blue")
262
256
 
263
257
  console.print(f"\nPlease review the Dockerfile at [u]{path}[/u] and make sure it is correct.", style="bold")
264
258
  correct_dockerfile = True if non_interactive else Confirm.ask("Is the Dockerfile correct?", default=True)
@@ -266,10 +260,9 @@ def _handle_cloud(
266
260
  console.print("Please fix the Dockerfile and try again.", style="red")
267
261
  return
268
262
 
269
- ls_deployer.authenticate()
270
263
  # list containers to create the project if it doesn't exist
271
264
  lit_cr = LitContainerApi()
272
- lit_cr.list_containers(resolved_teamspace.id)
265
+ lit_cr.list_containers(resolved_teamspace.id, cloud_account=cloud_account)
273
266
 
274
267
  with Progress(
275
268
  SpinnerColumn(),
@@ -279,18 +272,35 @@ def _handle_cloud(
279
272
  transient=True,
280
273
  ) as progress:
281
274
  try:
282
- ls_deployer.build_container(path, repository, tag, console, progress)
283
- push_status = ls_deployer.push_container(repository, tag, resolved_teamspace, lit_cr, progress)
275
+ # Build the container
276
+ build_task = progress.add_task("Building Docker image", total=None)
277
+ for line in ls_deployer.build_container(path, repository, tag):
278
+ console.print(line.strip())
279
+ progress.update(build_task, advance=1)
280
+ progress.update(build_task, description="[green]Build completed![/green]", completed=1.0)
281
+ progress.remove_task(build_task)
282
+
283
+ # Push the container to the registry
284
+ console.print("\nPushing image to registry. It may take a while...", style="bold")
285
+ push_task = progress.add_task("Pushing to registry", total=None)
286
+ push_status = {}
287
+ for line in ls_deployer.push_container(
288
+ repository, tag, resolved_teamspace, lit_cr, cloud_account=cloud_account
289
+ ):
290
+ push_status = line
291
+ progress.update(push_task, advance=1)
292
+ if not ("Pushing" in line["status"] or "Waiting" in line["status"]):
293
+ console.print(line["status"])
294
+ progress.update(push_task, description="[green]Push completed![/green]")
284
295
  except Exception as e:
285
296
  console.print(f"❌ Deployment failed: {e}", style="red")
286
297
  return
287
298
  console.print(f"\n✅ Image pushed to {repository}:{tag}")
288
- console.print(f"🔗 You can access the container at: [i]{push_status.get('url')}[/i]")
289
- repository = push_status.get("repository")
299
+ image = push_status.get("image")
290
300
 
291
- deployment_status = ls_deployer._run_on_cloud(
301
+ deployment_status = ls_deployer.run_on_cloud(
292
302
  deployment_name=deployment_name,
293
- image=repository,
303
+ image=image,
294
304
  teamspace=resolved_teamspace,
295
305
  metric=None,
296
306
  machine=machine,
@@ -11,11 +11,12 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
11
11
  from simple_term_menu import TerminalMenu
12
12
  from tqdm import tqdm
13
13
 
14
- from lightning_sdk.api.lit_container_api import LCRAuthFailedError, LitContainerApi
14
+ from lightning_sdk.api.lit_container_api import DockerNotRunningError, LCRAuthFailedError, LitContainerApi
15
15
  from lightning_sdk.api.utils import _get_cloud_url
16
16
  from lightning_sdk.cli.exceptions import StudioCliError
17
17
  from lightning_sdk.cli.studios_menu import _StudiosMenu
18
18
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
19
+ from lightning_sdk.constants import _LIGHTNING_DEBUG
19
20
  from lightning_sdk.models import upload_model as _upload_model
20
21
  from lightning_sdk.studio import Studio
21
22
  from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
@@ -116,11 +117,28 @@ def file(path: str, studio: Optional[str] = None, remote_path: Optional[str] = N
116
117
  "If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
117
118
  ),
118
119
  )
119
- def upload_container(container: str, tag: str = "latest", teamspace: Optional[str] = None) -> None:
120
+ @click.option(
121
+ "--cloud-account",
122
+ "--cloud_account", # The UI will present the above variant, using this as a secondary to be consistent w/ models
123
+ default=None,
124
+ help="The name of the cloud account to store the Container in.",
125
+ )
126
+ @click.option(
127
+ "--platform",
128
+ "--platform",
129
+ default="linux/amd64",
130
+ help="This is the platform the container pulled and push to Lightning AI will run on.",
131
+ )
132
+ def upload_container(
133
+ container: str,
134
+ tag: str = "latest",
135
+ teamspace: Optional[str] = None,
136
+ cloud_account: Optional[str] = None,
137
+ platform: Optional[str] = "linux/amd64",
138
+ ) -> None:
120
139
  """Upload a container to Lightning AI's container registry."""
121
140
  menu = _TeamspacesMenu()
122
141
  teamspace = menu._resolve_teamspace(teamspace)
123
- api = LitContainerApi()
124
142
  console = Console()
125
143
  with Progress(
126
144
  SpinnerColumn(),
@@ -129,8 +147,13 @@ def upload_container(container: str, tag: str = "latest", teamspace: Optional[st
129
147
  console=console,
130
148
  transient=False,
131
149
  ) as progress:
132
- push_task = progress.add_task("Pushing Docker image", total=None)
133
150
  try:
151
+ if platform != "linux/amd64":
152
+ console.print(
153
+ "[yellow]Warning: The platform you selected (" + platform + ") may not deploy successfully[/yellow]"
154
+ )
155
+ api = LitContainerApi()
156
+ push_task = progress.add_task("Pushing Docker image", total=None)
134
157
  console.print("Authenticating with Lightning Container Registry...")
135
158
  try:
136
159
  api.authenticate()
@@ -139,15 +162,27 @@ def upload_container(container: str, tag: str = "latest", teamspace: Optional[st
139
162
  # let the push with retry take control of auth moving forward
140
163
  pass
141
164
 
142
- lines = api.upload_container(container, teamspace, tag)
165
+ lines = api.upload_container(container, teamspace, tag, cloud_account, platform)
143
166
  _print_docker_push(lines, console, progress, push_task)
167
+ except DockerNotRunningError as e:
168
+ e.print_help()
169
+ return
144
170
  except LCRAuthFailedError:
145
171
  console.print("Re-authenticating with Lightning Container Registry...")
146
172
  if not api.authenticate(reauth=True):
147
173
  raise StudioCliError("Failed to authenticate with Lightning Container Registry") from None
148
174
  console.print("Authenticated with Lightning Container Registry", style="green")
149
- lines = api.upload_container(container, teamspace, tag)
175
+ lines = api.upload_container(container, teamspace, tag, cloud_account, platform)
150
176
  _print_docker_push(lines, console, progress, push_task)
177
+ except Exception as e:
178
+ if _LIGHTNING_DEBUG:
179
+ print(e)
180
+ if e.__cause__:
181
+ print(e.__cause__)
182
+
183
+ progress.update(push_task, description=f"[bold red]Error: {e!s}[/]")
184
+ progress.stop()
185
+ return
151
186
  progress.update(push_task, description="[green]Container pushed![/green]")
152
187
 
153
188
 
@@ -66,6 +66,8 @@ from lightning_sdk.lightning_cloud.openapi.configuration import Configuration
66
66
  from lightning_sdk.lightning_cloud.openapi.models.affiliatelinks_id_body import AffiliatelinksIdBody
67
67
  from lightning_sdk.lightning_cloud.openapi.models.agentmanagedendpoints_id_body import AgentmanagedendpointsIdBody
68
68
  from lightning_sdk.lightning_cloud.openapi.models.agents_id_body import AgentsIdBody
69
+ from lightning_sdk.lightning_cloud.openapi.models.alerts_config_billing import AlertsConfigBilling
70
+ from lightning_sdk.lightning_cloud.openapi.models.alerts_config_studios import AlertsConfigStudios
69
71
  from lightning_sdk.lightning_cloud.openapi.models.app_id_works_body import AppIdWorksBody
70
72
  from lightning_sdk.lightning_cloud.openapi.models.appinstances_id_body import AppinstancesIdBody
71
73
  from lightning_sdk.lightning_cloud.openapi.models.approveautojoindomain_domain_body import ApproveautojoindomainDomainBody
@@ -233,6 +235,8 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_agent_job import V1AgentJob
233
235
  from lightning_sdk.lightning_cloud.openapi.models.v1_agent_job_artifact import V1AgentJobArtifact
234
236
  from lightning_sdk.lightning_cloud.openapi.models.v1_agent_upload_multipart_url import V1AgentUploadMultipartUrl
235
237
  from lightning_sdk.lightning_cloud.openapi.models.v1_agent_upload_part_response import V1AgentUploadPartResponse
238
+ from lightning_sdk.lightning_cloud.openapi.models.v1_alert_method import V1AlertMethod
239
+ from lightning_sdk.lightning_cloud.openapi.models.v1_alerts_config import V1AlertsConfig
236
240
  from lightning_sdk.lightning_cloud.openapi.models.v1_api_pricing_spec import V1ApiPricingSpec
237
241
  from lightning_sdk.lightning_cloud.openapi.models.v1_app_type import V1AppType
238
242
  from lightning_sdk.lightning_cloud.openapi.models.v1_append_logger_metrics_response import V1AppendLoggerMetricsResponse
@@ -272,6 +276,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_artifact_event
272
276
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_artifact_event_type import V1CloudSpaceArtifactEventType
273
277
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_code_version import V1CloudSpaceCodeVersion
274
278
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_code_version_status import V1CloudSpaceCodeVersionStatus
279
+ from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_cold_start_metrics import V1CloudSpaceColdStartMetrics
275
280
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_engagement_response import V1CloudSpaceEngagementResponse
276
281
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_environment_template import V1CloudSpaceEnvironmentTemplate
277
282
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_environment_template_config import V1CloudSpaceEnvironmentTemplateConfig
@@ -574,6 +579,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_list_agent_job_artifacts_re
574
579
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_agent_jobs_response import V1ListAgentJobsResponse
575
580
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_assistants_response import V1ListAssistantsResponse
576
581
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_cloud_space_apps_response import V1ListCloudSpaceAppsResponse
582
+ from lightning_sdk.lightning_cloud.openapi.models.v1_list_cloud_space_cold_start_metrics_response import V1ListCloudSpaceColdStartMetricsResponse
577
583
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_cloud_space_environment_templates_response import V1ListCloudSpaceEnvironmentTemplatesResponse
578
584
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_cloud_space_instances_response import V1ListCloudSpaceInstancesResponse
579
585
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_cloud_space_publications_response import V1ListCloudSpacePublicationsResponse
@@ -768,6 +774,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_refresh_request import V1Re
768
774
  from lightning_sdk.lightning_cloud.openapi.models.v1_refresh_response import V1RefreshResponse
769
775
  from lightning_sdk.lightning_cloud.openapi.models.v1_region_state import V1RegionState
770
776
  from lightning_sdk.lightning_cloud.openapi.models.v1_regional_load_balancer import V1RegionalLoadBalancer
777
+ from lightning_sdk.lightning_cloud.openapi.models.v1_report_cloud_space_instance_stop_at_response import V1ReportCloudSpaceInstanceStopAtResponse
771
778
  from lightning_sdk.lightning_cloud.openapi.models.v1_report_logs_activity_response import V1ReportLogsActivityResponse
772
779
  from lightning_sdk.lightning_cloud.openapi.models.v1_report_restart_timings_response import V1ReportRestartTimingsResponse
773
780
  from lightning_sdk.lightning_cloud.openapi.models.v1_request_cluster_access_request import V1RequestClusterAccessRequest
@@ -802,6 +809,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_secret import V1Secret
802
809
  from lightning_sdk.lightning_cloud.openapi.models.v1_secret_type import V1SecretType
803
810
  from lightning_sdk.lightning_cloud.openapi.models.v1_select import V1Select
804
811
  from lightning_sdk.lightning_cloud.openapi.models.v1_server_alert import V1ServerAlert
812
+ from lightning_sdk.lightning_cloud.openapi.models.v1_server_alert_phase import V1ServerAlertPhase
805
813
  from lightning_sdk.lightning_cloud.openapi.models.v1_server_alert_severity import V1ServerAlertSeverity
806
814
  from lightning_sdk.lightning_cloud.openapi.models.v1_server_alert_type import V1ServerAlertType
807
815
  from lightning_sdk.lightning_cloud.openapi.models.v1_server_check_in_response import V1ServerCheckInResponse