lightning-sdk 0.2.10__py3-none-any.whl → 0.2.12__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 (46) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/deployment_api.py +37 -3
  3. lightning_sdk/api/lit_container_api.py +13 -7
  4. lightning_sdk/api/llm_api.py +34 -0
  5. lightning_sdk/api/teamspace_api.py +29 -4
  6. lightning_sdk/cli/serve.py +87 -36
  7. lightning_sdk/deployment/deployment.py +55 -6
  8. lightning_sdk/lightning_cloud/openapi/__init__.py +5 -1
  9. lightning_sdk/lightning_cloud/openapi/api/cloud_space_environment_template_service_api.py +4 -4
  10. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +13 -1
  11. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +4 -4
  12. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +115 -0
  13. lightning_sdk/lightning_cloud/openapi/models/__init__.py +5 -1
  14. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +27 -1
  15. lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +27 -1
  16. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
  17. lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +27 -1
  18. lightning_sdk/lightning_cloud/openapi/models/update.py +65 -195
  19. lightning_sdk/lightning_cloud/openapi/models/update1.py +357 -0
  20. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
  21. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template.py +27 -1
  22. lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +27 -1
  23. lightning_sdk/lightning_cloud/openapi/models/v1_delete_cloud_space_environment_template_response.py +1 -53
  24. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +1 -27
  25. lightning_sdk/lightning_cloud/openapi/models/v1_job_resource.py +279 -0
  26. lightning_sdk/lightning_cloud/openapi/models/v1_job_type.py +108 -0
  27. lightning_sdk/lightning_cloud/openapi/models/v1_list_job_resources_response.py +123 -0
  28. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +55 -1
  29. lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +29 -1
  30. lightning_sdk/lightning_cloud/openapi/models/v1_reservation_billing_session.py +279 -0
  31. lightning_sdk/lightning_cloud/openapi/models/v1_resources.py +55 -3
  32. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +1 -27
  33. lightning_sdk/lightning_cloud/openapi/models/v1_usage.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +105 -1
  35. lightning_sdk/llm/__init__.py +3 -0
  36. lightning_sdk/llm/llm.py +56 -0
  37. lightning_sdk/models.py +17 -1
  38. lightning_sdk/serve.py +7 -6
  39. lightning_sdk/teamspace.py +14 -2
  40. {lightning_sdk-0.2.10.dist-info → lightning_sdk-0.2.12.dist-info}/METADATA +1 -1
  41. {lightning_sdk-0.2.10.dist-info → lightning_sdk-0.2.12.dist-info}/RECORD +45 -38
  42. lightning_sdk/lightning_cloud/openapi/models/environmenttemplates_id_body.py +0 -253
  43. {lightning_sdk-0.2.10.dist-info → lightning_sdk-0.2.12.dist-info}/LICENSE +0 -0
  44. {lightning_sdk-0.2.10.dist-info → lightning_sdk-0.2.12.dist-info}/WHEEL +0 -0
  45. {lightning_sdk-0.2.10.dist-info → lightning_sdk-0.2.12.dist-info}/entry_points.txt +0 -0
  46. {lightning_sdk-0.2.10.dist-info → lightning_sdk-0.2.12.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.10"
34
+ __version__ = "0.2.12"
35
35
  _check_version_and_prompt_upgrade(__version__)
36
36
  _set_tqdm_envvars_noninteractive()
@@ -224,6 +224,7 @@ class DeploymentApi:
224
224
  def create_deployment(
225
225
  self,
226
226
  deployment: V1Deployment,
227
+ from_onboarding: Optional[bool] = None,
227
228
  ) -> V1Deployment:
228
229
  return self._client.jobs_service_create_deployment(
229
230
  project_id=deployment.project_id,
@@ -236,6 +237,7 @@ class DeploymentApi:
236
237
  replicas=deployment.replicas,
237
238
  spec=deployment.spec,
238
239
  strategy=deployment.strategy,
240
+ from_onboarding=from_onboarding,
239
241
  ),
240
242
  )
241
243
 
