lightning-sdk 0.2.2__py3-none-any.whl → 0.2.4__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
@@ -31,6 +31,6 @@ __all__ = [
31
31
  "User",
32
32
  ]
33
33
 
34
- __version__ = "0.2.2"
34
+ __version__ = "0.2.4"
35
35
  _check_version_and_prompt_upgrade(__version__)
36
36
  _set_tqdm_envvars_noninteractive()
lightning_sdk/ai_hub.py CHANGED
@@ -3,6 +3,7 @@ from urllib.parse import quote
3
3
 
4
4
  from lightning_sdk.api import AIHubApi
5
5
  from lightning_sdk.api.utils import _get_cloud_url
6
+ from lightning_sdk.lightning_cloud.openapi.models import V1Deployment
6
7
  from lightning_sdk.user import User
7
8
  from lightning_sdk.utils.resolve import _resolve_teamspace
8
9
 
@@ -109,7 +110,7 @@ class AIHub:
109
110
  org: Optional[Union[str, "Organization"]] = None,
110
111
  user: Optional[Union[str, "User"]] = None,
111
112
  machine: Optional[Union[str, "Machine"]] = None,
112
- ) -> Dict[str, Union[str, bool]]:
113
+ ) -> V1Deployment:
113
114
  """Deploy an API from the AI Hub.
114
115
 
115
116
  Example:
@@ -133,7 +134,7 @@ class AIHub:
133
134
  machine: The machine to run the deployment on. Defaults to the first option set in the AI Hub template.
134
135
 
135
136
  Returns:
136
- A dictionary containing the name of the deployed API,
137
+ A V1Deployment object containing the name of the deployed API,
137
138
  the URL to access it, and whether it is interruptible.
138
139
 
139
140
  Raises:
@@ -169,16 +170,9 @@ class AIHub:
169
170
 
170
171
  print("Deployment available at:", url)
171
172
 
172
- return {
173
- "id": deployment.id,
174
- "name": deployment.name,
175
- "deployment_url": url,
176
- "api_endpoint": deployment.status.urls[0],
177
- "interruptible": deployment.spec.spot,
178
- "teamspace id": teamspace_id,
179
- }
173
+ return deployment
180
174
 
181
- def delete_deployment(self, deployment: Dict[str, Union[str, bool]]) -> None:
175
+ def delete_deployment(self, deployment: V1Deployment) -> None:
182
176
  """Delete a deployment from the AI Hub.
183
177
 
184
178
  Example:
@@ -188,9 +182,6 @@ class AIHub:
188
182
  hub.delete_deployment(deployment)
189
183
 
190
184
  Args:
191
- deployment: The deployment dictionary returned by the run method.
185
+ deployment: The deployment object returned by the run method.
192
186
  """
193
- if "teamspace id" not in deployment or "id" not in deployment:
194
- raise ValueError("Deployment dictionary must contain 'teamspace id' and 'id' keys.")
195
-
196
- self._api.delete_api(deployment["id"], deployment["teamspace id"])
187
+ self._api.delete_api(deployment.id, deployment.project_id)
@@ -95,15 +95,17 @@ class AIHubApi:
95
95
 
96
96
  if p.type == V1DeploymentTemplateParameterType.CHECKBOX and p.checkbox:
97
97
  api_arguments[p.name] = (
98
- (p.checkbox.true_value or "True")
99
- if p.checkbox.is_checked
100
- else (p.checkbox.false_value or "False")
98
+ (p.checkbox.true_value or "") if p.checkbox.is_checked else (p.checkbox.false_value or "")
101
99
  )
102
100
 
103
101
  for p in parameters:
104
102
  name = p.name
105
103
  pattern = f"${{{name}}}"
106
104
  if name in api_arguments:
105
+ if p.type == V1DeploymentTemplateParameterType.CHECKBOX and p.checkbox:
106
+ api_arguments[p.name] = (
107
+ (p.checkbox.true_value or "") if api_arguments[name] is True else (p.checkbox.false_value or "")
108
+ )
107
109
  AIHubApi._update_parameters(job, p.placements, pattern, api_arguments[name])
108
110
  elif not p.required:
109
111
  AIHubApi._update_parameters(job, p.placements, pattern, "")
@@ -258,6 +258,7 @@ class DeploymentApi:
258
258
  auth: Optional[Union[BasicAuth, TokenAuth]] = None,
259
259
  custom_domain: Optional[str] = None,
260
260
  quantity: Optional[int] = None,
261
+ include_credentials: Optional[bool] = None,
261
262
  ) -> V1Deployment:
262
263
  # Update the deployment in place
263
264
 
@@ -283,6 +284,7 @@ class DeploymentApi:
283
284
  requires_release |= apply_change(deployment.spec, "cluster_id", cloud_account)
284
285
  requires_release |= apply_change(deployment.spec, "spot", spot)
285
286
  requires_release |= apply_change(deployment.spec, "quantity", quantity)
287
+ requires_release |= apply_change(deployment.spec, "include_credentials", include_credentials)
286
288
 
287
289
  if requires_release:
288
290
  if deployment.strategy is None:
@@ -549,6 +551,7 @@ def to_spec(
549
551
  env: Union[List[Union[Secret, Env]], Dict[str, str], None] = None,
550
552
  health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None,
551
553
  quantity: Optional[int] = None,
554
+ include_credentials: Optional[bool] = None,
552
555
  ) -> V1JobSpec:
553
556
  if cloud_account is None:
554
557
  raise ValueError("The cloud account should be defined.")
@@ -569,6 +572,7 @@ def to_spec(
569
572
  instance_name=_machine_to_compute_name(machine),
570
573
  readiness_probe=to_health_check(health_check),
571
574
  quantity=quantity,
575
+ include_credentials=include_credentials,
572
576
  )
573
577
 
574
578
 
@@ -120,6 +120,7 @@ class LitContainerApi:
120
120
  yield {
121
121
  "finish": True,
122
122
  "url": f"{LIGHTNING_CLOUD_URL}/{teamspace.owner.name}/{teamspace.name}/containers/{container_basename}",
123
+ "repository": repository,
123
124
  }
124
125
 
125
126
  def _push_with_retry(self, repository: str, max_retries: int = 3) -> Iterator[Dict[str, Any]]:
@@ -1,17 +1,22 @@
1
+ import os
1
2
  import subprocess
2
3
  from pathlib import Path
3
4
  from typing import Optional, Union
4
5
 
5
6
  import click
6
- import docker
7
7
  from rich.console import Console
8
8
  from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
9
9
  from rich.prompt import Confirm
10
10
 
11
+ from lightning_sdk import Machine, Teamspace
12
+ from lightning_sdk.api.deployment_api import DeploymentApi
11
13
  from lightning_sdk.api.lit_container_api import LitContainerApi
14
+ from lightning_sdk.cli.exceptions import StudioCliError
12
15
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
13
16
  from lightning_sdk.serve import _LitServeDeployer
14
17
 
18
+ _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
19
+
15
20
 
16
21
  @click.group("serve")
17
22
  def serve() -> None:
@@ -21,11 +26,10 @@ def serve() -> None:
21
26
  lightning serve api server.py # serve locally
22
27
 
23
28
  Example:
24
- lightning serve api server.py --cloud # deploy to the cloud
29
+ lightning serve api server.py --cloud --name litserve-api # deploy to the cloud
25
30
 
26
31
  You can deploy the API to the cloud by running `lightning serve api server.py --cloud`.
27
- This will generate a Dockerfile, build the image, and push it to the image registry.
28
- Deploying to the cloud requires pre-login to the docker registry.
32
+ This will build a docker container for the server.py script and deploy it to the Lightning AI platform.
29
33
  """
30
34
 
31
35
 
@@ -45,8 +49,7 @@ def serve() -> None:
45
49
  flag_value=True,
46
50
  help="Deploy the model to the Lightning AI platform",
47
51
  )
48
- @click.option("--gpu", is_flag=True, default=False, flag_value=True, help="Use GPU for serving")
49
- @click.option("--repository", default=None, help="Docker repository name (e.g., 'username/model-name')")
52
+ @click.option("--name", default=None, help="Name of the deployed API (e.g., 'classification-api', 'Llama-api')")
50
53
  @click.option(
51
54
  "--non-interactive",
52
55
  "--non_interactive",
@@ -55,17 +58,88 @@ def serve() -> None:
55
58
  flag_value=True,
56
59
  help="Do not prompt for confirmation",
57
60
  )
