lightning-sdk 0.2.9__py3-none-any.whl → 0.2.10__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 (50) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/deployment_api.py +1 -0
  3. lightning_sdk/api/lit_container_api.py +19 -2
  4. lightning_sdk/api/teamspace_api.py +20 -16
  5. lightning_sdk/api/utils.py +1 -1
  6. lightning_sdk/cli/entrypoint.py +2 -2
  7. lightning_sdk/cli/serve.py +141 -16
  8. lightning_sdk/cli/upload.py +4 -1
  9. lightning_sdk/deployment/deployment.py +5 -2
  10. lightning_sdk/lightning_cloud/openapi/__init__.py +5 -0
  11. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +98 -1
  12. lightning_sdk/lightning_cloud/openapi/models/__init__.py +5 -0
  13. lightning_sdk/lightning_cloud/openapi/models/cluster_id_capacityreservations_body.py +55 -3
  14. lightning_sdk/lightning_cloud/openapi/models/create.py +53 -1
  15. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +55 -3
  16. lightning_sdk/lightning_cloud/openapi/models/update.py +27 -1
  17. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +53 -1
  18. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_cold_start_metrics_stats.py +357 -0
  19. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +29 -3
  20. lightning_sdk/lightning_cloud/openapi/models/v1_cloudflare_v1.py +227 -0
  21. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  22. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +55 -3
  23. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +53 -1
  24. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  25. lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +29 -3
  26. lightning_sdk/lightning_cloud/openapi/models/v1_create_cluster_capacity_reservation_response.py +27 -1
  27. lightning_sdk/lightning_cloud/openapi/models/v1_data_connection.py +27 -1
  28. lightning_sdk/lightning_cloud/openapi/models/v1_gcp_direct_vpc.py +149 -0
  29. lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_cold_start_metrics_stats_response.py +123 -0
  30. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +53 -1
  31. lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +17 -17
  32. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +55 -3
  33. lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/v1_r2_data_connection.py +253 -0
  35. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
  36. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +50 -24
  37. lightning_sdk/lightning_cloud/openapi/models/v1_validate_data_connection_response.py +27 -1
  38. lightning_sdk/lightning_cloud/openapi/models/v1_weka_data_connection.py +29 -55
  39. lightning_sdk/lightning_cloud/openapi/models/validate.py +27 -1
  40. lightning_sdk/lit_container.py +12 -2
  41. lightning_sdk/models.py +22 -9
  42. lightning_sdk/serve.py +3 -0
  43. lightning_sdk/teamspace.py +2 -0
  44. lightning_sdk/utils/resolve.py +11 -4
  45. {lightning_sdk-0.2.9.dist-info → lightning_sdk-0.2.10.dist-info}/METADATA +1 -1
  46. {lightning_sdk-0.2.9.dist-info → lightning_sdk-0.2.10.dist-info}/RECORD +50 -45
  47. {lightning_sdk-0.2.9.dist-info → lightning_sdk-0.2.10.dist-info}/LICENSE +0 -0
  48. {lightning_sdk-0.2.9.dist-info → lightning_sdk-0.2.10.dist-info}/WHEEL +0 -0
  49. {lightning_sdk-0.2.9.dist-info → lightning_sdk-0.2.10.dist-info}/entry_points.txt +0 -0
  50. {lightning_sdk-0.2.9.dist-info → lightning_sdk-0.2.10.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.9"
34
+ __version__ = "0.2.10"
35
35
  _check_version_and_prompt_upgrade(__version__)
36
36
  _set_tqdm_envvars_noninteractive()
@@ -228,6 +228,7 @@ class DeploymentApi:
228
228
  return self._client.jobs_service_create_deployment(
229
229
  project_id=deployment.project_id,
230
230
  body=CreateDeploymentRequestDefinesASpecForTheJobThatAllowsForAutoscalingJobs(
231
+ cloudspace_id=deployment.cloudspace_id,
231
232
  autoscaling=deployment.autoscaling,
232
233
  cluster_id=deployment.spec.cluster_id,
233
234
  endpoint=deployment.endpoint,
@@ -7,6 +7,7 @@ import requests
7
7
  from rich.console import Console
8
8
 
9
9
  from lightning_sdk.api.utils import _get_registry_url
10
+ from lightning_sdk.lightning_cloud.env import LIGHTNING_CLOUD_URL
10
11
  from lightning_sdk.lightning_cloud.openapi.models import V1DeleteLitRepositoryResponse
11
12
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
12
13
  from lightning_sdk.teamspace import Teamspace
@@ -126,8 +127,14 @@ class LitContainerApi:
126
127
 
127
128
  @retry_on_lcr_auth_failure
128
129
  def upload_container(
129
- self, container: str, teamspace: Teamspace, tag: str, cloud_account: str, platform: str
130
- ) -> Generator[dict, None, None]:
130
+ self,
131
+ container: str,
132
+ teamspace: Teamspace,
133
+ tag: str,
134
+ cloud_account: str,
135
+ platform: str,
136
+ return_final_dict: bool = False,
137
+ ) -> Generator[dict, None, Dict]:
131
138
  """Upload container will push the container to LitCR.
132
139
 
133
140
  It uses docker push API to interact with docker daemon which will then push the container to a storage
@@ -140,6 +147,7 @@ class LitContainerApi:
140
147
  Named cloud-account in the CLI options.
141
148
  :param platform: If empty will be linux/amd64. This is important because our entire deployment infra runs on
142
149
  linux/amd64. Will show user a warning otherwise.
150
+ :return_final_dict: Controls whether we respond with the dictionary containing metadata about container upload
143
151
  :return: Generator[dict, None, dict]
144
152
  """
145
153
  try:
@@ -166,6 +174,15 @@ class LitContainerApi:
166
174
  raise ValueError(f"Could not tag container {container}:{tag} with {repository}:{tag}")
167
175
  yield from self._push_with_retry(repository, tag=tag)
168
176
 
177
+ if return_final_dict:
178
+ yield {
179
+ "finish": True,
180
+ "url": f"{LIGHTNING_CLOUD_URL}/{teamspace.owner.name}/{teamspace.name}/containers/"
181
+ f"{container_basename}?section=tags"
182
+ f"{f'?clusterId={cloud_account}' if cloud_account is not None else ''}",
183
+ "repository": repository,
184
+ }
185
+
169
186
  def _push_with_retry(self, repository: str, tag: str, max_retries: int = 3) -> Iterator[Dict[str, Any]]:
170
187
  def is_auth_error(error_msg: str) -> bool:
171
188
  auth_errors = ["unauthorized", "authentication required", "unauth"]
@@ -34,7 +34,7 @@ class TeamspaceApi:
34
34
 
35
35
  def __init__(self) -> None:
36
36
  self._client = LightningClient(max_tries=7)
37
- self._models: Optional[ModelsStoreApi] = None
37
+ self._models_api: Optional[ModelsStoreApi] = None
38
38
 
39
39
  def get_teamspace(self, name: str, owner_id: str) -> V1Project:
40
40
  """Get the current teamspace from the owner."""
@@ -166,12 +166,12 @@ class TeamspaceApi:
166
166
 
167
167
  # lazy property which is only created when needed
168
168
  @property
169
- def models(self) -> ModelsStoreApi:
170
- if not self._models:
171
- self._models = ModelsStoreApi(self._client.api_client)
172
- return self._models
169
+ def models_api(self) -> ModelsStoreApi:
170
+ if not self._models_api:
171
+ self._models_api = ModelsStoreApi(self._client.api_client)
172
+ return self._models_api
173
173
 
174
- def get_model_version(self, name: str, version: str, teamspace_id: str) -> V1ModelVersionArchive:
174
+ def get_model_version(self, name: str, version: Optional[str], teamspace_id: str) -> V1ModelVersionArchive:
175
175
  return _get_model_version(client=self._client, name=name, version=version, teamspace_id=teamspace_id)
176
176
 
177
177
  def create_model(
@@ -184,14 +184,14 @@ class TeamspaceApi:
184
184
  cloud_account: str,
185
185
  ) -> V1ModelVersionArchive:
186
186
  # ask if such model already exists by listing models with specific name
187
- models = self.models.models_store_list_models(project_id=teamspace_id, name=name).models
187
+ models = self.models_api.models_store_list_models(project_id=teamspace_id, name=name).models
188
188
  if len(models) == 0:
189
- return self.models.models_store_create_model(
189
+ return self.models_api.models_store_create_model(
190
190
  body=ProjectIdModelsBody(cluster_id=cloud_account, metadata=metadata, name=name, private=private),
191
191
  project_id=teamspace_id,
192
192
  )
193
193
  assert len(models) == 1, "Multiple models with the same name found"
194
- return self.models.models_store_create_model_version(
194
+ return self.models_api.models_store_create_model_version(
195
195
  body=ModelIdVersionsBody(cluster_id=cloud_account, version=version),
196
196
  project_id=teamspace_id,
197
197
  model_id=models[0].id,
@@ -199,16 +199,18 @@ class TeamspaceApi:
199
199
 
200
200
  def delete_model(self, name: str, version: Optional[str], teamspace_id: str) -> None:
201
201
  """Delete a model or a version from the model store."""
202
- models = self.models.models_store_list_models(project_id=teamspace_id, name=name).models
202
+ models = self.models_api.models_store_list_models(project_id=teamspace_id, name=name).models
203
203
  assert len(models) == 1, "Multiple models with the same name found"
204
- model_id = models[0].id
204
+ model = models[0]
205
205
  # decide if delete only version of whole model
206
206
  if version:
207
207
  if version == "default":
208
- version = models[0].default_version
209
- self.models.models_store_delete_model_version(project_id=teamspace_id, model_id=model_id, version=version)
208
+ version = model.default_version
209
+ self.models_api.models_store_delete_model_version(
210
+ project_id=teamspace_id, model_id=model.id, version=version
211
+ )
210
212
  else:
211
- self.models.models_store_delete_model(project_id=teamspace_id, model_id=model_id)
213
+ self.models_api.models_store_delete_model(project_id=teamspace_id, model_id=model.id)
212
214
 
213
215
  def upload_model_file(
214
216
  self,
@@ -256,7 +258,7 @@ class TeamspaceApi:
256
258
  main_pbar.update(1)
257
259
 
258
260
  def complete_model_upload(self, model_id: str, version: str, teamspace_id: str) -> None:
259
- self.models.models_store_complete_model_upload(
261
+ self.models_api.models_store_complete_model_upload(
260
262
  body=_DummyBody(),
261
263
  project_id=teamspace_id,
262
264
  model_id=model_id,
@@ -266,12 +268,14 @@ class TeamspaceApi:
266
268
  def download_model_files(
267
269
  self,
268
270
  name: str,
269
- version: str,
271
+ version: Optional[str],
270
272
  download_dir: Path,
271
273
  teamspace_name: str,
272
274
  teamspace_owner_name: str,
273
275
  progress_bar: bool = True,
274
276
  ) -> List[str]:
277
+ if version is None:
278
+ version = "default"
275
279
  return _download_model_files(
276
280
  client=self._client,
277
281
  teamspace_name=teamspace_name,
@@ -493,7 +493,7 @@ def _get_model_version(client: LightningClient, teamspace_id: str, name: str, ve
493
493
  raise ValueError(f"Model `{name}` does not exist")
494
494
  elif len(models) > 1:
495
495
  raise ValueError("Multiple models with the same name found")
496
- if version == ("default"):
496
+ if version is None or version == "default":
497
497
  return models[0].default_version
498
498
  versions = api.models_store_list_model_versions(project_id=teamspace_id, model_id=models[0].id).versions
499
499
  if not versions:
@@ -21,7 +21,7 @@ from lightning_sdk.cli.inspect import inspect
21
21
  from lightning_sdk.cli.list import list_cli
22
22
  from lightning_sdk.cli.open import open
23
23
  from lightning_sdk.cli.run import run
24
- from lightning_sdk.cli.serve import serve
24
+ from lightning_sdk.cli.serve import deploy
25
25
  from lightning_sdk.cli.start import start
26
26
  from lightning_sdk.cli.stop import stop
27
27
  from lightning_sdk.cli.switch import switch
@@ -76,7 +76,7 @@ main_cli.add_command(generate)
76
76
  main_cli.add_command(inspect)
77
77
  main_cli.add_command(list_cli)
78
78
  main_cli.add_command(run)
79
- main_cli.add_command(serve)
79
+ main_cli.add_command(deploy)
80
80
  main_cli.add_command(start)
81
81
  main_cli.add_command(stop)
82
82
  main_cli.add_command(switch)
@@ -1,10 +1,12 @@
1
1
  import os
2
+ import socket
2
3
  import subprocess
3
4
  import time
4
5
  import webbrowser
5
6
  from datetime import datetime
7
+ from enum import Enum
6
8
  from pathlib import Path
7
- from typing import Optional, Union
9
+ from typing import List, Optional, TypedDict, Union
8
10
  from urllib.parse import urlencode
9
11
 
10
12
  import click
@@ -18,10 +20,13 @@ from lightning_sdk.api.lit_container_api import LitContainerApi
18
20
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
19
21
  from lightning_sdk.lightning_cloud import env
20
22
  from lightning_sdk.lightning_cloud.login import Auth, AuthServer
23
+ from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
24
+ from lightning_sdk.lightning_cloud.rest_client import LightningClient
21
25
  from lightning_sdk.serve import _LitServeDeployer
22
26
  from lightning_sdk.utils.resolve import _get_authed_user, _resolve_teamspace
23
27
 
24
28
  _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
29
+ _POLL_TIMEOUT = 600
25
30
 
26
31
 
27
32
  class _ServeGroup(click.Group):
@@ -33,22 +38,22 @@ class _ServeGroup(click.Group):
33
38
  return super().parse_args(ctx, args)
34
39
 
35
40
 
36
- @click.group("serve", cls=_ServeGroup)
37
- def serve() -> None:
38
- """Serve a LitServe model.
41
+ @click.group("deploy", cls=_ServeGroup)
42
+ def deploy() -> None:
43
+ """Deploy a LitServe model.
39
44
 
40
45
  Example:
41
- lightning serve server.py # deploy to the cloud
46
+ lightning deploy server.py # deploy to the cloud
42
47
 
43
48
  Example:
44
- lightning serve server.py --local # serve locally
49
+ lightning deploy server.py --local # run locally
45
50
 
46
- You can deploy the API to the cloud by running `lightning serve server.py`.
51
+ You can deploy the API to the cloud by running `lightning deploy server.py`.
47
52
  This will build a docker container for the server.py script and deploy it to the Lightning AI platform.
48
53
  """
49
54
 
50
55
 
51
- @serve.command("api")
56
+ @deploy.command("api")
52
57
  @click.argument("script-path", type=click.Path(exists=True))
53
58
  @click.option(
54
59
  "--easy",
@@ -272,20 +277,127 @@ def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optiona
272
277
  return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
273
278
 
274
279
 
275
- def poll_verified_status() -> bool:
280
+ class _UserStatus(TypedDict):
281
+ verified: bool
282
+ onboarded: bool
283
+
284
+
285
+ def poll_verified_status(timeout: int = _POLL_TIMEOUT) -> _UserStatus:
276
286
  """Polls the verified status of the user until it is True or a timeout occurs."""
277
287
  user_api = UserApi()
278
288
  user = _get_authed_user()
279
289
  start_time = datetime.now()
280
- timeout = 600 # 10 minutes
290
+ result = {"onboarded": False, "verified": False}
281
291
  while True:
282
292
  user_resp = user_api.get_user(name=user.name)
293
+ result["onboarded"] = user_resp.status.completed_project_onboarding
294
+ result["verified"] = user_resp.status.verified
283
295
  if user_resp.status.verified:
284
- return True
296
+ return result
285
297
  if (datetime.now() - start_time).total_seconds() > timeout:
286
298
  break
287
299
  time.sleep(5)
288
- return False
300
+ return result
301
+
302
+
303
+ class _OnboardingStatus(Enum):
304
+ NOT_VERIFIED = "not_verified"
305
+ ONBOARDING = "onboarding"
306
+ ONBOARDED = "onboarded"
307
+
308
+
309
+ class _Onboarding:
310
+ def __init__(self, console: Console) -> None:
311
+ self.console = console
312
+ self.user = _get_authed_user()
313
+ self.user_api = UserApi()
314
+ self.client = LightningClient(max_tries=7)
315
+
316
+ @property
317
+ def verified(self) -> bool:
318
+ return self.user_api.get_user(name=self.user.name).status.verified
319
+
320
+ @property
321
+ def is_onboarded(self) -> bool:
322
+ return self.user_api.get_user(name=self.user.name).status.completed_project_onboarding
323
+
324
+ @property
325
+ def can_join_org(self) -> bool:
326
+ return len(self.client.organizations_service_list_joinable_organizations().joinable_organizations) > 0
327
+
328
+ @property
329
+ def status(self) -> _OnboardingStatus:
330
+ if not self.verified:
331
+ return _OnboardingStatus.NOT_VERIFIED
332
+ if self.is_onboarded:
333
+ return _OnboardingStatus.ONBOARDED
334
+ return _OnboardingStatus.ONBOARDING
335
+
336
+ def _wait(self, timeout: int = _POLL_TIMEOUT) -> None:
337
+ """Wait for user onboarding if they can join the teamspace otherwise move to select a teamspace."""
338
+ status = self.status
339
+ if status == _OnboardingStatus.ONBOARDED:
340
+ return
341
+
342
+ start_time = datetime.now()
343
+ while self.status != _OnboardingStatus.ONBOARDED:
344
+ time.sleep(5)
345
+ if self.is_onboarded:
346
+ return
347
+ if (datetime.now() - start_time).total_seconds() > timeout:
348
+ break
349
+
350
+ raise RuntimeError("Timed out waiting for onboarding status")
351
+
352
+ def get_cloudspace_id(self, teamspace: Teamspace) -> Optional[str]:
353
+ cloudspaces: List[V1CloudSpace] = self.client.cloud_space_service_list_cloud_spaces(teamspace.id).cloudspaces
354
+ for cloudspace in cloudspaces:
355
+ if "scratch-studio" in cloudspace.name or "scratch-studio" in cloudspace.display_name:
356
+ return cloudspace.id
357
+ return None
358
+
359
+ def select_teamspace(self, teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
360
+ """Select a teamspace while onboarding.
361
+
362
+ If user is being onboarded and can't join any org, the teamspace it will be resolved to the default
363
+ personal teamspace.
364
+ If user is being onboarded and can join an org then it will select default teamspace from the org.
365
+ """
366
+ if self.is_onboarded:
367
+ return select_teamspace(teamspace, org, user)
368
+
369
+ # Run only when user hasn't completed onboarding yet.
370
+ menu = _TeamspacesMenu()
371
+ possible_teamspaces = menu._get_possible_teamspaces(self.user)
372
+ can_join_org = self.can_join_org
373
+
374
+ if len(possible_teamspaces) == 1 and can_join_org:
375
+ # wait for onboarding to complete so that user can join an org
376
+ # create deployment in the org default teamspace
377
+ self.console.print("Waiting for account setup. Visit lightning.ai")
378
+ self._wait()
379
+
380
+ possible_teamspaces = menu._get_possible_teamspaces(self.user)
381
+ if len(possible_teamspaces) == 1:
382
+ # User didn't select any org
383
+ value = next(iter(possible_teamspaces.values()))
384
+ return Teamspace(name=value["name"], org=value["org"], user=value["user"])
385
+
386
+ for _, value in possible_teamspaces.items():
387
+ # User select an org
388
+ # Onboarding teamspace will be the default teamspace in the selected org
389
+ if value["org"]:
390
+ return Teamspace(name=value["name"], org=value["org"], user=value["user"])
391
+ raise RuntimeError("Unable to select teamspace. Visit lightning.ai")
392
+
393
+
394
+ def is_connected(host: str = "8.8.8.8", port: int = 53, timeout: int = 10) -> bool:
395
+ try:
396
+ socket.setdefaulttimeout(timeout)
397
+ socket.create_connection((host, port))
398
+ return True
399
+ except OSError:
400
+ return False
289
401
 
290
402
 
291
403
  def _handle_cloud(
@@ -306,6 +418,11 @@ def _handle_cloud(
306
418
  replicas: Optional[int] = 1,
307
419
  include_credentials: Optional[bool] = True,
308
420
  ) -> None:
421
+ if not is_connected():
422
+ console.print("❌ Internet connection required to deploy to the cloud.", style="red")
423
+ console.print("To run locally instead, use: `lightning serve [SCRIPT | server.py] --local`")
424
+ return
425
+
309
426
  deployment_name = os.path.basename(repository)
310
427
  tag = tag if tag else "latest"
311
428
 
@@ -346,11 +463,17 @@ def _handle_cloud(
346
463
  console.print("\nPushing container to registry. It may take a while...", style="bold")
347
464
  # Authenticate with LitServe affiliate
348
465
  authenticate(shall_confirm=not non_interactive)
349
- resolved_teamspace = select_teamspace(teamspace, org, user)
350
- verified = poll_verified_status()
351
- if not verified:
466
+ user_status = poll_verified_status()
467
+ cloudspace_id: Optional[str] = None
468
+ if not user_status["verified"]:
352
469
  console.print("❌ Verify phone number to continue. Visit lightning.ai.", style="red")
353
470
  return
471
+ if not user_status["onboarded"]:
472
+ onboarding = _Onboarding(console)
473
+ resolved_teamspace = onboarding.select_teamspace(teamspace, org, user)
474
+ cloudspace_id = onboarding.get_cloudspace_id(resolved_teamspace)
475
+ else:
476
+ resolved_teamspace = select_teamspace(teamspace, org, user)
354
477
 
355
478
  # list containers to create the project if it doesn't exist
356
479
  lit_cr = LitContainerApi()
@@ -393,6 +516,8 @@ def _handle_cloud(
393
516
  max_replica=max_replica,
394
517
  replicas=replicas,
395
518
  include_credentials=include_credentials,
519
+ cloudspace_id=cloudspace_id,
396
520
  )
397
521
  console.print(f"🚀 Deployment started, access at [i]{deployment_status.get('url')}[/i]")
398
- webbrowser.open(deployment_status.get("url"))
522
+ if user_status["onboarded"]:
523
+ webbrowser.open(deployment_status.get("url"))
@@ -1,6 +1,7 @@
1
1
  import concurrent.futures
2
2
  import json
3
3
  import os
4
+ import webbrowser
4
5
  from pathlib import Path
5
6
  from typing import Dict, Generator, List, Optional
6
7
 
@@ -162,7 +163,7 @@ def upload_container(
162
163
  # let the push with retry take control of auth moving forward
163
164
  pass
164
165
 
165
- lines = api.upload_container(container, teamspace, tag, cloud_account, platform)
166
+ lines = api.upload_container(container, teamspace, tag, cloud_account, platform, return_final_dict=True)
166
167
  _print_docker_push(lines, console, progress, push_task)
167
168
  except DockerNotRunningError as e:
168
169
  e.print_help()
@@ -303,6 +304,8 @@ def _print_docker_push(lines: Generator, console: Console, progress: Progress, p
303
304
  console.print(f"\n[red]{line}[/red]")
304
305
  return
305
306
  elif "finish" in line:
307
+ if "url" in line:
308
+ webbrowser.open(line["url"])
306
309
  console.print(f"Container available at [i]{line['url']}[/i]")
307
310
  return
308
311
  else:
@@ -117,6 +117,7 @@ class Deployment:
117
117
  cloud_account: Optional[str] = None,
118
118
  custom_domain: Optional[str] = None,
119
119
  cluster: Optional[str] = None, # deprecated in favor of cloud_account
120
+ cloudspace_id: Optional[str] = None,
120
121
  quantity: Optional[int] = None,
121
122
  include_credentials: Optional[bool] = None,
122
123
  ) -> None:
@@ -141,9 +142,10 @@ class Deployment:
141
142
  auth: The auth config to protect your services. Only Basic and Token supported.
142
143
  cloud_account: The name of the cloud account, the studio should be created on.
143
144
  Doesn't matter when the studio already exists.
144
- custom_domain: Whether your service would be referenced under a custom doamin.
145
+ custom_domain: Whether your service would be referenced under a custom domain.
146
+ cloudspace_id: Connect deployment to a Studio.
145
147
  quantity: The number of machines per replica to deploy.
146
- include_credentials: Whether to include the environement variables for the SDK to authenticate
148
+ include_credentials: Whether to include the environment variables for the SDK to authenticate
147
149
 
148
150
  Note:
149
151
  Since a teamspace can either be owned by an org or by a user directly,
@@ -166,6 +168,7 @@ class Deployment:
166
168
  name=self._name,
167
169
  project_id=self._teamspace.id,
168
170
  replicas=replicas,
171
+ cloudspace_id=cloudspace_id,
169
172
  spec=to_spec(
170
173
  cloud_account=cloud_account,
171
174
  command=command,
@@ -278,6 +278,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_artifact_event_
278
278
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_code_version import V1CloudSpaceCodeVersion
279
279
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_code_version_status import V1CloudSpaceCodeVersionStatus
280
280
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_cold_start_metrics import V1CloudSpaceColdStartMetrics
281
+ from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_cold_start_metrics_stats import V1CloudSpaceColdStartMetricsStats
281
282
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_engagement_response import V1CloudSpaceEngagementResponse
282
283
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_environment_template import V1CloudSpaceEnvironmentTemplate
283
284
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_environment_template_config import V1CloudSpaceEnvironmentTemplateConfig
@@ -293,6 +294,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_session import
293
294
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_state import V1CloudSpaceState
294
295
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_version import V1CloudSpaceVersion
295
296
  from lightning_sdk.lightning_cloud.openapi.models.v1_cloud_space_version_publication import V1CloudSpaceVersionPublication
297
+ from lightning_sdk.lightning_cloud.openapi.models.v1_cloudflare_v1 import V1CloudflareV1
296
298
  from lightning_sdk.lightning_cloud.openapi.models.v1_cluster_accelerator import V1ClusterAccelerator
297
299
  from lightning_sdk.lightning_cloud.openapi.models.v1_cluster_availability import V1ClusterAvailability
298
300
  from lightning_sdk.lightning_cloud.openapi.models.v1_cluster_capacity_reservation import V1ClusterCapacityReservation
@@ -484,6 +486,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_filesystem_work import V1Fi
484
486
  from lightning_sdk.lightning_cloud.openapi.models.v1_find_capacity_block_offering_response import V1FindCapacityBlockOfferingResponse
485
487
  from lightning_sdk.lightning_cloud.openapi.models.v1_flowserver import V1Flowserver
486
488
  from lightning_sdk.lightning_cloud.openapi.models.v1_folder_index_status import V1FolderIndexStatus
489
+ from lightning_sdk.lightning_cloud.openapi.models.v1_gcp_direct_vpc import V1GCPDirectVPC
487
490
  from lightning_sdk.lightning_cloud.openapi.models.v1_gcs_folder_data_connection import V1GCSFolderDataConnection
488
491
  from lightning_sdk.lightning_cloud.openapi.models.v1_gpu_system_metrics import V1GPUSystemMetrics
489
492
  from lightning_sdk.lightning_cloud.openapi.models.v1_gallery_app import V1GalleryApp
@@ -495,6 +498,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_get_affiliate_link_response
495
498
  from lightning_sdk.lightning_cloud.openapi.models.v1_get_agent_job_env_response import V1GetAgentJobEnvResponse
496
499
  from lightning_sdk.lightning_cloud.openapi.models.v1_get_agent_job_logs_metadata_response import V1GetAgentJobLogsMetadataResponse
497
500
  from lightning_sdk.lightning_cloud.openapi.models.v1_get_artifacts_page_response import V1GetArtifactsPageResponse
501
+ from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_cold_start_metrics_stats_response import V1GetCloudSpaceColdStartMetricsStatsResponse
498
502
  from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_instance_status_response import V1GetCloudSpaceInstanceStatusResponse
499
503
  from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_instance_system_metrics_aggregate_response import V1GetCloudSpaceInstanceSystemMetricsAggregateResponse
500
504
  from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_size_response import V1GetCloudSpaceSizeResponse
@@ -770,6 +774,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_quest_status import V1Quest
770
774
  from lightning_sdk.lightning_cloud.openapi.models.v1_queue_server_type import V1QueueServerType
771
775
  from lightning_sdk.lightning_cloud.openapi.models.v1_quotas import V1Quotas
772
776
  from lightning_sdk.lightning_cloud.openapi.models.v1_quote_subscription_response import V1QuoteSubscriptionResponse
777
+ from lightning_sdk.lightning_cloud.openapi.models.v1_r2_data_connection import V1R2DataConnection
773
778
  from lightning_sdk.lightning_cloud.openapi.models.v1_refresh_index_response import V1RefreshIndexResponse
774
779
  from lightning_sdk.lightning_cloud.openapi.models.v1_refresh_path_response import V1RefreshPathResponse
775
780
  from lightning_sdk.lightning_cloud.openapi.models.v1_refresh_request import V1RefreshRequest
@@ -3174,6 +3174,99 @@ class CloudSpaceServiceApi(object):
3174
3174
  _request_timeout=params.get('_request_timeout'),
3175
3175
  collection_formats=collection_formats)
3176
3176
 
3177
+ def cloud_space_service_get_cloud_space_cold_start_metrics_stats(self, project_id: 'str', **kwargs) -> 'V1GetCloudSpaceColdStartMetricsStatsResponse': # noqa: E501
3178
+ """cloud_space_service_get_cloud_space_cold_start_metrics_stats # noqa: E501
3179
+
3180
+ This method makes a synchronous HTTP request by default. To make an
3181
+ asynchronous HTTP request, please pass async_req=True
3182
+ >>> thread = api.cloud_space_service_get_cloud_space_cold_start_metrics_stats(project_id, async_req=True)
3183
+ >>> result = thread.get()
3184
+
3185
+ :param async_req bool
3186
+ :param str project_id: (required)
3187
+ :return: V1GetCloudSpaceColdStartMetricsStatsResponse
3188
+ If the method is called asynchronously,
3189
+ returns the request thread.
3190
+ """
3191
+ kwargs['_return_http_data_only'] = True
3192
+ if kwargs.get('async_req'):
3193
+ return self.cloud_space_service_get_cloud_space_cold_start_metrics_stats_with_http_info(project_id, **kwargs) # noqa: E501
3194
+ else:
3195
+ (data) = self.cloud_space_service_get_cloud_space_cold_start_metrics_stats_with_http_info(project_id, **kwargs) # noqa: E501
3196
+ return data
3197
+
3198
+ def cloud_space_service_get_cloud_space_cold_start_metrics_stats_with_http_info(self, project_id: 'str', **kwargs) -> 'V1GetCloudSpaceColdStartMetricsStatsResponse': # noqa: E501
3199
+ """cloud_space_service_get_cloud_space_cold_start_metrics_stats # noqa: E501
3200
+
3201
+ This method makes a synchronous HTTP request by default. To make an
3202
+ asynchronous HTTP request, please pass async_req=True
3203
+ >>> thread = api.cloud_space_service_get_cloud_space_cold_start_metrics_stats_with_http_info(project_id, async_req=True)
3204
+ >>> result = thread.get()
3205
+
3206
+ :param async_req bool
3207
+ :param str project_id: (required)
3208
+ :return: V1GetCloudSpaceColdStartMetricsStatsResponse
3209
+ If the method is called asynchronously,
3210
+ returns the request thread.
3211
+ """
3212
+
3213
+ all_params = ['project_id'] # noqa: E501
3214
+ all_params.append('async_req')
3215
+ all_params.append('_return_http_data_only')
3216
+ all_params.append('_preload_content')
3217
+ all_params.append('_request_timeout')
3218
+
3219
+ params = locals()
3220
+ for key, val in six.iteritems(params['kwargs']):
3221
+ if key not in all_params:
3222
+ raise TypeError(
3223
+ "Got an unexpected keyword argument '%s'"
3224
+ " to method cloud_space_service_get_cloud_space_cold_start_metrics_stats" % key
3225
+ )
3226
+ params[key] = val
3227
+ del params['kwargs']
3228
+ # verify the required parameter 'project_id' is set
3229
+ if ('project_id' not in params or
3230
+ params['project_id'] is None):
3231
+ raise ValueError("Missing the required parameter `project_id` when calling `cloud_space_service_get_cloud_space_cold_start_metrics_stats`") # noqa: E501
3232
+
3233
+ collection_formats = {}
3234
+
3235
+ path_params = {}
3236
+ if 'project_id' in params:
3237
+ path_params['projectId'] = params['project_id'] # noqa: E501
3238
+
3239
+ query_params = []
3240
+
3241
+ header_params = {}
3242
+
3243
+ form_params = []
3244
+ local_var_files = {}
3245
+
3246
+ body_params = None
3247
+ # HTTP header `Accept`
3248
+ header_params['Accept'] = self.api_client.select_header_accept(
3249
+ ['application/json']) # noqa: E501
3250
+
3251
+ # Authentication setting
3252
+ auth_settings = [] # noqa: E501
3253
+
3254
+ return self.api_client.call_api(
3255
+ '/v1/projects/{projectId}/cloudspaces/cold-start-stats', 'GET',
3256
+ path_params,
3257
+ query_params,
3258
+ header_params,
3259
+ body=body_params,
3260
+ post_params=form_params,
3261
+ files=local_var_files,
3262
+ response_type='V1GetCloudSpaceColdStartMetricsStatsResponse', # noqa: E501
3263
+ auth_settings=auth_settings,
3264
+ async_req=params.get('async_req'),
3265
+ _return_http_data_only=params.get('_return_http_data_only'),
3266
+ _preload_content=params.get('_preload_content', True),
3267
+ _request_timeout=params.get('_request_timeout'),
3268
+ collection_formats=collection_formats)
3269
+
3177
3270
  def cloud_space_service_get_cloud_space_folder_index(self, project_id: 'str', id: 'str', **kwargs) -> 'V1GetFolderIndexResponse': # noqa: E501
3178
3271
  """cloud_space_service_get_cloud_space_folder_index # noqa: E501
3179
3272
 
@@ -3499,6 +3592,7 @@ class CloudSpaceServiceApi(object):
3499
3592
 
3500
3593
  :param async_req bool
3501
3594
  :param str project_id: (required)
3595
+ :param str cloudspace_id:
3502
3596
  :return: V1GetCloudSpaceInstanceSystemMetricsAggregateResponse
3503
3597
  If the method is called asynchronously,
3504
3598
  returns the request thread.
@@ -3520,12 +3614,13 @@ class CloudSpaceServiceApi(object):
3520
3614
 
3521
3615
  :param async_req bool
3522
3616
  :param str project_id: (required)
3617
+ :param str cloudspace_id:
3523
3618
  :return: V1GetCloudSpaceInstanceSystemMetricsAggregateResponse
3524
3619
  If the method is called asynchronously,
3525
3620
  returns the request thread.
3526
3621
  """
3527
3622
 
3528
- all_params = ['project_id'] # noqa: E501
3623
+ all_params = ['project_id', 'cloudspace_id'] # noqa: E501
3529
3624
  all_params.append('async_req')
3530
3625
  all_params.append('_return_http_data_only')
3531
3626
  all_params.append('_preload_content')
@@ -3552,6 +3647,8 @@ class CloudSpaceServiceApi(object):
3552
3647
  path_params['projectId'] = params['project_id'] # noqa: E501
3553
3648
 
3554
3649
  query_params = []
3650
+ if 'cloudspace_id' in params:
3651
+ query_params.append(('cloudspaceId', params['cloudspace_id'])) # noqa: E501
3555
3652
 
3556
3653
  header_params = {}
3557
3654