@@ -458,7 +460,7 @@ def to_autoscaling(
458
460
  if target_metrics is None and (threshold < 0 or threshold > 100):
459
461
  raise ValueError("The autoscaling threshold should be defined between 0 and 100.")
460
462
 
461
- if target_metrics is not None and len(target_metrics) == 0:
463
+ if target_metrics is not None and len(target_metrics) == 0 and metric is None:
462
464
  raise ValueError("The target_metrics must be provided.")
463
465
 
464
466
  if target_metrics is not None:
@@ -522,8 +524,14 @@ def to_endpoint(
522
524
  def to_health_check(
523
525
  health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None
524
526
  ) -> Optional[V1JobHealthCheckConfig]:
527
+ # Use Default health check if none is provided
525
528
  if not health_check:
526
- return None
529
+ return V1JobHealthCheckConfig(
530
+ failure_threshold=600,
531
+ initial_delay_seconds=0,
532
+ interval_seconds=1,
533
+ timeout_seconds=600,
534
+ )
527
535
 
528
536
  health_check_config = V1JobHealthCheckConfig(
529
537
  failure_threshold=health_check.failure_threshold,
@@ -553,6 +561,7 @@ def to_spec(
553
561
  health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None,
554
562
  quantity: Optional[int] = None,
555
563
  include_credentials: Optional[bool] = None,
564
+ cloudspace_id: Optional[None] = None,
556
565
  ) -> V1JobSpec:
557
566
  if cloud_account is None:
558
567
  raise ValueError("The cloud account should be defined.")
@@ -560,9 +569,15 @@ def to_spec(
560
569
  if machine is None:
561
570
  raise ValueError("The machine should be defined.")
562
571
 
563
- if image is None:
572
+ if image is None and cloudspace_id is None:
564
573
  raise ValueError("The image should be defined.")
565
574
 
575
+ if entrypoint is not None and cloudspace_id is not None:
576
+ raise ValueError("The entrypoint shouldn't be defined when a Studio is provided.")
577
+
578
+ if command is None and cloudspace_id is not None:
579
+ raise ValueError("The command should be defined.")
580
+
566
581
  return V1JobSpec(
567
582
  cluster_id=cloud_account,
568
583
  command=command,
@@ -574,6 +589,7 @@ def to_spec(
574
589
  readiness_probe=to_health_check(health_check),
575
590
  quantity=quantity,
576
591
  include_credentials=include_credentials,
592
+ cloudspace_id=cloudspace_id,
577
593
  )
578
594
 
579
595
 
@@ -598,3 +614,21 @@ def apply_change(spec: Any, key: str, value: Any) -> bool:
598
614
  return True
599
615
 
600
616
  return False
617
+
618
+
619
+ def compose_commands(commands: List[str]) -> str:
620
+ composite_command = []
621
+
622
+ for command in commands:
623
+ command = command.strip()
624
+
625
+ # Check if the command already has '&'
626
+ if command.endswith("&"):
627
+ # It's a background command, add it as a subshell without further adjustment
628
+ composite_command.append(f"( {command} )")
629
+ else:
630
+ # Sequential execution, add as-is and use `&&` to connect if followed by another command
631
+ composite_command.append(command)
632
+
633
+ # Joining commands, using `&&` between sequential parts and respecting subshell backgrounds
634
+ return " && ".join(composite_command)
@@ -125,6 +125,17 @@ class LitContainerApi:
125
125
  except Exception as e:
126
126
  raise ValueError(f"Could not delete container {container} from project {project_id}: {e!s}") from e
127
127
 
128
+ def get_container_url(
129
+ self, repository: str, tag: str, teamspace: Teamspace, cloud_account: Optional[str] = None
130
+ ) -> str:
131
+ """Docker container will be pushed to the URL returned from this function."""
132
+ registry_url = _get_registry_url()
133
+ container_basename = repository.split("/")[-1]
134
+ return (
135
+ f"{registry_url}/lit-container{f'-{cloud_account}' if cloud_account is not None else ''}/"
136
+ f"{teamspace.owner.name}/{teamspace.name}/{container_basename}"
137
+ )
138
+
128
139
  @retry_on_lcr_auth_failure
129
140
  def upload_container(
130
141
  self,
@@ -147,7 +158,6 @@ class LitContainerApi:
147
158
  Named cloud-account in the CLI options.
148
159
  :param platform: If empty will be linux/amd64. This is important because our entire deployment infra runs on
149
160
  linux/amd64. Will show user a warning otherwise.
150
- :return_final_dict: Controls whether we respond with the dictionary containing metadata about container upload
151
161
  :return: Generator[dict, None, dict]
152
162
  """
153
163
  try:
@@ -163,18 +173,14 @@ class LitContainerApi:
163
173
  except Exception as e:
164
174
  raise ValueError(f"Unable to upload {container}:{tag}") from e
165
175
 
166
- registry_url = _get_registry_url()
167
- container_basename = container.split("/")[-1]
168
- repository = (
169
- f"{registry_url}/lit-container{f'-{cloud_account}' if cloud_account is not None else ''}/"
170
- f"{teamspace.owner.name}/{teamspace.name}/{container_basename}"
171
- )
176
+ repository = self.get_container_url(container, tag, teamspace, cloud_account)
172
177
  tagged = self._docker_client.api.tag(f"{container}:{tag}", repository, tag)
173
178
  if not tagged:
174
179
  raise ValueError(f"Could not tag container {container}:{tag} with {repository}:{tag}")
175
180
  yield from self._push_with_retry(repository, tag=tag)
176
181
 
177
182
  if return_final_dict:
183
+ container_basename = repository.split("/")[-1]
178
184
  yield {
179
185
  "finish": True,
180
186
  "url": f"{LIGHTNING_CLOUD_URL}/{teamspace.owner.name}/{teamspace.name}/containers/"
@@ -0,0 +1,34 @@
1
+ from typing import List, Optional
2
+
3
+ from lightning_sdk.lightning_cloud.openapi.models.v1_conversation_response_chunk import V1ConversationResponseChunk
4
+ from lightning_sdk.lightning_cloud.rest_client import LightningClient
5
+
6
+
7
+ class LLMApi:
8
+ def __init__(self) -> None:
9
+ self._client = LightningClient(retry=False, max_tries=0)
10
+
11
+ def list_models(self) -> List[str]:
12
+ result = self._client.assistants_service_list_assistant_managed_endpoints()
13
+ return result.endpoints
14
+
15
+ def get_public_models(self) -> List[str]:
16
+ result = self._client.assistants_service_list_assistants(published=True)
17
+ return result.assistants
18
+
19
+ def start_conversation(
20
+ self, prompt: str, system_prompt: Optional[str], assistant_id: str
21
+ ) -> V1ConversationResponseChunk:
22
+ body = {
23
+ "message": {
24
+ "author": {"role": "user"},
25
+ "content": [
26
+ {
27
+ "contentType": "text",
28
+ "parts": [prompt],
29
+ }
30
+ ],
31
+ },
32
+ }
33
+ result = self._client.assistants_service_start_conversation(body, assistant_id)
34
+ return result.result
@@ -17,6 +17,7 @@ from lightning_sdk.lightning_cloud.openapi import (
17
17
  V1ClusterAccelerator,
18
18
  V1Endpoint,
19
19
  V1Job,
20
+ V1Model,
20
21
  V1ModelVersionArchive,
21
22
  V1MultiMachineJob,
22
23
  V1Project,
@@ -199,9 +200,7 @@ class TeamspaceApi:
199
200
 
200
201
  def delete_model(self, name: str, version: Optional[str], teamspace_id: str) -> None:
201
202
  """Delete a model or a version from the model store."""
202
- models = self.models_api.models_store_list_models(project_id=teamspace_id, name=name).models
203
- assert len(models) == 1, "Multiple models with the same name found"
204
- model = models[0]
203
+ model = self.get_model(teamspace_id=teamspace_id, model_name=name)
205
204
  # decide if delete only version of whole model
206
205
  if version:
207
206
  if version == "default":
@@ -257,7 +256,7 @@ class TeamspaceApi:
257
256
  if main_pbar:
258
257
  main_pbar.update(1)
259
258
 
260
- def complete_model_upload(self, model_id: str, version: str, teamspace_id: str) -> None:
259
+ def _complete_model_upload(self, model_id: str, version: str, teamspace_id: str) -> None:
261
260
  self.models_api.models_store_complete_model_upload(
262
261
  body=_DummyBody(),
263
262
  project_id=teamspace_id,
@@ -306,3 +305,29 @@ class TeamspaceApi:
306
305
  project_id=teamspace_id, id=cloud_account
307
306
  )
308
307
  return response.accelerator
308
+
309
+ def get_model(self, teamspace_id: str, model_id: Optional[str] = None, model_name: Optional[str] = None) -> V1Model:
310
+ if model_id:
311
+ return self.models_api.models_store_get_model(project_id=teamspace_id, model_id=model_id)
312
+ if not model_name:
313
+ raise ValueError("Either `model_id` or `model_name` must be provided.")
314
+ # list models with specific name
315
+ models = self.models_api.models_store_list_models(project_id=teamspace_id, name=model_name).models
316
+ if len(models) == 0:
317
+ raise ValueError(f"Model '{model_name}' does not exist.")
318
+ if len(models) > 1:
319
+ raise RuntimeError(f"Model name '{model_name}' is not a unique with this teamspace.")
320
+ # if there is only one model with the name, return it
321
+ return models[0]
322
+
323
+ def list_models(self, teamspace_id: str) -> List[V1Model]:
324
+ response = self.models_api.models_store_list_models(project_id=teamspace_id)
325
+ return response.models
326
+
327
+ def list_model_versions(
328
+ self, teamspace_id: str, model_id: Optional[str] = None, model_name: Optional[str] = None
329
+ ) -> List[V1ModelVersionArchive]:
330
+ if model_name and not model_id:
331
+ model_id = self.get_model(teamspace_id=teamspace_id, model_name=model_name).id
332
+ response = self.models_api.models_store_list_model_versions(project_id=teamspace_id, model_id=model_id)
333
+ return response.versions
@@ -6,6 +6,7 @@ import webbrowser
6
6
  from datetime import datetime
7
7
  from enum import Enum
8
8
  from pathlib import Path
9
+ from threading import Thread
9
10
  from typing import List, Optional, TypedDict, Union
10
11
  from urllib.parse import urlencode
11
12
 
@@ -17,6 +18,7 @@ from rich.prompt import Confirm
17
18
  from lightning_sdk import Machine, Teamspace
18
19
  from lightning_sdk.api import UserApi
19
20
  from lightning_sdk.api.lit_container_api import LitContainerApi
21
+ from lightning_sdk.api.utils import _get_registry_url
20
22
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
21
23
  from lightning_sdk.lightning_cloud import env
22
24
  from lightning_sdk.lightning_cloud.login import Auth, AuthServer
@@ -27,6 +29,7 @@ from lightning_sdk.utils.resolve import _get_authed_user, _resolve_teamspace
27
29
 
28
30
  _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
29
31
  _POLL_TIMEOUT = 600
32
+ LITSERVE_CODE = os.environ.get("LITSERVE_CODE", "j39bzk903h")
30
33
 
31
34
 
32
35
  class _ServeGroup(click.Group):
@@ -233,7 +236,7 @@ def api_impl(
233
236
  class _AuthServer(AuthServer):
234
237
  def get_auth_url(self, port: int) -> str:
235
238
  redirect_uri = f"http://localhost:{port}/login-complete"
236
- params = urlencode({"redirectTo": redirect_uri, "inviteCode": "litserve"})
239
+ params = urlencode({"redirectTo": redirect_uri, "okbhrt": LITSERVE_CODE})
237
240
  return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
238
241
 
239
242
 
@@ -333,12 +336,13 @@ class _Onboarding:
333
336
  return _OnboardingStatus.ONBOARDED
334
337
  return _OnboardingStatus.ONBOARDING
335
338
 
336
- def _wait(self, timeout: int = _POLL_TIMEOUT) -> None:
339
+ def _wait_user_onboarding(self, timeout: int = _POLL_TIMEOUT) -> None:
337
340
  """Wait for user onboarding if they can join the teamspace otherwise move to select a teamspace."""
338
341
  status = self.status
339
342
  if status == _OnboardingStatus.ONBOARDED:
340
343
  return
341
344
 
345
+ self.console.print("Waiting for account setup. Visit lightning.ai")
342
346
  start_time = datetime.now()
343
347
  while self.status != _OnboardingStatus.ONBOARDED:
344
348
  time.sleep(5)
@@ -351,9 +355,13 @@ class _Onboarding:
351
355
 
352
356
  def get_cloudspace_id(self, teamspace: Teamspace) -> Optional[str]:
353
357
  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
358
+ cloudspaces = sorted(cloudspaces, key=lambda cloudspace: cloudspace.created_at, reverse=True)
359
+ if len(cloudspaces) == 0:
360
+ raise RuntimeError("Error creating deployment! Finish account setup at lightning.ai first.")
361
+ # get the first cloudspace
362
+ cloudspace = cloudspaces[0]
363
+ if "scratch-studio" in cloudspace.name or "scratch-studio" in cloudspace.display_name:
364
+ return cloudspace.id
357
365
  return None
358
366
 
359
367
  def select_teamspace(self, teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
@@ -368,15 +376,8 @@ class _Onboarding:
368
376
 
369
377
  # Run only when user hasn't completed onboarding yet.
370
378
  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
-
379
+ self._wait_user_onboarding()
380
+ # Onboarding has been completed - user already selected organization if they could
380
381
  possible_teamspaces = menu._get_possible_teamspaces(self.user)
381
382
  if len(possible_teamspaces) == 1:
382
383
  # User didn't select any org
@@ -400,6 +401,38 @@ def is_connected(host: str = "8.8.8.8", port: int = 53, timeout: int = 10) -> bo
400
401
  return False
401
402
 
402
403
 
404
+ def _upload_container(
405
+ console: Console,
406
+ ls_deployer: _LitServeDeployer,
407
+ repository: str,
408
+ tag: str,
409
+ resolved_teamspace: Teamspace,
410
+ lit_cr: LitContainerApi,
411
+ cloud_account: Optional[str],
412
+ ) -> bool:
413
+ with Progress(
414
+ SpinnerColumn(),
415
+ TextColumn("[progress.description]{task.description}"),
416
+ TimeElapsedColumn(),
417
+ console=console,
418
+ transient=True,
419
+ ) as progress:
420
+ try:
421
+ push_task = progress.add_task("Uploading container to Lightning registry", total=None)
422
+ for line in ls_deployer.push_container(
423
+ repository, tag, resolved_teamspace, lit_cr, cloud_account=cloud_account
424
+ ):
425
+ progress.update(push_task, advance=1)
426
+ if not ("Pushing" in line["status"] or "Waiting" in line["status"]):
427
+ console.print(line["status"])
428
+ progress.update(push_task, description="[green]Push completed![/green]")
429
+ except Exception as e:
430
+ console.print(f"❌ Deployment failed: {e}", style="red")
431
+ return False
432
+ console.print(f"\n✅ Image pushed to {repository}:{tag}")
433
+ return True
434
+
435
+
403
436
  def _handle_cloud(
404
437
  script_path: Union[str, Path],
405
438
  console: Console,
@@ -465,13 +498,16 @@ def _handle_cloud(
465
498
  authenticate(shall_confirm=not non_interactive)
466
499
  user_status = poll_verified_status()
467
500
  cloudspace_id: Optional[str] = None
501
+ from_onboarding = False
468
502
  if not user_status["verified"]:
469
503
  console.print("❌ Verify phone number to continue. Visit lightning.ai.", style="red")
470
504
  return
471
505
  if not user_status["onboarded"]:
506
+ console.print("onboarding user")
472
507
  onboarding = _Onboarding(console)
473
508
  resolved_teamspace = onboarding.select_teamspace(teamspace, org, user)
474
509
  cloudspace_id = onboarding.get_cloudspace_id(resolved_teamspace)
510
+ from_onboarding = True
475
511
  else:
476
512
  resolved_teamspace = select_teamspace(teamspace, org, user)
477
513
 
@@ -479,29 +515,43 @@ def _handle_cloud(
479
515
  lit_cr = LitContainerApi()
480
516
  lit_cr.list_containers(resolved_teamspace.id, cloud_account=cloud_account)
481
517
 
482
- with Progress(
483
- SpinnerColumn(),
484
- TextColumn("[progress.description]{task.description}"),
485
- TimeElapsedColumn(),
486
- console=console,
487
- transient=True,
488
- ) as progress:
489
- try:
490
- push_task = progress.add_task("Pushing to registry", total=None)
491
- push_status = {}
492
- for line in ls_deployer.push_container(
493
- repository, tag, resolved_teamspace, lit_cr, cloud_account=cloud_account
494
- ):
495
- push_status = line
496
- progress.update(push_task, advance=1)
497
- if not ("Pushing" in line["status"] or "Waiting" in line["status"]):
498
- console.print(line["status"])
499
- progress.update(push_task, description="[green]Push completed![/green]")
500
- except Exception as e:
501
- console.print(f"❌ Deployment failed: {e}", style="red")
518
+ registry_url = _get_registry_url()
519
+ container_basename = repository.split("/")[-1]
520
+ image = (
521
+ f"{registry_url}/lit-container{f'-{cloud_account}' if cloud_account is not None else ''}/"
522
+ f"{resolved_teamspace.owner.name}/{resolved_teamspace.name}/{container_basename}"
523
+ )
524
+
525
+ if from_onboarding:
526
+ thread = Thread(
527
+ target=ls_deployer.run_on_cloud,
528
+ kwargs={
529
+ "deployment_name": deployment_name,
530
+ "image": image,
531
+ "teamspace": resolved_teamspace,
532
+ "metric": None,
533
+ "machine": machine,
534
+ "spot": interruptible,
535
+ "cloud_account": cloud_account,
536
+ "port": port,
537
+ "min_replica": min_replica,
538
+ "max_replica": max_replica,
539
+ "replicas": replicas,
540
+ "include_credentials": include_credentials,
541
+ "cloudspace_id": cloudspace_id,
542
+ "from_onboarding": from_onboarding,
543
+ },
544
+ )
545
+ thread.start()
546
+ console.print("🚀 Deployment started")
547
+ if not _upload_container(console, ls_deployer, repository, tag, resolved_teamspace, lit_cr, cloud_account):
548
+ thread.join()
502
549
  return
503
- console.print(f"\n✅ Image pushed to {repository}:{tag}")
504
- image = push_status.get("image")
550
+ thread.join()
551
+ return
552
+
553
+ if not _upload_container(console, ls_deployer, repository, tag, resolved_teamspace, lit_cr, cloud_account):
554
+ return
505
555
 
506
556
  deployment_status = ls_deployer.run_on_cloud(
507
557
  deployment_name=deployment_name,
@@ -517,6 +567,7 @@ def _handle_cloud(
517
567
  replicas=replicas,
518
568
  include_credentials=include_credentials,
519
569
  cloudspace_id=cloudspace_id,
570
+ from_onboarding=from_onboarding,
520
571
  )
521
572
  console.print(f"🚀 Deployment started, access at [i]{deployment_status.get('url')}[/i]")
522
573
  if user_status["onboarded"]:
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from datetime import datetime
2
3
  from typing import Any, Dict, List, Optional, Union
3
4
 
4
5
  import requests
@@ -15,6 +16,7 @@ from lightning_sdk.api.deployment_api import (
15
16
  ReleaseStrategy,
16
17
  Secret,
17
18
  TokenAuth,
19
+ compose_commands,
18
20
  restore_auth,
19
21
  restore_autoscale,
20
22
  restore_env,
@@ -30,6 +32,7 @@ from lightning_sdk.lightning_cloud.openapi import V1Deployment
30
32
  from lightning_sdk.machine import Machine
31
33
  from lightning_sdk.organization import Organization
32
34
  from lightning_sdk.services.utilities import _get_cluster
35
+ from lightning_sdk.studio import Studio
33
36
  from lightning_sdk.teamspace import Teamspace
34
37
  from lightning_sdk.user import User
35
38
  from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_org, _resolve_teamspace, _resolve_user
@@ -55,7 +58,7 @@ class Deployment:
55
58
 
56
59
  def __init__(
57
60
  self,
58
- name: str,
61
+ name: Optional[str] = None,
59
62
  teamspace: Optional[Union[str, Teamspace]] = None,
60
63
  org: Optional[Union[str, Organization]] = None,
61
64
  user: Optional[Union[str, User]] = None,
@@ -72,6 +75,9 @@ class Deployment:
72
75
  except ConnectionError as e:
73
76
  raise e
74
77
 
78
+ if name is None:
79
+ name = "dep_" + datetime.now().strftime("%m-%d_%H:%M:%S")
80
+
75
81
  self._name = name
76
82
  self._user = _resolve_user(self._user or user)
77
83
  self._org = _resolve_org(org)
@@ -102,13 +108,15 @@ class Deployment:
102
108
 
103
109
  def start(
104
110
  self,
111
+ studio: Optional[Union[str, Studio]] = None,
105
112
  machine: Optional[Machine] = None,
106
113
  image: Optional[str] = None,
107
114
  autoscale: Optional[AutoScaleConfig] = None,
108
- ports: Optional[List[float]] = None,
115
+ ports: Optional[Union[float, List[float]]] = None,
109
116
  release_strategy: Optional[ReleaseStrategy] = None,
110
117
  entrypoint: Optional[str] = None,
111
118
  command: Optional[str] = None,
119
+ commands: Optional[List[str]] = None,
112
120
  env: Union[List[Union[Secret, Env]], Dict[str, str], None] = None,
113
121
  spot: Optional[bool] = None,
114
122
  replicas: Optional[int] = None,
@@ -120,6 +128,7 @@ class Deployment:
120
128
  cloudspace_id: Optional[str] = None,
121
129
  quantity: Optional[int] = None,
122
130
  include_credentials: Optional[bool] = None,
131
+ from_onboarding: Optional[bool] = None,
123
132
  ) -> None:
124
133
  """The Lightning AI Deployment.
125
134
 
@@ -146,6 +155,7 @@ class Deployment:
146
155
  cloudspace_id: Connect deployment to a Studio.
147
156
  quantity: The number of machines per replica to deploy.
148
157
  include_credentials: Whether to include the environment variables for the SDK to authenticate
158
+ from_onboarding: Whether the deployment is from onboarding.
149
159
 
150
160
  Note:
151
161
  Since a teamspace can either be owned by an org or by a user directly,
@@ -155,12 +165,45 @@ class Deployment:
155
165
  if self._is_created:
156
166
  raise RuntimeError("This deployment has already been started.")
157
167
 
158
- cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
168
+ if isinstance(studio, Studio):
169
+ cloudspace_id = studio._studio.id
170
+ cloud_account = studio._studio.cluster_id
171
+
172
+ if isinstance(studio, str):
173
+ studio = Studio(studio)
174
+ cloudspace_id = studio._studio.id
175
+ cloud_account = studio._studio.cluster_id
176
+
177
+ if cloud_account is None:
178
+ cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
159
179
 
160
180
  if cloud_account is None and self._cloud_account is not None:
161
181
  print(f"No cloud account was provided, defaulting to {self._cloud_account.cluster_id}")
162
182
  cloud_account = os.getenv("LIGHTNING_CLUSTER_ID") or self._cloud_account.cluster_id
163
183
 
184
+ if isinstance(ports, float):
185
+ ports = [ports]
186
+
187
+ if replicas is None and autoscale is None:
188
+ replicas = 1
189
+
190
+ if machine is None:
191
+ machine = Machine.CPU
192
+
193
+ if commands is not None and command is not None:
194
+ raise ValueError("Commands and command are mutually exclusive")
195
+
196
+ if commands is not None:
197
+ command = compose_commands(commands)
198
+
199
+ if autoscale is None:
200
+ autoscale = AutoScaleConfig(
201
+ min_replicas=0,
202
+ max_replicas=1,
203
+ metric="CPU" if machine.is_cpu() else "GPU",
204
+ threshold=90,
205
+ )
206
+
164
207
  self._deployment = self._deployment_api.create_deployment(
165
208
  V1Deployment(
166
209
  autoscaling=to_autoscaling(autoscale, replicas),
@@ -180,9 +223,11 @@ class Deployment:
180
223
  health_check=health_check,
181
224
  quantity=quantity,
182
225
  include_credentials=include_credentials if include_credentials is not None else True,
226
+ cloudspace_id=cloudspace_id,
183
227
  ),
184
228
  strategy=to_strategy(release_strategy),
185
- )
229
+ ),
230
+ from_onboarding=from_onboarding,
186
231
  )
187
232
 
188
233
  # Overrides the name
@@ -196,6 +241,7 @@ class Deployment:
196
241
  image: Optional[str] = None,
197
242
  entrypoint: Optional[str] = None,
198
243
  command: Optional[str] = None,
244
+ commands: Optional[List[str]] = None,
199
245
  env: Optional[List[Union[Env, Secret]]] = None,
200
246
  spot: Optional[bool] = None,
201
247
  cloud_account: Optional[str] = None,
@@ -215,6 +261,9 @@ class Deployment:
215
261
  ) -> None:
216
262
  cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
217
263
 
264
+ if command is None and commands is not None:
265
+ command = compose_commands(commands)
266
+
218
267
  self._deployment = self._deployment_api.update_deployment(
219
268
  self._deployment,
220
269
  name=name or self._name,
@@ -225,8 +274,8 @@ class Deployment:
225
274
  cloud_account=cloud_account,
226
275
  machine=machine,
227
276
  image=image,
228
- entrypoint=entrypoint,
229
- command=command,
277
+ entrypoint=entrypoint or "",
278
+ command=command or "",
230
279
  ports=ports,
231
280
  custom_domain=custom_domain,
232
281
  auth=auth,
@@ -104,7 +104,6 @@ from lightning_sdk.lightning_cloud.openapi.models.datasets_id_body import Datase
104
104
  from lightning_sdk.lightning_cloud.openapi.models.deployments_id_body import DeploymentsIdBody
105
105
  from lightning_sdk.lightning_cloud.openapi.models.deploymenttemplates_id_body import DeploymenttemplatesIdBody
106
106
  from lightning_sdk.lightning_cloud.openapi.models.endpoints_id_body import EndpointsIdBody
107
- from lightning_sdk.lightning_cloud.openapi.models.environmenttemplates_id_body import EnvironmenttemplatesIdBody
108
107
  from lightning_sdk.lightning_cloud.openapi.models.experiment_name_variant_name_body import ExperimentNameVariantNameBody
109
108
  from lightning_sdk.lightning_cloud.openapi.models.externalv1_cloud_space_instance_status import Externalv1CloudSpaceInstanceStatus
110
109
  from lightning_sdk.lightning_cloud.openapi.models.externalv1_cluster import Externalv1Cluster
@@ -214,6 +213,7 @@ from lightning_sdk.lightning_cloud.openapi.models.stream_result_of_v1_conversati
214
213
  from lightning_sdk.lightning_cloud.openapi.models.stream_result_of_v1_get_long_running_command_in_cloud_space_response import StreamResultOfV1GetLongRunningCommandInCloudSpaceResponse
215
214
  from lightning_sdk.lightning_cloud.openapi.models.studioapp_jobs_body import StudioappJobsBody
216
215
  from lightning_sdk.lightning_cloud.openapi.models.update import Update
216
+ from lightning_sdk.lightning_cloud.openapi.models.update1 import Update1
217
217
  from lightning_sdk.lightning_cloud.openapi.models.upload_id_complete_body import UploadIdCompleteBody
218
218
  from lightning_sdk.lightning_cloud.openapi.models.upload_id_complete_body1 import UploadIdCompleteBody1
219
219
  from lightning_sdk.lightning_cloud.openapi.models.upload_id_parts_body import UploadIdPartsBody
@@ -556,8 +556,10 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_job_health_check_config imp
556
556
  from lightning_sdk.lightning_cloud.openapi.models.v1_job_log_entry import V1JobLogEntry
557
557
  from lightning_sdk.lightning_cloud.openapi.models.v1_job_logs_page import V1JobLogsPage
558
558
  from lightning_sdk.lightning_cloud.openapi.models.v1_job_logs_response import V1JobLogsResponse
559
+ from lightning_sdk.lightning_cloud.openapi.models.v1_job_resource import V1JobResource
559
560
  from lightning_sdk.lightning_cloud.openapi.models.v1_job_spec import V1JobSpec
560
561
  from lightning_sdk.lightning_cloud.openapi.models.v1_job_timing import V1JobTiming
562
+ from lightning_sdk.lightning_cloud.openapi.models.v1_job_type import V1JobType
561
563
  from lightning_sdk.lightning_cloud.openapi.models.v1_joinable_organization import V1JoinableOrganization
562
564
  from lightning_sdk.lightning_cloud.openapi.models.v1_keep_alive_cloud_space_instance_response import V1KeepAliveCloudSpaceInstanceResponse
563
565
  from lightning_sdk.lightning_cloud.openapi.models.v1_knowledge_configuration import V1KnowledgeConfiguration
@@ -624,6 +626,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_list_filesystem_snowflake_r
624
626
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_gallery_components_response import V1ListGalleryComponentsResponse
625
627
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_gallery_lightningapps_response import V1ListGalleryLightningappsResponse
626
628
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_job_files_response import V1ListJobFilesResponse
629
+ from lightning_sdk.lightning_cloud.openapi.models.v1_list_job_resources_response import V1ListJobResourcesResponse
627
630
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_jobs_response import V1ListJobsResponse
628
631
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_joinable_organizations_response import V1ListJoinableOrganizationsResponse
629
632
  from lightning_sdk.lightning_cloud.openapi.models.v1_list_lightning_run_response import V1ListLightningRunResponse
@@ -788,6 +791,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_report_restart_timings_resp
788
791
  from lightning_sdk.lightning_cloud.openapi.models.v1_request_cluster_access_request import V1RequestClusterAccessRequest
789
792
  from lightning_sdk.lightning_cloud.openapi.models.v1_request_cluster_access_response import V1RequestClusterAccessResponse
790
793
  from lightning_sdk.lightning_cloud.openapi.models.v1_request_verification_code_response import V1RequestVerificationCodeResponse
794
+ from lightning_sdk.lightning_cloud.openapi.models.v1_reservation_billing_session import V1ReservationBillingSession
791
795
  from lightning_sdk.lightning_cloud.openapi.models.v1_reservation_details import V1ReservationDetails
792
796
  from lightning_sdk.lightning_cloud.openapi.models.v1_resource_tag import V1ResourceTag
793
797
  from lightning_sdk.lightning_cloud.openapi.models.v1_resource_visibility import V1ResourceVisibility