61
+ @click.option(
62
+ "--machine",
63
+ default="CPU",
64
+ show_default=True,
65
+ type=click.Choice(_MACHINE_VALUES),
66
+ help="The machine type to deploy the API on.",
67
+ )
68
+ @click.option(
69
+ "--interruptible",
70
+ is_flag=True,
71
+ default=False,
72
+ flag_value=True,
73
+ help="Whether the machine should be interruptible (spot) or not.",
74
+ )
75
+ @click.option(
76
+ "--teamspace",
77
+ default=None,
78
+ help="The teamspace the deployment should be associated with. Defaults to the current teamspace.",
79
+ )
80
+ @click.option(
81
+ "--org",
82
+ default=None,
83
+ help="The organization owning the teamspace (if any). Defaults to the current organization.",
84
+ )
85
+ @click.option("--user", default=None, help="The user owning the teamspace (if any). Defaults to the current user.")
86
+ @click.option(
87
+ "--cloud-account",
88
+ "--cloud_account",
89
+ default=None,
90
+ help=(
91
+ "The cloud account to run the deployment on. "
92
+ "Defaults to the studio cloud account if running with studio compute env. "
93
+ "If not provided will fall back to the teamspaces default cloud account."
94
+ ),
95
+ )
96
+ @click.option("--port", default=8000, help="The port to expose the API on.")
97
+ @click.option("--min_replica", "--min-replica", default=0, help="Number of replicas to start with.")
98
+ @click.option("--max_replica", "--max-replica", default=1, help="Number of replicas to scale up to.")
99
+ @click.option("--replicas", "--replicas", default=1, help="Deployment will start with this many replicas.")
100
+ @click.option(
101
+ "--no_credentials",
102
+ "--no-credentials",
103
+ is_flag=True,
104
+ default=False,
105
+ flag_value=True,
106
+ help="Whether to include credentials in the deployment.",
107
+ )
58
108
  def api(
59
109
  script_path: str,
60
110
  easy: bool,
61
111
  cloud: bool,
62
- gpu: bool,
63
- repository: str,
112
+ name: str,
64
113
  non_interactive: bool,
114
+ machine: str,
115
+ interruptible: bool,
116
+ teamspace: Optional[str],
117
+ org: Optional[str],
118
+ user: Optional[str],
119
+ cloud_account: Optional[str],
120
+ port: Optional[int],
121
+ min_replica: Optional[int],
122
+ max_replica: Optional[int],
123
+ replicas: Optional[int],
124
+ no_credentials: Optional[bool],
65
125
  ) -> None:
66
126
  """Deploy a LitServe model script."""
67
127
  return api_impl(
68
- script_path=script_path, easy=easy, cloud=cloud, gpu=gpu, repository=repository, non_interactive=non_interactive
128
+ script_path=script_path,
129
+ easy=easy,
130
+ cloud=cloud,
131
+ repository=name,
132
+ non_interactive=non_interactive,
133
+ machine=machine,
134
+ interruptible=interruptible,
135
+ teamspace=teamspace,
136
+ org=org,
137
+ user=user,
138
+ cloud_account=cloud_account,
139
+ port=port,
140
+ min_replica=min_replica,
141
+ max_replica=max_replica,
142
+ include_credentials=not no_credentials,
69
143
  )
70
144
 
71
145
 
@@ -73,9 +147,20 @@ def api_impl(
73
147
  script_path: Union[str, Path],
74
148
  easy: bool = False,
75
149
  cloud: bool = False,
76
- gpu: bool = False,
77
- repository: Optional[str] = None,
150
+ repository: [str] = None,
151
+ tag: Optional[str] = None,
78
152
  non_interactive: bool = False,
153
+ machine: str = "CPU",
154
+ interruptible: bool = False,
155
+ teamspace: Optional[str] = None,
156
+ org: Optional[str] = None,
157
+ user: Optional[str] = None,
158
+ cloud_account: Optional[str] = None,
159
+ port: Optional[int] = 8000,
160
+ min_replica: Optional[int] = 0,
161
+ max_replica: Optional[int] = 1,
162
+ replicas: Optional[int] = 1,
163
+ include_credentials: Optional[bool] = True,
79
164
  ) -> None:
80
165
  """Deploy a LitServe model script."""
81
166
  console = Console()
