lightning-sdk 0.2.11__py3-none-any.whl → 0.2.13__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 +1 -1
- lightning_sdk/api/deployment_api.py +35 -3
- lightning_sdk/api/lit_container_api.py +13 -7
- lightning_sdk/api/llm_api.py +46 -0
- lightning_sdk/api/studio_api.py +17 -0
- lightning_sdk/cli/entrypoint.py +1 -1
- lightning_sdk/cli/serve.py +221 -62
- lightning_sdk/deployment/deployment.py +53 -7
- lightning_sdk/lightning_cloud/openapi/__init__.py +11 -1
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_environment_template_service_api.py +4 -4
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +13 -1
- lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +4 -4
- lightning_sdk/lightning_cloud/openapi/api/git_credentials_service_api.py +497 -0
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +124 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +10 -1
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/update.py +65 -195
- lightning_sdk/lightning_cloud/openapi/models/update1.py +357 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_git_credentials_request.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_cloud_space_environment_template_response.py +1 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_git_credentials_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +2 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_git_credentials.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_job_resource.py +279 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_job_type.py +108 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_git_credentials_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_job_resources_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +55 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +29 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_reservation_billing_session.py +279 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_resources.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_usage.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +157 -1
- lightning_sdk/llm/__init__.py +3 -0
- lightning_sdk/llm/llm.py +118 -0
- lightning_sdk/plugin.py +19 -0
- lightning_sdk/serve.py +4 -6
- lightning_sdk/studio.py +33 -0
- {lightning_sdk-0.2.11.dist-info → lightning_sdk-0.2.13.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.11.dist-info → lightning_sdk-0.2.13.dist-info}/RECORD +60 -47
- lightning_sdk/lightning_cloud/openapi/models/environmenttemplates_id_body.py +0 -253
- /lightning_sdk/cli/{docker.py → docker_cli.py} +0 -0
- {lightning_sdk-0.2.11.dist-info → lightning_sdk-0.2.13.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.11.dist-info → lightning_sdk-0.2.13.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.11.dist-info → lightning_sdk-0.2.13.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.2.11.dist-info → lightning_sdk-0.2.13.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
|
@@ -460,7 +460,7 @@ def to_autoscaling(
|
|
|
460
460
|
if target_metrics is None and (threshold < 0 or threshold > 100):
|
|
461
461
|
raise ValueError("The autoscaling threshold should be defined between 0 and 100.")
|
|
462
462
|
|
|
463
|
-
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:
|
|
464
464
|
raise ValueError("The target_metrics must be provided.")
|
|
465
465
|
|
|
466
466
|
if target_metrics is not None:
|
|
@@ -524,8 +524,14 @@ def to_endpoint(
|
|
|
524
524
|
def to_health_check(
|
|
525
525
|
health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None
|
|
526
526
|
) -> Optional[V1JobHealthCheckConfig]:
|
|
527
|
+
# Use Default health check if none is provided
|
|
527
528
|
if not health_check:
|
|
528
|
-
return
|
|
529
|
+
return V1JobHealthCheckConfig(
|
|
530
|
+
failure_threshold=600,
|
|
531
|
+
initial_delay_seconds=0,
|
|
532
|
+
interval_seconds=1,
|
|
533
|
+
timeout_seconds=600,
|
|
534
|
+
)
|
|
529
535
|
|
|
530
536
|
health_check_config = V1JobHealthCheckConfig(
|
|
531
537
|
failure_threshold=health_check.failure_threshold,
|
|
@@ -555,6 +561,7 @@ def to_spec(
|
|
|
555
561
|
health_check: Optional[Union[HttpHealthCheck, ExecHealthCheck]] = None,
|
|
556
562
|
quantity: Optional[int] = None,
|
|
557
563
|
include_credentials: Optional[bool] = None,
|
|
564
|
+
cloudspace_id: Optional[None] = None,
|
|
558
565
|
) -> V1JobSpec:
|
|
559
566
|
if cloud_account is None:
|
|
560
567
|
raise ValueError("The cloud account should be defined.")
|
|
@@ -562,9 +569,15 @@ def to_spec(
|
|
|
562
569
|
if machine is None:
|
|
563
570
|
raise ValueError("The machine should be defined.")
|
|
564
571
|
|
|
565
|
-
if image is None:
|
|
572
|
+
if image is None and cloudspace_id is None:
|
|
566
573
|
raise ValueError("The image should be defined.")
|
|
567
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
|
+
|
|
568
581
|
return V1JobSpec(
|
|
569
582
|
cluster_id=cloud_account,
|
|
570
583
|
command=command,
|
|
@@ -576,6 +589,7 @@ def to_spec(
|
|
|
576
589
|
readiness_probe=to_health_check(health_check),
|
|
577
590
|
quantity=quantity,
|
|
578
591
|
include_credentials=include_credentials,
|
|
592
|
+
cloudspace_id=cloudspace_id,
|
|
579
593
|
)
|
|
580
594
|
|
|
581
595
|
|
|
@@ -600,3 +614,21 @@ def apply_change(spec: Any, key: str, value: Any) -> bool:
|
|
|
600
614
|
return True
|
|
601
615
|
|
|
602
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
|
-
|
|
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,46 @@
|
|
|
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 get_public_models(self) -> List[str]:
|
|
12
|
+
result = self._client.assistants_service_list_assistants(published=True)
|
|
13
|
+
return result.assistants
|
|
14
|
+
|
|
15
|
+
def get_org_models(self, org_id: str) -> List[str]:
|
|
16
|
+
result = self._client.assistants_service_list_assistants(org_id=org_id)
|
|
17
|
+
return result.assistants
|
|
18
|
+
|
|
19
|
+
def get_user_models(self, user_id: str) -> List[str]:
|
|
20
|
+
result = self._client.assistants_service_list_assistants(user_id=user_id)
|
|
21
|
+
return result.assistants
|
|
22
|
+
|
|
23
|
+
def start_conversation(
|
|
24
|
+
self,
|
|
25
|
+
prompt: str,
|
|
26
|
+
system_prompt: Optional[str],
|
|
27
|
+
max_completion_tokens: Optional[int],
|
|
28
|
+
assistant_id: str,
|
|
29
|
+
conversation_id: Optional[str],
|
|
30
|
+
) -> V1ConversationResponseChunk:
|
|
31
|
+
body = {
|
|
32
|
+
"message": {
|
|
33
|
+
"author": {"role": "user"},
|
|
34
|
+
"content": [
|
|
35
|
+
{
|
|
36
|
+
"contentType": "text",
|
|
37
|
+
"parts": [prompt],
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
"max_completion_tokens": max_completion_tokens,
|
|
42
|
+
}
|
|
43
|
+
if conversation_id:
|
|
44
|
+
body["conversation_id"] = conversation_id
|
|
45
|
+
result = self._client.assistants_service_start_conversation(body, assistant_id)
|
|
46
|
+
return result.result
|
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -36,13 +36,16 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
36
36
|
V1CloudSpaceInstanceConfig,
|
|
37
37
|
V1CloudSpaceSeedFile,
|
|
38
38
|
V1CloudSpaceState,
|
|
39
|
+
V1EndpointType,
|
|
39
40
|
V1GetCloudSpaceInstanceStatusResponse,
|
|
40
41
|
V1GetLongRunningCommandInCloudSpaceResponse,
|
|
41
42
|
V1LoginRequest,
|
|
42
43
|
V1Plugin,
|
|
43
44
|
V1PluginsListResponse,
|
|
45
|
+
V1UpstreamCloudSpace,
|
|
44
46
|
V1UserRequestedComputeConfig,
|
|
45
47
|
)
|
|
48
|
+
from lightning_sdk.lightning_cloud.openapi.models import ProjectIdEndpointsBody
|
|
46
49
|
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
47
50
|
from lightning_sdk.machine import Machine
|
|
48
51
|
|
|
@@ -668,6 +671,20 @@ class StudioApi:
|
|
|
668
671
|
interruptible=interruptible,
|
|
669
672
|
)
|
|
670
673
|
|
|
674
|
+
def start_new_port(self, teamspace_id: str, studio_id: str, name: str, port: int, auto_start: bool = False) -> str:
|
|
675
|
+
"""Starts a new port to the given Studio."""
|
|
676
|
+
endpoint = self._client.endpoint_service_create_endpoint(
|
|
677
|
+
project_id=teamspace_id,
|
|
678
|
+
body=ProjectIdEndpointsBody(
|
|
679
|
+
name=name,
|
|
680
|
+
ports=[str(port)],
|
|
681
|
+
cloudspace=V1UpstreamCloudSpace(
|
|
682
|
+
cloudspace_id=studio_id, port=str(port), type=V1EndpointType.PLUGIN_PORT, auto_start=auto_start
|
|
683
|
+
),
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
return endpoint.urls[0]
|
|
687
|
+
|
|
671
688
|
def _create_app(
|
|
672
689
|
self, studio_id: str, teamspace_id: str, cloud_account: str, plugin_type: str, **other_arguments: Any
|
|
673
690
|
) -> Externalv1LightningappInstance:
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -14,7 +14,7 @@ from lightning_sdk.cli.configure import configure
|
|
|
14
14
|
from lightning_sdk.cli.connect import connect
|
|
15
15
|
from lightning_sdk.cli.create import create
|
|
16
16
|
from lightning_sdk.cli.delete import delete
|
|
17
|
-
from lightning_sdk.cli.
|
|
17
|
+
from lightning_sdk.cli.docker_cli import dockerize
|
|
18
18
|
from lightning_sdk.cli.download import download
|
|
19
19
|
from lightning_sdk.cli.generate import generate
|
|
20
20
|
from lightning_sdk.cli.inspect import inspect
|
lightning_sdk/cli/serve.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import concurrent.futures
|
|
1
2
|
import os
|
|
2
3
|
import socket
|
|
3
4
|
import subprocess
|
|
@@ -6,7 +7,8 @@ import webbrowser
|
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from enum import Enum
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from
|
|
10
|
+
from threading import Thread
|
|
11
|
+
from typing import Dict, List, Optional, TypedDict, Union
|
|
10
12
|
from urllib.parse import urlencode
|
|
11
13
|
|
|
12
14
|
import click
|
|
@@ -17,16 +19,24 @@ from rich.prompt import Confirm
|
|
|
17
19
|
from lightning_sdk import Machine, Teamspace
|
|
18
20
|
from lightning_sdk.api import UserApi
|
|
19
21
|
from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
22
|
+
from lightning_sdk.api.utils import _get_registry_url
|
|
20
23
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
24
|
+
from lightning_sdk.cli.upload import (
|
|
25
|
+
_dump_current_upload_state,
|
|
26
|
+
_resolve_previous_upload_state,
|
|
27
|
+
_start_parallel_upload,
|
|
28
|
+
)
|
|
21
29
|
from lightning_sdk.lightning_cloud import env
|
|
22
30
|
from lightning_sdk.lightning_cloud.login import Auth, AuthServer
|
|
23
31
|
from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
|
|
24
32
|
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
25
33
|
from lightning_sdk.serve import _LitServeDeployer
|
|
26
|
-
from lightning_sdk.
|
|
34
|
+
from lightning_sdk.studio import Studio
|
|
35
|
+
from lightning_sdk.utils.resolve import _get_authed_user, _get_studio_url, _resolve_teamspace
|
|
27
36
|
|
|
28
37
|
_MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
|
|
29
38
|
_POLL_TIMEOUT = 600
|
|
39
|
+
LITSERVE_CODE = os.environ.get("LITSERVE_CODE", "j39bzk903h")
|
|
30
40
|
|
|
31
41
|
|
|
32
42
|
class _ServeGroup(click.Group):
|
|
@@ -83,7 +93,14 @@ def deploy() -> None:
|
|
|
83
93
|
default="CPU",
|
|
84
94
|
show_default=True,
|
|
85
95
|
type=click.Choice(_MACHINE_VALUES),
|
|
86
|
-
help="
|
|
96
|
+
help="Machine type to deploy the API on. Defaults to CPU.",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--devbox",
|
|
100
|
+
default=None,
|
|
101
|
+
show_default=True,
|
|
102
|
+
type=click.Choice(_MACHINE_VALUES),
|
|
103
|
+
help="Machine type to build the API on. Setting this argument will open the server in a Studio.",
|
|
87
104
|
)
|
|
88
105
|
@click.option(
|
|
89
106
|
"--interruptible",
|
|
@@ -131,7 +148,8 @@ def api(
|
|
|
131
148
|
local: bool,
|
|
132
149
|
name: Optional[str],
|
|
133
150
|
non_interactive: bool,
|
|
134
|
-
machine: str,
|
|
151
|
+
machine: Optional[str],
|
|
152
|
+
devbox: Optional[str],
|
|
135
153
|
interruptible: bool,
|
|
136
154
|
teamspace: Optional[str],
|
|
137
155
|
org: Optional[str],
|
|
@@ -148,9 +166,10 @@ def api(
|
|
|
148
166
|
script_path=script_path,
|
|
149
167
|
easy=easy,
|
|
150
168
|
local=local,
|
|
151
|
-
|
|
169
|
+
name=name,
|
|
152
170
|
non_interactive=non_interactive,
|
|
153
171
|
machine=machine,
|
|
172
|
+
devbox=devbox,
|
|
154
173
|
interruptible=interruptible,
|
|
155
174
|
teamspace=teamspace,
|
|
156
175
|
org=org,
|
|
@@ -168,10 +187,11 @@ def api_impl(
|
|
|
168
187
|
script_path: Union[str, Path],
|
|
169
188
|
easy: bool = False,
|
|
170
189
|
local: bool = False,
|
|
171
|
-
|
|
190
|
+
name: Optional[str] = None,
|
|
172
191
|
tag: Optional[str] = None,
|
|
173
192
|
non_interactive: bool = False,
|
|
174
193
|
machine: str = "CPU",
|
|
194
|
+
devbox: Optional[str] = None,
|
|
175
195
|
interruptible: bool = False,
|
|
176
196
|
teamspace: Optional[str] = None,
|
|
177
197
|
org: Optional[str] = None,
|
|
@@ -193,47 +213,50 @@ def api_impl(
|
|
|
193
213
|
|
|
194
214
|
_LitServeDeployer.generate_client() if easy else None
|
|
195
215
|
|
|
196
|
-
if not
|
|
216
|
+
if not name:
|
|
197
217
|
timestr = datetime.now().strftime("%b-%d-%H_%M")
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if not local:
|
|
201
|
-
repository = repository or "litserve-model"
|
|
202
|
-
machine = Machine.from_str(machine)
|
|
203
|
-
return _handle_cloud(
|
|
204
|
-
script_path,
|
|
205
|
-
console,
|
|
206
|
-
repository=repository,
|
|
207
|
-
tag=tag,
|
|
208
|
-
non_interactive=non_interactive,
|
|
209
|
-
machine=machine,
|
|
210
|
-
interruptible=interruptible,
|
|
211
|
-
teamspace=teamspace,
|
|
212
|
-
org=org,
|
|
213
|
-
user=user,
|
|
214
|
-
cloud_account=cloud_account,
|
|
215
|
-
port=port,
|
|
216
|
-
min_replica=min_replica,
|
|
217
|
-
max_replica=max_replica,
|
|
218
|
-
replicas=replicas,
|
|
219
|
-
include_credentials=include_credentials,
|
|
220
|
-
)
|
|
218
|
+
name = f"litserve-{timestr}".lower()
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
if local:
|
|
221
|
+
try:
|
|
222
|
+
subprocess.run(
|
|
223
|
+
["python", str(script_path)],
|
|
224
|
+
check=True,
|
|
225
|
+
text=True,
|
|
226
|
+
)
|
|
227
|
+
return None
|
|
228
|
+
except subprocess.CalledProcessError as e:
|
|
229
|
+
error_msg = f"Script execution failed with exit code {e.returncode}\nstdout: {e.stdout}\nstderr: {e.stderr}"
|
|
230
|
+
raise RuntimeError(error_msg) from None
|
|
231
|
+
|
|
232
|
+
if devbox:
|
|
233
|
+
return _handle_devbox(name, script_path, console, non_interactive, devbox, interruptible, teamspace, org, user)
|
|
234
|
+
|
|
235
|
+
machine = Machine.from_str(machine)
|
|
236
|
+
return _handle_cloud(
|
|
237
|
+
script_path,
|
|
238
|
+
console,
|
|
239
|
+
repository=name,
|
|
240
|
+
tag=tag,
|
|
241
|
+
non_interactive=non_interactive,
|
|
242
|
+
machine=machine,
|
|
243
|
+
interruptible=interruptible,
|
|
244
|
+
teamspace=teamspace,
|
|
245
|
+
org=org,
|
|
246
|
+
user=user,
|
|
247
|
+
cloud_account=cloud_account,
|
|
248
|
+
port=port,
|
|
249
|
+
min_replica=min_replica,
|
|
250
|
+
max_replica=max_replica,
|
|
251
|
+
replicas=replicas,
|
|
252
|
+
include_credentials=include_credentials,
|
|
253
|
+
)
|
|
231
254
|
|
|
232
255
|
|
|
233
256
|
class _AuthServer(AuthServer):
|
|
234
257
|
def get_auth_url(self, port: int) -> str:
|
|
235
258
|
redirect_uri = f"http://localhost:{port}/login-complete"
|
|
236
|
-
params = urlencode({"redirectTo": redirect_uri, "
|
|
259
|
+
params = urlencode({"redirectTo": redirect_uri, "okbhrt": LITSERVE_CODE})
|
|
237
260
|
return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
|
|
238
261
|
|
|
239
262
|
|
|
@@ -398,6 +421,128 @@ def is_connected(host: str = "8.8.8.8", port: int = 53, timeout: int = 10) -> bo
|
|
|
398
421
|
return False
|
|
399
422
|
|
|
400
423
|
|
|
424
|
+
def _upload_container(
|
|
425
|
+
console: Console,
|
|
426
|
+
ls_deployer: _LitServeDeployer,
|
|
427
|
+
repository: str,
|
|
428
|
+
tag: str,
|
|
429
|
+
resolved_teamspace: Teamspace,
|
|
430
|
+
lit_cr: LitContainerApi,
|
|
431
|
+
cloud_account: Optional[str],
|
|
432
|
+
) -> bool:
|
|
433
|
+
with Progress(
|
|
434
|
+
SpinnerColumn(),
|
|
435
|
+
TextColumn("[progress.description]{task.description}"),
|
|
436
|
+
TimeElapsedColumn(),
|
|
437
|
+
console=console,
|
|
438
|
+
transient=True,
|
|
439
|
+
) as progress:
|
|
440
|
+
try:
|
|
441
|
+
push_task = progress.add_task("Uploading container to Lightning registry", total=None)
|
|
442
|
+
for line in ls_deployer.push_container(
|
|
443
|
+
repository, tag, resolved_teamspace, lit_cr, cloud_account=cloud_account
|
|
444
|
+
):
|
|
445
|
+
progress.update(push_task, advance=1)
|
|
446
|
+
if not ("Pushing" in line["status"] or "Waiting" in line["status"]):
|
|
447
|
+
console.print(line["status"])
|
|
448
|
+
progress.update(push_task, description="[green]Push completed![/green]")
|
|
449
|
+
except Exception as e:
|
|
450
|
+
console.print(f"❌ Deployment failed: {e}", style="red")
|
|
451
|
+
return False
|
|
452
|
+
console.print(f"\n✅ Image pushed to {repository}:{tag}")
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# TODO: Move the rest of the devbox logic here
|
|
457
|
+
class _LitServeDevbox:
|
|
458
|
+
"""Build LitServe API in a Studio."""
|
|
459
|
+
|
|
460
|
+
def resolve_previous_upload(self, studio: Studio, folder: str) -> Dict[str, str]:
|
|
461
|
+
remote_path = "."
|
|
462
|
+
pairs = {}
|
|
463
|
+
for root, _, files in os.walk(folder):
|
|
464
|
+
rel_root = os.path.relpath(root, folder)
|
|
465
|
+
for f in files:
|
|
466
|
+
pairs[os.path.join(root, f)] = os.path.join(remote_path, rel_root, f)
|
|
467
|
+
return _resolve_previous_upload_state(studio, remote_path, pairs)
|
|
468
|
+
|
|
469
|
+
def upload_folder(self, studio: Studio, folder: str, upload_state: Dict[str, str]) -> None:
|
|
470
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
471
|
+
futures = _start_parallel_upload(executor, studio, upload_state)
|
|
472
|
+
total_files = len(upload_state)
|
|
473
|
+
|
|
474
|
+
with Progress(
|
|
475
|
+
SpinnerColumn(),
|
|
476
|
+
TextColumn("[progress.description]{task.description}"),
|
|
477
|
+
TimeElapsedColumn(),
|
|
478
|
+
console=Console(),
|
|
479
|
+
transient=True,
|
|
480
|
+
) as progress:
|
|
481
|
+
upload_task = progress.add_task(f"[cyan]Uploading {total_files} files to Studio...", total=total_files)
|
|
482
|
+
for f in concurrent.futures.as_completed(futures):
|
|
483
|
+
upload_state.pop(f.result())
|
|
484
|
+
_dump_current_upload_state(studio, ".", upload_state)
|
|
485
|
+
progress.update(upload_task, advance=1)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _handle_devbox(
|
|
489
|
+
name: str,
|
|
490
|
+
script_path: Path,
|
|
491
|
+
console: Console,
|
|
492
|
+
non_interactive: bool = False,
|
|
493
|
+
devbox: Machine = "CPU",
|
|
494
|
+
interruptible: bool = False,
|
|
495
|
+
teamspace: Optional[str] = None,
|
|
496
|
+
org: Optional[str] = None,
|
|
497
|
+
user: Optional[str] = None,
|
|
498
|
+
) -> None:
|
|
499
|
+
if script_path.suffix != ".py":
|
|
500
|
+
console.print("❌ Error: Only Python files (.py) are supported for development servers", style="red")
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
resolved_teamspace = select_teamspace(teamspace, org, user)
|
|
504
|
+
studio = Studio(name=name, teamspace=resolved_teamspace)
|
|
505
|
+
lit_devbox = _LitServeDevbox()
|
|
506
|
+
|
|
507
|
+
studio_url = _get_studio_url(studio, turn_on=True)
|
|
508
|
+
pathlib_path = Path(script_path).resolve()
|
|
509
|
+
ok = False
|
|
510
|
+
studio_path = f"{studio.owner.name}/{studio.teamspace.name}/{studio.name}"
|
|
511
|
+
|
|
512
|
+
console.print("\n=== Lightning Studio Setup ===")
|
|
513
|
+
console.print(f"🔧 [bold]Setting up Studio:[/bold] {studio_path}")
|
|
514
|
+
console.print(f"📁 [bold]Local project:[/bold] {pathlib_path.parent}")
|
|
515
|
+
|
|
516
|
+
upload_state = lit_devbox.resolve_previous_upload(studio, pathlib_path.parent)
|
|
517
|
+
if non_interactive:
|
|
518
|
+
console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
519
|
+
ok = webbrowser.open(studio_url)
|
|
520
|
+
else:
|
|
521
|
+
if Confirm.ask("Would you like to open your Studio in the browser?", default=True):
|
|
522
|
+
console.print(f"🌐 [bold]Opening Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
523
|
+
ok = webbrowser.open(studio_url)
|
|
524
|
+
|
|
525
|
+
if not ok:
|
|
526
|
+
console.print(f"🔗 [bold]Access Studio:[/bold] [link={studio_url}]{studio_url}[/link]")
|
|
527
|
+
|
|
528
|
+
console.print("\n⚡ Initializing Studio (this typically takes 1-2 minutes)...")
|
|
529
|
+
studio.start(machine=devbox, interruptible=interruptible)
|
|
530
|
+
studio.install_plugin("custom-port")
|
|
531
|
+
console.print("🔌 Configuring server port...")
|
|
532
|
+
studio.run_plugin("custom-port", port=8000) # TODO: Remove hardcoded port and fetch from LitServe
|
|
533
|
+
|
|
534
|
+
console.print("📤 Syncing project files to Studio...")
|
|
535
|
+
lit_devbox.upload_folder(studio, pathlib_path.parent, upload_state)
|
|
536
|
+
|
|
537
|
+
# Add completion message with next steps
|
|
538
|
+
console.print("\n✅ Studio ready!")
|
|
539
|
+
console.print("\n📋 [bold]Next steps:[/bold]")
|
|
540
|
+
console.print(" [bold]1.[/bold] Server code will be available in the Studio")
|
|
541
|
+
console.print(" [bold]2.[/bold] The Studio is now running with the specified configuration")
|
|
542
|
+
console.print(" [bold]3.[/bold] Modify and run your server directly in the Studio")
|
|
543
|
+
# TODO: Once server running is implemented
|
|
544
|
+
|
|
545
|
+
|
|
401
546
|
def _handle_cloud(
|
|
402
547
|
script_path: Union[str, Path],
|
|
403
548
|
console: Console,
|
|
@@ -480,29 +625,43 @@ def _handle_cloud(
|
|
|
480
625
|
lit_cr = LitContainerApi()
|
|
481
626
|
lit_cr.list_containers(resolved_teamspace.id, cloud_account=cloud_account)
|
|
482
627
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
628
|
+
registry_url = _get_registry_url()
|
|
629
|
+
container_basename = repository.split("/")[-1]
|
|
630
|
+
image = (
|
|
631
|
+
f"{registry_url}/lit-container{f'-{cloud_account}' if cloud_account is not None else ''}/"
|
|
632
|
+
f"{resolved_teamspace.owner.name}/{resolved_teamspace.name}/{container_basename}"
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
if from_onboarding:
|
|
636
|
+
thread = Thread(
|
|
637
|
+
target=ls_deployer.run_on_cloud,
|
|
638
|
+
kwargs={
|
|
639
|
+
"deployment_name": deployment_name,
|
|
640
|
+
"image": image,
|
|
641
|
+
"teamspace": resolved_teamspace,
|
|
642
|
+
"metric": None,
|
|
643
|
+
"machine": machine,
|
|
644
|
+
"spot": interruptible,
|
|
645
|
+
"cloud_account": cloud_account,
|
|
646
|
+
"port": port,
|
|
647
|
+
"min_replica": min_replica,
|
|
648
|
+
"max_replica": max_replica,
|
|
649
|
+
"replicas": replicas,
|
|
650
|
+
"include_credentials": include_credentials,
|
|
651
|
+
"cloudspace_id": cloudspace_id,
|
|
652
|
+
"from_onboarding": from_onboarding,
|
|
653
|
+
},
|
|
654
|
+
)
|
|
655
|
+
thread.start()
|
|
656
|
+
console.print("🚀 Deployment started")
|
|
657
|
+
if not _upload_container(console, ls_deployer, repository, tag, resolved_teamspace, lit_cr, cloud_account):
|
|
658
|
+
thread.join()
|
|
503
659
|
return
|
|
504
|
-
|
|
505
|
-
|
|
660
|
+
thread.join()
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
if not _upload_container(console, ls_deployer, repository, tag, resolved_teamspace, lit_cr, cloud_account):
|
|
664
|
+
return
|
|
506
665
|
|
|
507
666
|
deployment_status = ls_deployer.run_on_cloud(
|
|
508
667
|
deployment_name=deployment_name,
|