@@ -85,12 +170,29 @@ def api_impl(
85
170
  if not script_path.is_file():
86
171
  raise ValueError(f"Path is not a file: {script_path}")
87
172
 
88
- ls_deployer = _LitServeDeployer()
89
- ls_deployer.generate_client() if easy else None
173
+ _LitServeDeployer.generate_client() if easy else None
90
174
 
91
175
  if cloud:
92
- tag = repository if repository else "litserve-model"
93
- return _handle_cloud(script_path, console, gpu=gpu, tag=tag, non_interactive=non_interactive)
176
+ repository = repository or "litserve-model"
177
+ machine = Machine.from_str(machine)
178
+ return _handle_cloud(
179
+ script_path,
180
+ console,
181
+ repository=repository,
182
+ tag=tag,
183
+ non_interactive=non_interactive,
184
+ machine=machine,
185
+ interruptible=interruptible,
186
+ teamspace=teamspace,
187
+ org=org,
188
+ user=user,
189
+ cloud_account=cloud_account,
190
+ port=port,
191
+ min_replica=min_replica,
192
+ max_replica=max_replica,
193
+ replicas=replicas,
194
+ include_credentials=include_credentials,
195
+ )
94
196
 
95
197
  try:
96
198
  subprocess.run(
@@ -106,20 +208,30 @@ def api_impl(
106
208
  def _handle_cloud(
107
209
  script_path: Union[str, Path],
108
210
  console: Console,
109
- gpu: bool,
110
211
  repository: str = "litserve-model",
111
212
  tag: Optional[str] = None,
112
- teamspace: Optional[str] = None,
113
213
  non_interactive: bool = False,
214
+ machine: Machine = "CPU",
215
+ interruptible: bool = False,
216
+ teamspace: Optional[str] = None,
217
+ org: Optional[str] = None,
218
+ user: Optional[str] = None,
219
+ cloud_account: Optional[str] = None,
220
+ port: Optional[int] = 8000,
221
+ min_replica: Optional[int] = 0,
222
+ max_replica: Optional[int] = 1,
223
+ replicas: Optional[int] = 1,
224
+ include_credentials: Optional[bool] = True,
114
225
  ) -> None:
115
- try:
116
- client = docker.from_env()
117
- client.ping()
118
- except docker.errors.DockerException as e:
119
- raise RuntimeError(f"Failed to connect to Docker daemon: {e!s}. Is Docker running?") from None
226
+ if teamspace is None:
227
+ menu = _TeamspacesMenu()
228
+ resolved_teamspace = menu._resolve_teamspace(teamspace)
229
+ else:
230
+ resolved_teamspace = Teamspace(name=teamspace, org=org, user=user)
120
231
 
232
+ port = port or 8000
121
233
  ls_deployer = _LitServeDeployer()
122
- path = ls_deployer.dockerize_api(script_path, port=8000, gpu=gpu, tag=tag)
234
+ path = ls_deployer.dockerize_api(script_path, port=port, gpu=not machine.is_cpu(), tag=tag, print_success=False)
123
235
  console.clear()
124
236
  if non_interactive:
125
237
  console.print("[italic]non-interactive[/italic] mode enabled, skipping confirmation prompts", style="blue")
@@ -133,18 +245,41 @@ def _handle_cloud(
133
245
  tag = tag if tag else "latest"
134
246
 
135
247
  lit_cr = LitContainerApi()
136
- menu = _TeamspacesMenu()
137
- teamspace = menu._resolve_teamspace(teamspace)
248
+ deployment_name = os.path.basename(repository)
249
+
250
+ ls_deployer.authenticate()
251
+ if DeploymentApi().get_deployment_by_name(deployment_name, resolved_teamspace.id):
252
+ raise StudioCliError(f"Deployment {deployment_name} already exists. Please choose a different name.") from None
253
+
138
254
  with Progress(
139
255
  SpinnerColumn(),
140
256
  TextColumn("[progress.description]{task.description}"),
141
257
  TimeElapsedColumn(),
142
258
  console=console,
143
- transient=False,
259
+ transient=True,
144
260
  ) as progress:
145
- ls_deployer._build_container(path, repository, tag, console, progress)
146
- ls_deployer._push_container(repository, tag, teamspace, lit_cr, progress)
147
- console.print(f"\n✅ Image pushed to {tag}", style="bold green")
148
- console.print(
149
- "Soon you will be able to deploy this model to the Lightning Studio!",
261
+ try:
262
+ ls_deployer.build_container(path, repository, tag, console, progress)
263
+ push_status = ls_deployer.push_container(repository, tag, resolved_teamspace, lit_cr, progress)
264
+ except Exception as e:
265
+ console.print(f" Deployment failed: {e}", style="red")
266
+ return
267
+ console.print(f"\n✅ Image pushed to {repository}:{tag}")
268
+ console.print(f"🔗 You can access the container at: [i]{push_status.get('url')}[/i]")
269
+ repository = push_status.get("repository")
270
+
271
+ deployment_status = ls_deployer._run_on_cloud(
272
+ deployment_name=deployment_name,
273
+ image=repository,
274
+ teamspace=resolved_teamspace,
275
+ metric=None,
276
+ machine=machine,
277
+ spot=interruptible,
278
+ cloud_account=cloud_account,
279
+ port=port,
280
+ min_replica=min_replica,
281
+ max_replica=max_replica,
282
+ replicas=replicas,
283
+ include_credentials=include_credentials,
150
284
  )
285
+ console.print(f"🚀 Deployment started, access at [i]{deployment_status.get('url')}[/i]")
@@ -32,7 +32,7 @@ from lightning_sdk.organization import Organization
32
32
  from lightning_sdk.services.utilities import _get_cluster
33
33
  from lightning_sdk.teamspace import Teamspace
34
34
  from lightning_sdk.user import User
35
- from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_org, _resolve_teamspace, _resolve_user
35
+ from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_teamspace, _resolve_user
36
36
 
37
37
 
38
38
  class Deployment:
@@ -73,13 +73,12 @@ class Deployment:
73
73
  raise e
74
74
 
75
75
  self._name = name
76
- self._org = _resolve_org(org)
77
76
  self._user = _resolve_user(self._user or user)
78
77
 
79
78
  self._teamspace = _resolve_teamspace(
80
79
  teamspace=teamspace,
81
- org=self._org,
82
- user=self._user,
80
+ org=org,
81
+ user=user,
83
82
  )
84
83
  if self._teamspace is None:
85
84
  raise ValueError("You need to pass a teamspace or an org for your deployment.")
@@ -116,6 +115,7 @@ class Deployment:
116
115
  custom_domain: Optional[str] = None,
117
116
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
118
117
  quantity: Optional[int] = None,
118
+ include_credentials: Optional[bool] = None,
119
119
  ) -> None:
120
120
  """The Lightning AI Deployment.
121
121
 
@@ -140,6 +140,7 @@ class Deployment:
140
140
  Doesn't matter when the studio already exists.
141
141
  custom_domain: Whether your service would be referenced under a custom doamin.
142
142
  quantity: The number of machines per replica to deploy.
143
+ include_credentials: Whether to include the environement variables for the SDK to authenticate
143
144
 
144
145
  Note:
145
146
  Since a teamspace can either be owned by an org or by a user directly,
@@ -172,6 +173,7 @@ class Deployment:
172
173
  machine=machine,
173
174
  health_check=health_check,
174
175
  quantity=quantity,
176
+ include_credentials=include_credentials if include_credentials is not None else True,
175
177
  ),
176
178
  strategy=to_strategy(release_strategy),
177
179
  )
@@ -203,6 +205,7 @@ class Deployment:
203
205
  custom_domain: Optional[str] = None,
204
206
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
205
207
  quantity: Optional[int] = None,
208
+ include_credentials: Optional[bool] = None,
206
209
  ) -> None:
207
210
  cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
208
211
 
@@ -225,6 +228,7 @@ class Deployment:
225
228
  health_check=health_check,
226
229
  release_strategy=release_strategy,
227
230
  quantity=quantity,
231
+ include_credentials=include_credentials if include_credentials is not None else True,
228
232
  )
229
233
 
230
234
  def stop(self) -> None:
@@ -359,6 +363,14 @@ class Deployment:
359
363
  return self._deployment.spec.quantity
360
364
  return None
361
365
 
366
+ @property
367
+ def include_credentials(self) -> Optional[bool]:
368
+ """The number of machines per replica."""
369
+ if self._deployment:
370
+ self._deployment = self._deployment_api.get_deployment_by_name(self._name, self._teamspace.id)
371
+ return self._deployment.spec.include_credentials
372
+ return None
373
+
362
374
  @property
363
375
  def user(self) -> Optional[User]:
364
376
  """The teamspace of the deployment."""
@@ -52,6 +52,7 @@ class ProjectIdCloudspacesBody(object):
52
52
  'name': 'str',
53
53
  'plugins': 'list[str]',
54
54
  'requested_run_duration_seconds': 'str',
55
+ 'same_compute_on_resume': 'bool',
55
56
  'seed_files': 'list[V1CloudSpaceSeedFile]',
56
57
  'spot': 'bool'
57
58
  }
@@ -68,11 +69,12 @@ class ProjectIdCloudspacesBody(object):
68
69
  'name': 'name',
69
70
  'plugins': 'plugins',
70
71
  'requested_run_duration_seconds': 'requestedRunDurationSeconds',
72
+ 'same_compute_on_resume': 'sameComputeOnResume',
71
73
  'seed_files': 'seedFiles',
72
74
  'spot': 'spot'
73
75
  }
74
76
 
75
- def __init__(self, can_download_source_code: 'bool' =None, cloud_space_instance_cpu_image_override: 'str' =None, cloud_space_instance_gpu_image_override: 'str' =None, cluster_id: 'str' =None, compute_name: 'str' =None, data_connection_mounts: 'list[V1DataConnectionMount]' =None, disk_size: 'str' =None, display_name: 'str' =None, name: 'str' =None, plugins: 'list[str]' =None, requested_run_duration_seconds: 'str' =None, seed_files: 'list[V1CloudSpaceSeedFile]' =None, spot: 'bool' =None): # noqa: E501
77
+ def __init__(self, can_download_source_code: 'bool' =None, cloud_space_instance_cpu_image_override: 'str' =None, cloud_space_instance_gpu_image_override: 'str' =None, cluster_id: 'str' =None, compute_name: 'str' =None, data_connection_mounts: 'list[V1DataConnectionMount]' =None, disk_size: 'str' =None, display_name: 'str' =None, name: 'str' =None, plugins: 'list[str]' =None, requested_run_duration_seconds: 'str' =None, same_compute_on_resume: 'bool' =None, seed_files: 'list[V1CloudSpaceSeedFile]' =None, spot: 'bool' =None): # noqa: E501
76
78
  """ProjectIdCloudspacesBody - a model defined in Swagger""" # noqa: E501
77
79
  self._can_download_source_code = None
78
80
  self._cloud_space_instance_cpu_image_override = None
@@ -85,6 +87,7 @@ class ProjectIdCloudspacesBody(object):
85
87
  self._name = None
86
88
  self._plugins = None
87
89
  self._requested_run_duration_seconds = None
90
+ self._same_compute_on_resume = None
88
91
  self._seed_files = None
89
92
  self._spot = None
90
93
  self.discriminator = None
@@ -110,6 +113,8 @@ class ProjectIdCloudspacesBody(object):
110
113
  self.plugins = plugins
111
114
  if requested_run_duration_seconds is not None:
112
115
  self.requested_run_duration_seconds = requested_run_duration_seconds
116
+ if same_compute_on_resume is not None:
117
+ self.same_compute_on_resume = same_compute_on_resume
113
118
  if seed_files is not None:
114
119
  self.seed_files = seed_files
115
120
  if spot is not None:
@@ -346,6 +351,27 @@ class ProjectIdCloudspacesBody(object):
346
351
 
347
352
  self._requested_run_duration_seconds = requested_run_duration_seconds
348
353
 
354
+ @property
355
+ def same_compute_on_resume(self) -> 'bool':
356
+ """Gets the same_compute_on_resume of this ProjectIdCloudspacesBody. # noqa: E501
357
+
358
+
359
+ :return: The same_compute_on_resume of this ProjectIdCloudspacesBody. # noqa: E501
360
+ :rtype: bool
361
+ """
362
+ return self._same_compute_on_resume
363
+
364
+ @same_compute_on_resume.setter
365
+ def same_compute_on_resume(self, same_compute_on_resume: 'bool'):
366
+ """Sets the same_compute_on_resume of this ProjectIdCloudspacesBody.
367
+
368
+
369
+ :param same_compute_on_resume: The same_compute_on_resume of this ProjectIdCloudspacesBody. # noqa: E501
370
+ :type: bool
371
+ """
372
+
373
+ self._same_compute_on_resume = same_compute_on_resume
374
+
349
375
  @property
350
376
  def seed_files(self) -> 'list[V1CloudSpaceSeedFile]':
351
377
  """Gets the seed_files of this ProjectIdCloudspacesBody. # noqa: E501
@@ -61,6 +61,7 @@ class V1UserFeatures(object):
61
61
  'deployment_alerts': 'bool',
62
62
  'deployment_persistent_disk': 'bool',
63
63
  'deployment_reservations': 'bool',
64
+ 'dgx_cloud': 'bool',
64
65
  'docs_agent': 'bool',
65
66
  'drive_v2': 'bool',
66
67
  'enable_crypto_crackdown': 'bool',
@@ -82,7 +83,6 @@ class V1UserFeatures(object):
82
83
  'open_api_in_studio': 'bool',
83
84
  'org_level_member_permissions': 'bool',
84
85
  'pipelines': 'bool',
85
- 'plugin_biz_chat': 'bool',
86
86
  'plugin_distributed': 'bool',
87
87
  'plugin_fiftyone': 'bool',
88
88
  'plugin_inference': 'bool',
@@ -139,6 +139,7 @@ class V1UserFeatures(object):
139
139
  'deployment_alerts': 'deploymentAlerts',
140
140
  'deployment_persistent_disk': 'deploymentPersistentDisk',
141
141
  'deployment_reservations': 'deploymentReservations',
142
+ 'dgx_cloud': 'dgxCloud',
142
143
  'docs_agent': 'docsAgent',
143
144
  'drive_v2': 'driveV2',
144
145
  'enable_crypto_crackdown': 'enableCryptoCrackdown',
@@ -160,7 +161,6 @@ class V1UserFeatures(object):
160
161
  'open_api_in_studio': 'openApiInStudio',
161
162
  'org_level_member_permissions': 'orgLevelMemberPermissions',
162
163
  'pipelines': 'pipelines',
163
- 'plugin_biz_chat': 'pluginBizChat',
164
164
  'plugin_distributed': 'pluginDistributed',
165
165
  'plugin_fiftyone': 'pluginFiftyone',
166
166
  'plugin_inference': 'pluginInference',
@@ -196,7 +196,7 @@ class V1UserFeatures(object):
196
196
  'vultr': 'vultr'
197
197
  }
198
198
 
199
- def __init__(self, affiliate_links: 'bool' =None, agents_v2: 'bool' =None, ai_hub_monetization: 'bool' =None, auto_fast_load: 'bool' =None, auto_join_orgs: 'bool' =None, b2c_experience: 'bool' =None, cap_add: 'list[str]' =None, cap_drop: 'list[str]' =None, capacity_reservation_byoc: 'bool' =None, capacity_reservation_dry_run: 'bool' =None, chat_models: 'bool' =None, code_tab: 'bool' =None, collab_screen_sharing: 'bool' =None, cost_attribution_settings: 'bool' =None, custom_app_domain: 'bool' =None, custom_instance_types: 'bool' =None, default_one_cluster: 'bool' =None, deployment_alerts: 'bool' =None, deployment_persistent_disk: 'bool' =None, deployment_reservations: 'bool' =None, docs_agent: 'bool' =None, drive_v2: 'bool' =None, enable_crypto_crackdown: 'bool' =None, enable_storage_limits: 'bool' =None, fair_share: 'bool' =None, featured_studios_admin: 'bool' =None, filestore: 'bool' =None, instant_capacity_reservation: 'bool' =None, job_artifacts_v2: 'bool' =None, jobs_v2: 'bool' =None, lambda_labs: 'bool' =None, landing_studios: 'bool' =None, lit_logger: 'bool' =None, mmt_fault_tolerance: 'bool' =None, mmt_strategy_selector: 'bool' =None, mmt_v2: 'bool' =None, multicloud_saas: 'bool' =None, multiple_studio_versions: 'bool' =None, open_api_in_studio: 'bool' =None, org_level_member_permissions: 'bool' =None, pipelines: 'bool' =None, plugin_biz_chat: 'bool' =None, plugin_distributed: 'bool' =None, plugin_fiftyone: 'bool' =None, plugin_inference: 'bool' =None, plugin_label_studio: 'bool' =None, plugin_langflow: 'bool' =None, plugin_lightning_apps: 'bool' =None, plugin_lightning_apps_distributed: 'bool' =None, plugin_mage_ai: 'bool' =None, plugin_milvus: 'bool' =None, plugin_python_profiler: 'bool' =None, plugin_react: 'bool' =None, plugin_service: 'bool' =None, plugin_sweeps: 'bool' =None, plugin_weviate: 'bool' =None, pricing_updates: 'bool' =None, product_generator: 'bool' =None, project_selector: 'bool' =None, restartable_jobs: 'bool' =None, runnable_public_studio_page: 'bool' =None, security_docs: 'bool' =None, show_dev_admin: 'bool' =None, slurm: 'bool' =None, slurm_machine_selector: 'bool' =None, snapshotter_service: 'bool' =None, snowflake_connection: 'bool' =None, stop_ide_container_on_shutdown: 'bool' =None, studio_config: 'bool' =None, studio_on_stop: 'bool' =None, studio_version_visibility: 'bool' =None, teamspace_storage_tab: 'bool' =None, trainium2: 'bool' =None, use_rclone_mounts_only: 'bool' =None, vultr: 'bool' =None): # noqa: E501
199
+ def __init__(self, affiliate_links: 'bool' =None, agents_v2: 'bool' =None, ai_hub_monetization: 'bool' =None, auto_fast_load: 'bool' =None, auto_join_orgs: 'bool' =None, b2c_experience: 'bool' =None, cap_add: 'list[str]' =None, cap_drop: 'list[str]' =None, capacity_reservation_byoc: 'bool' =None, capacity_reservation_dry_run: 'bool' =None, chat_models: 'bool' =None, code_tab: 'bool' =None, collab_screen_sharing: 'bool' =None, cost_attribution_settings: 'bool' =None, custom_app_domain: 'bool' =None, custom_instance_types: 'bool' =None, default_one_cluster: 'bool' =None, deployment_alerts: 'bool' =None, deployment_persistent_disk: 'bool' =None, deployment_reservations: 'bool' =None, dgx_cloud: 'bool' =None, docs_agent: 'bool' =None, drive_v2: 'bool' =None, enable_crypto_crackdown: 'bool' =None, enable_storage_limits: 'bool' =None, fair_share: 'bool' =None, featured_studios_admin: 'bool' =None, filestore: 'bool' =None, instant_capacity_reservation: 'bool' =None, job_artifacts_v2: 'bool' =None, jobs_v2: 'bool' =None, lambda_labs: 'bool' =None, landing_studios: 'bool' =None, lit_logger: 'bool' =None, mmt_fault_tolerance: 'bool' =None, mmt_strategy_selector: 'bool' =None, mmt_v2: 'bool' =None, multicloud_saas: 'bool' =None, multiple_studio_versions: 'bool' =None, open_api_in_studio: 'bool' =None, org_level_member_permissions: 'bool' =None, pipelines: 'bool' =None, plugin_distributed: 'bool' =None, plugin_fiftyone: 'bool' =None, plugin_inference: 'bool' =None, plugin_label_studio: 'bool' =None, plugin_langflow: 'bool' =None, plugin_lightning_apps: 'bool' =None, plugin_lightning_apps_distributed: 'bool' =None, plugin_mage_ai: 'bool' =None, plugin_milvus: 'bool' =None, plugin_python_profiler: 'bool' =None, plugin_react: 'bool' =None, plugin_service: 'bool' =None, plugin_sweeps: 'bool' =None, plugin_weviate: 'bool' =None, pricing_updates: 'bool' =None, product_generator: 'bool' =None, project_selector: 'bool' =None, restartable_jobs: 'bool' =None, runnable_public_studio_page: 'bool' =None, security_docs: 'bool' =None, show_dev_admin: 'bool' =None, slurm: 'bool' =None, slurm_machine_selector: 'bool' =None, snapshotter_service: 'bool' =None, snowflake_connection: 'bool' =None, stop_ide_container_on_shutdown: 'bool' =None, studio_config: 'bool' =None, studio_on_stop: 'bool' =None, studio_version_visibility: 'bool' =None, teamspace_storage_tab: 'bool' =None, trainium2: 'bool' =None, use_rclone_mounts_only: 'bool' =None, vultr: 'bool' =None): # noqa: E501
200
200
  """V1UserFeatures - a model defined in Swagger""" # noqa: E501
201
201
  self._affiliate_links = None
202
202
  self._agents_v2 = None
@@ -218,6 +218,7 @@ class V1UserFeatures(object):
218
218
  self._deployment_alerts = None
219
219
  self._deployment_persistent_disk = None
220
220
  self._deployment_reservations = None
221
+ self._dgx_cloud = None
221
222
  self._docs_agent = None
222
223
  self._drive_v2 = None
223
224
  self._enable_crypto_crackdown = None
@@ -239,7 +240,6 @@ class V1UserFeatures(object):
239
240
  self._open_api_in_studio = None
240
241
  self._org_level_member_permissions = None
241
242
  self._pipelines = None
242
- self._plugin_biz_chat = None
243
243
  self._plugin_distributed = None
244
244
  self._plugin_fiftyone = None
245
245
  self._plugin_inference = None
@@ -314,6 +314,8 @@ class V1UserFeatures(object):
314
314
  self.deployment_persistent_disk = deployment_persistent_disk
315
315
  if deployment_reservations is not None:
316
316
  self.deployment_reservations = deployment_reservations
317
+ if dgx_cloud is not None:
318
+ self.dgx_cloud = dgx_cloud
317
319
  if docs_agent is not None:
318
320
  self.docs_agent = docs_agent
319
321
  if drive_v2 is not None:
@@ -356,8 +358,6 @@ class V1UserFeatures(object):
356
358
  self.org_level_member_permissions = org_level_member_permissions
357
359
  if pipelines is not None:
358
360
  self.pipelines = pipelines
359
- if plugin_biz_chat is not None:
360
- self.plugin_biz_chat = plugin_biz_chat
361
361
  if plugin_distributed is not None:
362
362
  self.plugin_distributed = plugin_distributed
363
363
  if plugin_fiftyone is not None:
@@ -845,6 +845,27 @@ class V1UserFeatures(object):
845
845
 
846
846
  self._deployment_reservations = deployment_reservations
847
847
 
848
+ @property
849
+ def dgx_cloud(self) -> 'bool':
850
+ """Gets the dgx_cloud of this V1UserFeatures. # noqa: E501
851
+
852
+
853
+ :return: The dgx_cloud of this V1UserFeatures. # noqa: E501
854
+ :rtype: bool
855
+ """
856
+ return self._dgx_cloud
857
+
858
+ @dgx_cloud.setter
859
+ def dgx_cloud(self, dgx_cloud: 'bool'):
860
+ """Sets the dgx_cloud of this V1UserFeatures.
861
+
862
+
863
+ :param dgx_cloud: The dgx_cloud of this V1UserFeatures. # noqa: E501
864
+ :type: bool
865
+ """
866
+
867
+ self._dgx_cloud = dgx_cloud
868
+
848
869
  @property
849
870
  def docs_agent(self) -> 'bool':
850
871
  """Gets the docs_agent of this V1UserFeatures. # noqa: E501
@@ -1286,27 +1307,6 @@ class V1UserFeatures(object):
1286
1307
 
1287
1308
  self._pipelines = pipelines
1288
1309
 
1289
- @property
1290
- def plugin_biz_chat(self) -> 'bool':
1291
- """Gets the plugin_biz_chat of this V1UserFeatures. # noqa: E501
1292
-
1293
-
1294
- :return: The plugin_biz_chat of this V1UserFeatures. # noqa: E501
1295
- :rtype: bool
1296
- """
1297
- return self._plugin_biz_chat
1298
-
1299
- @plugin_biz_chat.setter
1300
- def plugin_biz_chat(self, plugin_biz_chat: 'bool'):
1301
- """Sets the plugin_biz_chat of this V1UserFeatures.
1302
-
1303
-
1304
- :param plugin_biz_chat: The plugin_biz_chat of this V1UserFeatures. # noqa: E501
1305
- :type: bool
1306
- """
1307
-
1308
- self._plugin_biz_chat = plugin_biz_chat
1309
-
1310
1310
  @property
1311
1311
  def plugin_distributed(self) -> 'bool':
1312
1312
  """Gets the plugin_distributed of this V1UserFeatures. # noqa: E501
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  from typing import Dict, List, Optional
2
3
 
3
4
  from lightning_sdk.api.lit_container_api import LitContainerApi
@@ -29,11 +30,17 @@ class LitContainer:
29
30
  repositories = self._api.list_containers(project_id)
30
31
  table = []
31
32
  for repo in repositories:
33
+ created_date = repo.creation_time
34
+ if isinstance(repo.creation_time, str):
35
+ created_date = datetime.fromisoformat(created_date)
36
+
37
+ created = created_date.strftime("%Y-%m-%d %H:%M:%S")
38
+
32
39
  table.append(
33
40
  {
34
41
  "REPOSITORY": repo.name,
35
42
  "IMAGE ID": repo.id,
36
- "CREATED": repo.creation_time.strftime("%Y-%m-%d %H:%M:%S"),
43
+ "CREATED": created,
37
44
  }
38
45
  )
39
46
  return table
lightning_sdk/mmt/mmt.py CHANGED
@@ -1,7 +1,5 @@
1
- from functools import lru_cache
2
1
  from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
3
2
 
4
- from lightning_sdk.api.user_api import UserApi
5
3
  from lightning_sdk.mmt.base import MMTMachine, _BaseMMT
6
4
  from lightning_sdk.mmt.v1 import _MMTV1
7
5
  from lightning_sdk.mmt.v2 import _MMTV2
@@ -18,21 +16,6 @@ if TYPE_CHECKING:
18
16
  _logger = _setup_logger(__name__)
19
17
 
20
18
 
21
- @lru_cache(maxsize=None)
22
- def _has_mmt_v2() -> bool:
23
- api = UserApi()
24
- try:
25
- feature_flags = api._get_feature_flags()
26
- except Exception:
27
- return False
28
-
29
- try:
30
- return feature_flags.mmt_v2
31
- except AttributeError:
32
- # Feature flag doesn't exist anymore, so return True
33
- return True
34
-
35
-
36
19
  class MMT(_BaseMMT):
37
20
  """Class to submit and manage multi-machine jobs on the Lightning AI Platform."""
38
21
 
@@ -60,7 +43,7 @@ class MMT(_BaseMMT):
60
43
  """
61
44
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
62
45
 
63
- if _has_mmt_v2() and not self._force_v1:
46
+ if not self._force_v1:
64
47
  # try with v2 and fall back to v1
65
48
  try:
66
49
  mmt = _MMTV2(
lightning_sdk/mmt/v1.py CHANGED
@@ -92,34 +92,7 @@ class _MMTV1(_BaseMMT):
92
92
  path_mappings: The mappings from data connection inside your container (not supported)
93
93
 
94
94
  """
95
- if studio is None:
96
- raise ValueError("Studio is required for submitting jobs")
97
- if image is not None or image_credentials is not None or cloud_account_auth or entrypoint != "sh -c":
98
- raise ValueError("Image is not supported for submitting jobs")
99
-
100
- if artifacts_local is not None or artifacts_remote is not None:
101
- raise ValueError("Specifying how to persist artifacts is not yet supported with jobs")
102
-
103
- if env is not None:
104
- raise ValueError("Environment variables are not supported for submitting jobs")
105
- if command is None:
106
- raise ValueError("Command is required for submitting multi-machine jobs")
107
-
108
- _submitted = self._job_api.submit_job(
109
- name=self._name,
110
- num_machines=num_machines,
111
- command=command,
112
- studio_id=studio._studio.id,
113
- teamspace_id=self._teamspace.id,
114
- cloud_account=cloud_account or "",
115
- machine=machine,
116
- interruptible=interruptible,
117
- strategy="parallel",
118
- )
119
-
120
- self._name = _submitted.name
121
- self._job = _submitted
122
- return self
95
+ raise NotImplementedError("Cannot submit new mmts with MMTV1!")
123
96
 
124
97
  def _update_internal_job(self) -> None:
125
98
  try:
lightning_sdk/models.py CHANGED
@@ -72,6 +72,19 @@ def _get_teamspace(name: str, organization: str) -> "Teamspace":
72
72
  raise RuntimeError(f"Teamspace `{requested_teamspace}` not found. Available teamspaces: {os.linesep}\t{options}")
73
73
 
74
74
 
75
+ def _extend_model_name_with_teamspace(name: str) -> str:
76
+ """Extend the model name with the teamspace if it can be determined from env. variables."""
77
+ if "/" in name:
78
+ return name
79
+ # do some magic if you run studio
80
+ teamspace = _resolve_teamspace(None, None, None)
81
+ if not teamspace:
82
+ raise ValueError(
83
+ f"Model name must be in the format `organization/teamspace/model_name` but you provided '{name}'."
84
+ )
85
+ return f"{teamspace.owner.name}/{teamspace.name}/{name}"
86
+
87
+
75
88
  def _parse_model_name_and_version(name: str) -> Tuple[str, str, str, str]:
76
89
  """Parse the name argument into its components."""
77
90
  try:
@@ -105,9 +118,7 @@ def download_model(
105
118
  download_dir: The directory where the Model should be downloaded.
106
119
  progress_bar: Whether to show a progress bar when downloading.
107
120
  """
108
- if "/" not in name: # do some magic if you run studio
109
- teamspace = _resolve_teamspace(None, None, None)
110
- name = f"{teamspace.owner.name}/{teamspace.name}/{name}"
121
+ name = _extend_model_name_with_teamspace(name)
111
122
  teamspace_owner_name, teamspace_name, model_name, version = _parse_model_name_and_version(name)
112
123
 
113
124
  download_dir = Path(download_dir)
@@ -147,9 +158,7 @@ def upload_model(
147
158
  If not provided, the default cloud account for the Teamspace will be used.
148
159
  progress_bar: Whether to show a progress bar for the upload.
149
160
  """
150
- if "/" not in name: # do some magic if you run studio
151
- teamspace = _resolve_teamspace(None, None, None)
152
- name = f"{teamspace.owner.name}/{teamspace.name}/{name}"
161
+ name = _extend_model_name_with_teamspace(name)
153
162
  org_name, teamspace_name, model_name, _ = _parse_model_name_and_version(name)
154
163
  teamspace = _get_teamspace(name=teamspace_name, organization=org_name)
155
164
  return teamspace.upload_model(
@@ -49,8 +49,8 @@ class Pipeline:
49
49
 
50
50
  self._teamspace = _resolve_teamspace(
51
51
  teamspace=teamspace,
52
- org=self._org,
53
- user=self._user,
52
+ org=org,
53
+ user=user,
54
54
  )
55
55
 
56
56
  self._pipeline_api = PipelineApi()
lightning_sdk/serve.py CHANGED
@@ -1,13 +1,38 @@
1
1
  import os
2
- import warnings
2
+ import shlex
3
+ import subprocess
3
4
  from pathlib import Path
5
+ from typing import Generator, Optional
6
+ from urllib.parse import urlencode
4
7
 
5
8
  import docker
6
9
  from rich.console import Console
7
10
  from rich.progress import Progress
8
11
 
9
- from lightning_sdk import Teamspace
12
+ from lightning_sdk import Deployment, Machine, Teamspace
13
+ from lightning_sdk.api.deployment_api import AutoScaleConfig
10
14
  from lightning_sdk.api.lit_container_api import LitContainerApi
15
+ from lightning_sdk.api.utils import _get_cloud_url
16
+ from lightning_sdk.lightning_cloud import env
17
+ from lightning_sdk.lightning_cloud.login import Auth, AuthServer
18
+
19
+ _DOCKER_NOT_RUNNING_MSG = (
20
+ "Deploying LitServe requires Docker to be running on the machine. "
21
+ "If Docker is not installed, please install it from https://docs.docker.com/get-docker/ "
22
+ "and start the Docker daemon before running this command."
23
+ )
24
+
25
+
26
+ class _AuthServer(AuthServer):
27
+ def get_auth_url(self, port: int) -> str:
28
+ redirect_uri = f"http://localhost:{port}/login-complete"
29
+ params = urlencode({"redirectTo": redirect_uri, "inviteCode": "litserve"})
30
+ return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
31
+
32
+
33
+ class _Auth(Auth):
34
+ def _run_server(self) -> None:
35
+ _AuthServer().login_with_browser(self)
11
36
 
12
37
 
13
38
  class _LitServeDeployer:
@@ -15,36 +40,49 @@ class _LitServeDeployer:
15
40
  self._console = Console()
16
41
  self._client = None
17
42
 
43
+ @staticmethod
44
+ def authenticate() -> None:
45
+ auth = _Auth()
46
+ auth.authenticate()
47
+
18
48
  @property
19
49
  def client(self) -> docker.DockerClient:
20
50
  if self._client is None:
21
51
  try:
52
+ os.environ["DOCKER_BUILDKIT"] = "1"
22
53
  self._client = docker.from_env()
23
54
  self._client.ping()
24
- except docker.errors.DockerException as e:
25
- raise RuntimeError(f"Failed to connect to Docker daemon: {e!s}. Is Docker running?") from None
55
+ except docker.errors.DockerException:
56
+ raise RuntimeError(_DOCKER_NOT_RUNNING_MSG) from None
26
57
  return self._client
27
58
 
28
59
  def dockerize_api(
29
- self, server_filename: str, port: int = 8000, gpu: bool = False, tag: str = "litserve-model"
60
+ self,
61
+ server_filename: str,
62
+ port: int = 8000,
63
+ gpu: bool = False,
64
+ tag: str = "litserve-model",
65
+ print_success: bool = True,
30
66
  ) -> str:
31
67
  import litserve as ls
32
68
  from litserve import docker_builder
33
69
 
34
70
  console = self._console
35
- if os.path.exists("Dockerfile"):
36
- console.print("Dockerfile already exists. Skipping generation.")
37
- return os.path.abspath("Dockerfile")
38
-
39
71
  requirements = ""
40
72
  if os.path.exists("requirements.txt"):
41
73
  requirements = "-r requirements.txt"
42
74
  else:
43
- warnings.warn(
75
+ console.print(
44
76
  f"requirements.txt not found at {os.getcwd()}. "
45
77
  f"Make sure to install the required packages in the Dockerfile.",
46
- UserWarning,
78
+ style="yellow",
79
+ )
80
+
81
+ if os.path.exists("Dockerfile"):
82
+ console.print(
83
+ "Dockerfile already exists in the current directory, we will use it for building the container."
47
84
  )
85
+ return os.path.abspath("Dockerfile")
48
86
  current_dir = Path.cwd()
49
87
  if not (current_dir / server_filename).is_file():
50
88
  raise FileNotFoundError(f"Server file `{server_filename}` must be in the current directory: {os.getcwd()}")
@@ -65,7 +103,8 @@ class _LitServeDeployer:
65
103
  with open("Dockerfile", "w") as f:
66
104
  f.write(dockerfile_content)
67
105
 
68
- success_msg = f"""[bold]Dockerfile created successfully[/bold]
106
+ if print_success:
107
+ success_msg = f"""[bold]Dockerfile created successfully[/bold]
69
108
  Update [underline]{os.path.abspath("Dockerfile")}[/underline] to add any additional dependencies or commands.
70
109
 
71
110
  [bold]Build the container with:[/bold]
@@ -77,11 +116,14 @@ Update [underline]{os.path.abspath("Dockerfile")}[/underline] to add any additio
77
116
  [bold]To push the container to a registry:[/bold]
78
117
  > [underline]docker push {tag}[/underline]
79
118
  """
80
- console.print(success_msg)
119
+ console.print(success_msg)
81
120
  return os.path.abspath("Dockerfile")
82
121
 
83
- def generate_client(self) -> None:
84
- console = self._console
122
+ @staticmethod
123
+ def generate_client() -> None:
124
+ from rich.console import Console
125
+
126
+ console = Console()
85
127
  try:
86
128
  from litserve.python_client import client_template
87
129
  except ImportError:
@@ -99,36 +141,114 @@ Update [underline]{os.path.abspath("Dockerfile")}[/underline] to add any additio
99
141
  except OSError as e:
100
142
  raise OSError(f"Failed to generate client.py: {e!s}") from None
101
143
 
102
- def _build_container(self, path: str, repository: str, tag: str, console: Console, progress: Progress) -> None:
103
- build_task = progress.add_task("Building Docker image", total=None)
104
- build_status = self.client.api.build(
105
- path=os.path.dirname(path), dockerfile=path, tag=f"{repository}:{tag}", decode=True, quiet=False
144
+ def _docker_build_with_logs(
145
+ self, path: str, repository: str, tag: str, platform: str = "linux/amd64"
146
+ ) -> Generator[str, None, None]:
147
+ """Build Docker image using CLI with real-time log streaming.
148
+
149
+ Returns:
150
+ Tuple: (image_id, logs generator)
151
+
152
+ Raises:
153
+ RuntimeError: On build failure
154
+ """
155
+ cmd = f"docker build --platform {platform} -t {repository}:{tag} ."
156
+ proc = subprocess.Popen(
157
+ shlex.split(cmd),
158
+ stdout=subprocess.PIPE,
159
+ stderr=subprocess.STDOUT,
160
+ text=True,
161
+ bufsize=1, # Line buffered
106
162
  )
107
- for line in build_status:
163
+
164
+ def log_generator() -> Generator[str, None, None]:
165
+ while True:
166
+ line = proc.stdout.readline()
167
+ if not line and proc.poll() is not None:
168
+ break
169
+ yield line.strip()
170
+ if "error" in line.lower():
171
+ proc.terminate()
172
+ raise RuntimeError(f"Build failed: {line.strip()}")
173
+
174
+ if proc.returncode != 0:
175
+ raise RuntimeError(f"Build failed with exit code {proc.returncode}")
176
+
177
+ return log_generator()
178
+
179
+ def build_container(self, path: str, repository: str, tag: str, console: Console, progress: Progress) -> None:
180
+ build_task = progress.add_task("Building Docker image", total=None)
181
+ build_logs = self._docker_build_with_logs(path, repository, tag=tag)
182
+
183
+ for line in build_logs:
108
184
  if "error" in line:
109
185
  progress.stop()
110
186
  console.print(f"\n[red]{line}[/red]")
111
- return
112
- if "stream" in line and line["stream"].strip():
113
- console.print(line["stream"].strip(), style="bright_black")
187
+ raise RuntimeError(f"Failed to build image: {line}")
188
+ else:
189
+ console.print(
190
+ line.strip(),
191
+ )
114
192
  progress.update(build_task, description="Building Docker image")
115
193
 
116
194
  progress.update(build_task, description="[green]Build completed![/green]")
117
195
 
118
- def _push_container(
196
+ def push_container(
119
197
  self, repository: str, tag: str, teamspace: Teamspace, lit_cr: LitContainerApi, progress: Progress
120
- ) -> None:
198
+ ) -> dict:
121
199
  console = self._console
122
200
  push_task = progress.add_task("Pushing to registry", total=None)
123
201
  console.print("\nPushing image...", style="bold blue")
124
202
  lit_cr.authenticate()
125
203
  push_status = lit_cr.upload_container(repository, teamspace, tag=tag)
204
+ last_status = {}
126
205
  for line in push_status:
206
+ last_status = line
127
207
  if "error" in line:
128
208
  progress.stop()
129
209
  console.print(f"\n[red]{line}[/red]")
130
- return
210
+ raise RuntimeError(f"Failed to push image: {line}")
131
211
  if "status" in line:
132
- console.print(line["status"], style="bright_black")
212
+ console.print(line["status"].strip())
133
213
  progress.update(push_task, description="Pushing to registry")
134
214
  progress.update(push_task, description="[green]Push completed![/green]")
215
+ return last_status
216
+
217
+ def _run_on_cloud(
218
+ self,
219
+ deployment_name: str,
220
+ teamspace: Teamspace,
221
+ image: str,
222
+ metric: Optional[str] = None,
223
+ machine: Optional[Machine] = None,
224
+ min_replica: Optional[int] = 0,
225
+ max_replica: Optional[int] = 1,
226
+ spot: Optional[bool] = None,
227
+ replicas: Optional[int] = 1,
228
+ cloud_account: Optional[str] = None,
229
+ port: Optional[int] = 8000,
230
+ include_credentials: Optional[bool] = True,
231
+ ) -> dict:
232
+ url = f"{_get_cloud_url()}/{teamspace.owner.name}/{teamspace.name}/jobs/{deployment_name}"
233
+ machine = machine or Machine.CPU
234
+ metric = metric or ("CPU" if machine.is_cpu() else "GPU")
235
+ deployment = Deployment(deployment_name, teamspace)
236
+ if deployment.is_started:
237
+ raise RuntimeError(
238
+ f"Deployment with name {deployment_name} already running. "
239
+ "Please stop the deployment before starting a new one.\n"
240
+ f"You can access the deployment at {url}"
241
+ )
242
+ autoscale = AutoScaleConfig(min_replicas=min_replica, max_replicas=max_replica, metric=metric, threshold=0.95)
243
+ deployment.start(
244
+ machine=machine,
245
+ image=image,
246
+ autoscale=autoscale,
247
+ spot=spot,
248
+ replicas=replicas,
249
+ cloud_account=cloud_account,
250
+ ports=[port],
251
+ include_credentials=include_credentials,
252
+ )
253
+
254
+ return {"deployment": deployment, "url": url}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 0.2.2
3
+ Version: 0.2.4
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,26 +1,26 @@
1
1
  docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
2
- lightning_sdk/__init__.py,sha256=AXfGavmhTcfNwZPxvjYvNeu_YAKQzZ8A9NSbbFp8e90,1104
2
+ lightning_sdk/__init__.py,sha256=0M0hCXuVUl2Lxnfphl1qbZNnMBde0xhorpbackzGcHk,1104
3
3
  lightning_sdk/agents.py,sha256=ly6Ma1j0ZgGPFyvPvMN28JWiB9dATIstFa5XM8pMi6I,1577
4
- lightning_sdk/ai_hub.py,sha256=dfw5CS93ehGmPlZCbbE2Pu3v0ANI6ZY05-ULbcHFqs4,7189
4
+ lightning_sdk/ai_hub.py,sha256=Zsc8GhFuRQIAYk3J3znUYDfyVhe3sd0WYLk2bFcoxVI,6803
5
5
  lightning_sdk/constants.py,sha256=ztl1PTUBULnqTf3DyKUSJaV_O20hNtUYT6XvAYIrmIk,749
6
6
  lightning_sdk/helpers.py,sha256=KWMWnORHItIIA3PGn71YPs-7RjzGi8IXa2kQ5Qo4U8M,2459
7
- lightning_sdk/lit_container.py,sha256=423XYYpzoGppv200kMbD9dB-iqP6xdHj7q5kejJLDKE,3778
7
+ lightning_sdk/lit_container.py,sha256=2A_7Hc_tX2ilj6UZmdLm-us144_KIcnWB0qT96sJGQM,4000
8
8
  lightning_sdk/machine.py,sha256=EMr-ulyYEYhIkKFnGBOofnFf4asndTSeoOQxlyd3xW4,3632
9
- lightning_sdk/models.py,sha256=inFyRA8aLMvrAhCeIdJaTGHjp0Qtou9G_J3gXePxU4E,5975
9
+ lightning_sdk/models.py,sha256=0aJ4OXNY0MYUCfsYI6i18fV61DU-HGKIKEHFD99r1zk,6231
10
10
  lightning_sdk/organization.py,sha256=WCfzdgjtvY1_A07DnxOpp74V2JR2gQwtXbIEcFDnoVU,1232
11
11
  lightning_sdk/owner.py,sha256=t5svD2it4C9pbSpVuG9WJL46CYi37JXNziwnXxhiU5U,1361
12
12
  lightning_sdk/plugin.py,sha256=TGxCa-xsOpv_kaKiVnNBNG3jLe9MweSS5Cnz9PcxQfY,14860
13
- lightning_sdk/serve.py,sha256=FydpScppL1HhzwkvvcihRVcWc6kdQSuajwTlC6tXHiU,5368
13
+ lightning_sdk/serve.py,sha256=3nE_0TkTOtiCpdieQ01Pkyi-rb1rIkBJiF_g_fGIos8,9456
14
14
  lightning_sdk/status.py,sha256=lLGAuSvXBoXQFEEsEYwdCi0RcSNatUn5OPjJVjDtoM0,386
15
15
  lightning_sdk/studio.py,sha256=hyvAiVhkETAtbu0RRF1Aw6F8Y__E1SSAmsB8PHfmqHo,19935
16
16
  lightning_sdk/teamspace.py,sha256=CzpQJ6Uj_at0W-HIzYd-Y3gN9qBotUbAV3YBWjNT9U8,14364
17
17
  lightning_sdk/user.py,sha256=vdn8pZqkAZO0-LoRsBdg0TckRKtd_H3QF4gpiZcl4iY,1130
18
18
  lightning_sdk/api/__init__.py,sha256=Qn2VVRvir_gO7w4yxGLkZY-R3T7kdiTPKgQ57BhIA9k,413
19
19
  lightning_sdk/api/agents_api.py,sha256=G47TbFo9kYqnBMqdw2RW-lfS1VAUBSXDmzs6fpIEMUs,4059
20
- lightning_sdk/api/ai_hub_api.py,sha256=Yr9VvxueIqPUUeMqExbjbWD5CE_yeWjq0dXQQv47erg,6779
21
- lightning_sdk/api/deployment_api.py,sha256=rtjhC19LuyABQ2R5CjXrF6_Y05kHQTyzhc6UTG36J84,22332
20
+ lightning_sdk/api/ai_hub_api.py,sha256=IaA57Cqu_zdj5zRwKkx9LJQIGFFo2mHmPOYekKxbZ9A,6999
21
+ lightning_sdk/api/deployment_api.py,sha256=z_D7ZZAnsVe3Q_ZVx1azcLK2_pP5xj63tmU97qhgyOw,22583
22
22
  lightning_sdk/api/job_api.py,sha256=_mMAI_BG_48i-BLwCP_U72zgmM5zYa2KUZ7u66HWkIc,13568
23
- lightning_sdk/api/lit_container_api.py,sha256=ij2Z3Hg1JceVmabSn8imw8s2F74NyTIDCb3YYP5r8a4,7610
23
+ lightning_sdk/api/lit_container_api.py,sha256=m-3qZIIpZ24Z2Z8z9x8Di6cOSjkZmBFcf9R2yVMpA_4,7648
24
24
  lightning_sdk/api/mmt_api.py,sha256=-v7ATab-ThAM-HRClS92Ehxuu9MlBfdKWWFCGvVUHiM,8962
25
25
  lightning_sdk/api/org_api.py,sha256=Ze3z_ATVrukobujV5YdC42DKj45Vuwl7X52q_Vr-o3U,803
26
26
  lightning_sdk/api/pipeline_api.py,sha256=P5P9C6qOpyBGU0t5N68h1LuFAsAKmPPgkac6uObrYKw,1676
@@ -47,7 +47,7 @@ lightning_sdk/cli/list.py,sha256=dAZ94QPvE4IkH6GfL7171TMrMfcuWB53cvW6wk2eL4w,101
47
47
  lightning_sdk/cli/mmts_menu.py,sha256=HUXo3ZoZ3fWOCNWTQWoJgUlFXYq5uVm_6uFjAq7BDe8,2219
48
48
  lightning_sdk/cli/open.py,sha256=sgMLWBnkXdIq8H9XK_rph2bye3b07AKTJBQIk9fCGVc,1937
49
49
  lightning_sdk/cli/run.py,sha256=8JZiDrKwDhlaTOJd6qq2mCWJRqKm6shCWLzpbmFYIkE,13929
50
- lightning_sdk/cli/serve.py,sha256=bnAgsotNaTpud4grws3eWxfybVTRhPnoDpT13kOeGYI,4880
50
+ lightning_sdk/cli/serve.py,sha256=IHQneUUWhyYQHP_ChQafm7vyfb5-9crIH_CmjKUy7TM,9445
51
51
  lightning_sdk/cli/start.py,sha256=jUk52lkEFC_fqiEPkwM8GwE68WMNEtzBuzjkvr3POd0,1840
52
52
  lightning_sdk/cli/stop.py,sha256=5nCrUe1BONpX1nKNhbSFqLaXXKaRhSO7PvM1BVYLgn4,2864
53
53
  lightning_sdk/cli/studios_menu.py,sha256=TA9rO6_fFHGMz0Nt4rJ6iV80X5pZE4xShrSiyXoU-oQ,4129
@@ -55,7 +55,7 @@ lightning_sdk/cli/switch.py,sha256=qLvDoQRldCNgO1XvIGg-i-GyqnkHVb_U1-X4svqeNNU,1
55
55
  lightning_sdk/cli/teamspace_menu.py,sha256=C3g3spTKgtMwoK7pnooy0MBPz4AKhFjcObkvZyZ4v04,3797
56
56
  lightning_sdk/cli/upload.py,sha256=nZ7hVKccis4Fnx2ae3Y2utUWTlKH_9N-xjRFG9Xh3q4,12486
57
57
  lightning_sdk/deployment/__init__.py,sha256=dXsa4psDzFYFklsq3JC-2V_L4FQjGZnQAf-ZiVlqG9c,545
58
- lightning_sdk/deployment/deployment.py,sha256=uXqRT6zbwp1_od-reQpv0QagrNEZuEQVsoaILzU17E8,16532
58
+ lightning_sdk/deployment/deployment.py,sha256=EbFoDLdj1UubXyOTSp1VTlpDTgwdczfyusdLStqD7WA,17218
59
59
  lightning_sdk/job/__init__.py,sha256=1MxjQ6rHkyUHCypSW9RuXuVMVH11WiqhIXcU2LCFMwE,64
60
60
  lightning_sdk/job/base.py,sha256=TS5KavtfBAFbLbNqqumEizY9edjO1joSmtUdcO5CThQ,17748
61
61
  lightning_sdk/job/job.py,sha256=1Xf0ne4wwXpkb_GJgWD8mfueJZoKR8G4lC5T6OXijlw,13075
@@ -207,7 +207,7 @@ lightning_sdk/lightning_cloud/openapi/models/profiler_captures_body.py,sha256=xT
207
207
  lightning_sdk/lightning_cloud/openapi/models/profiler_enabled_body.py,sha256=4YnP4KmUlXM5Brm5Sj5WnF82MTPr48FqH0C4R5YZRVY,4703
208
208
  lightning_sdk/lightning_cloud/openapi/models/project_id_agentmanagedendpoints_body.py,sha256=pK1HjgWsXhU7OOKwYdt-SwklzF39N3Ap7Lyrk75T43k,4673
209
209
  lightning_sdk/lightning_cloud/openapi/models/project_id_agents_body.py,sha256=oE3OGIhX0FfiKqE96YJmHnrRRAXgqrFsIAw840ib64Q,16193
210
- lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py,sha256=q36orD3j5-RjVaMtFp3q_DFD1ZbrHBnT2VLyciDsIeM,15176
210
+ lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py,sha256=kroaORLZVLUQIXO-jQrlZvmVXXJF9t6CFM09jTFRBeQ,16188
211
211
  lightning_sdk/lightning_cloud/openapi/models/project_id_clusters_body.py,sha256=ewyBgf8oHlhgXKmn4YdjuyG84iONOeNXiWtbRQJfj4U,4348
212
212
  lightning_sdk/lightning_cloud/openapi/models/project_id_datasets_body.py,sha256=NkuH_enmOZ0MnYJ_hH2iVP1-Y1a9XinHR_c-5DaJxqU,15094
213
213
  lightning_sdk/lightning_cloud/openapi/models/project_id_endpoints_body.py,sha256=bnCgn3Jz3KE7blDn7chONuxJuc7Wg1eYM1QG0hxGR9U,7507
@@ -876,7 +876,7 @@ lightning_sdk/lightning_cloud/openapi/models/v1_upstream_open_ai.py,sha256=jt1qQ
876
876
  lightning_sdk/lightning_cloud/openapi/models/v1_usage.py,sha256=RhhnH9ygScZyExg06WhvMNPPRLSe8FYkIftqF-D9NIU,13408
877
877
  lightning_sdk/lightning_cloud/openapi/models/v1_usage_details.py,sha256=U7qC698Xj5tb3D93ZskG6sDf3lTXE13UTlGeDTvtRU4,14062
878
878
  lightning_sdk/lightning_cloud/openapi/models/v1_usage_report.py,sha256=iH67BcONBSLYzcZpGpKWSOzJTCpuqYt7FU4OUs8BJ9k,6076
879
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py,sha256=Hsvc-MaidKATyRa6gshe0SXCSlcSQLjoh-3SmplRZys,68004
879
+ lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py,sha256=_I8vCY0BOl6yv-f_ZaqOmBhogr1SPhAz3JZZ7Veoq94,67885
880
880
  lightning_sdk/lightning_cloud/openapi/models/v1_user_requested_compute_config.py,sha256=3jeJfpbBpYY2B2Ao2j2N93CMO2CnvmPqndE4Lw3ZfMA,13056
881
881
  lightning_sdk/lightning_cloud/openapi/models/v1_user_requested_flow_compute_config.py,sha256=3WpZ-lf7xPwuYyQDMdP7Uc6-dh3vf5TaaUlcMfesfMk,5208
882
882
  lightning_sdk/lightning_cloud/openapi/models/v1_user_slurm_job_action_response.py,sha256=BdNzXH8Vsf5PHjl9Rd-TVkpAgx1YC9rf8LD0js-ba20,3058
@@ -913,11 +913,11 @@ lightning_sdk/lightning_cloud/utils/name_generator.py,sha256=MkciuA10332V0mcE2Px
913
913
  lightning_sdk/lightning_cloud/utils/network.py,sha256=axPgl8rhyPcPjxiztDxyksfxax3VNg2OXL5F5Uc81b4,406
914
914
  lightning_sdk/mmt/__init__.py,sha256=ExMu90-96bGBnyp5h0CErQszUGB1-PcjC4-R8_NYbeY,117
915
915
  lightning_sdk/mmt/base.py,sha256=B3HC-c82bPHprEZh1mhLCPCrCE8BOKqwIhY7xCF9CXg,15152
916
- lightning_sdk/mmt/mmt.py,sha256=UbbK6UFEd-S2OPjezCG7cy-7kfMbyLOg3LAzs_Zq2Ds,13872
917
- lightning_sdk/mmt/v1.py,sha256=dROOXRWLzG2kUwGtvozB9Vywt_3QK8j4XFHVZFohWUM,9527
916
+ lightning_sdk/mmt/mmt.py,sha256=swdGP1DOM42a_QmmY1vg3--6ZBDiC4zToAzU9Eycv4U,13446
917
+ lightning_sdk/mmt/v1.py,sha256=TxLtL0ssDoP8eyleDaFyYr4evkOKbLJcckLVIfalOno,8429
918
918
  lightning_sdk/mmt/v2.py,sha256=Em1XBoqViqUMKm-sshzdMcSH5UTtZZwbJcsgqY6-mw0,9625
919
919
  lightning_sdk/pipeline/__init__.py,sha256=uZS2v8HPGfNz2SrCCVrZ9EhihS3kd6aGtwJLbuaI3dA,167
920
- lightning_sdk/pipeline/pipeline.py,sha256=L1Aj5kRffjBKLUYt6YJDerw2CwCGi8g1TTwHV3qrpfU,3774
920
+ lightning_sdk/pipeline/pipeline.py,sha256=CCRcHUjL24yKm24NMB5xzK1sre6bQ5sY07tMifmpqI0,3762
921
921
  lightning_sdk/pipeline/types.py,sha256=dZYNrWpuytmyjBHh8OUYT8BRwMe1_5PDDSl0AWsXkyA,9662
922
922
  lightning_sdk/pipeline/utils.py,sha256=gccFmSx3VrkDpWtq26eRCnvWT8l4hKdYLntDMYlB5jw,2627
923
923
  lightning_sdk/services/__init__.py,sha256=gSWUjccEhMI9CIWL_nbrFHUK2S6TM2725mEzrLMfK1Y,225
@@ -928,9 +928,9 @@ lightning_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
928
928
  lightning_sdk/utils/dynamic.py,sha256=glUTO1JC9APtQ6Gr9SO02a3zr56-sPAXM5C3NrTpgyQ,1959
929
929
  lightning_sdk/utils/enum.py,sha256=h2JRzqoBcSlUdanFHmkj_j5DleBHAu1esQYUsdNI-hU,4106
930
930
  lightning_sdk/utils/resolve.py,sha256=MALzFO5iVlkZpnEiC9QkyWcTTYksCeHsloG6MCSJl48,6461
931
- lightning_sdk-0.2.2.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
932
- lightning_sdk-0.2.2.dist-info/METADATA,sha256=9XLJ6BN4gnCUuy4FTWyOA4mwG9e3vdoyv5_tktYGcsI,3991
933
- lightning_sdk-0.2.2.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
934
- lightning_sdk-0.2.2.dist-info/entry_points.txt,sha256=msB9PJWIJ784dX-OP8by51d4IbKYH3Fj1vCuA9oXjHY,68
935
- lightning_sdk-0.2.2.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
936
- lightning_sdk-0.2.2.dist-info/RECORD,,
931
+ lightning_sdk-0.2.4.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
932
+ lightning_sdk-0.2.4.dist-info/METADATA,sha256=H8Yv9rh5KKFyNlVa6zU8k8Frb6C4ZeWoaauSIO2DUnk,3991
933
+ lightning_sdk-0.2.4.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
934
+ lightning_sdk-0.2.4.dist-info/entry_points.txt,sha256=msB9PJWIJ784dX-OP8by51d4IbKYH3Fj1vCuA9oXjHY,68
935
+ lightning_sdk-0.2.4.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
936
+ lightning_sdk-0.2.4.dist-info/RECORD,,