lightning-sdk 0.2.14__py3-none-any.whl → 0.2.16__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/base_studio_api.py +79 -0
- lightning_sdk/api/cluster_api.py +83 -1
- lightning_sdk/api/license_api.py +48 -0
- lightning_sdk/api/llm_api.py +73 -12
- lightning_sdk/api/studio_api.py +50 -1
- lightning_sdk/api/teamspace_api.py +127 -1
- lightning_sdk/api/utils.py +4 -0
- lightning_sdk/base_studio.py +83 -0
- lightning_sdk/cli/create.py +21 -1
- lightning_sdk/cli/delete.py +6 -8
- lightning_sdk/cli/deploy/__init__.py +0 -0
- lightning_sdk/cli/deploy/_auth.py +189 -0
- lightning_sdk/cli/deploy/devbox.py +157 -0
- lightning_sdk/cli/{serve.py → deploy/serve.py} +22 -281
- lightning_sdk/cli/download.py +69 -16
- lightning_sdk/cli/entrypoint.py +1 -1
- lightning_sdk/cli/open.py +21 -2
- lightning_sdk/cli/start.py +12 -3
- lightning_sdk/cli/teamspace_menu.py +9 -1
- lightning_sdk/cli/upload.py +2 -5
- lightning_sdk/lightning_cloud/openapi/__init__.py +29 -0
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +121 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +9 -1
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +226 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +178 -0
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +984 -101
- lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +525 -0
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +93 -0
- lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +28 -0
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/cloudspaces_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body.py +331 -0
- lightning_sdk/lightning_cloud/openapi/models/deployment_id_alertingpolicies_body1.py +305 -0
- lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/endpoints_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/model_id_versions_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/models_id_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +183 -1
- lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/project_id_cloudspaces_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +107 -3
- lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/update.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_aws_direct_v1.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +3 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_config.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_source_type.py +103 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloudflare_v1.py +66 -66
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_tagging_options.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_upload.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_deployment_alerting_policy_response.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_event.py +487 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy.py +383 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_frequency.py +105 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_operation.py +105 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_severity.py +106 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_alerting_policy_type.py +111 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_api.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +4 -4
- lightning_sdk/lightning_cloud/openapi/models/v1_endpoint.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_ge_list_deployment_routing_telemetry_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_instance_open_ports_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_deployment_routing_telemetry_content_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_job_stats_response.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_get_organization_storage_metadata_response.py +331 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_project_balance_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_events_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_deployment_alerting_policies_response.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_product_licenses_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_membership.py +43 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_modify_filesystem_volume_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +183 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_state.py +111 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_presigned_url.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +409 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_product_license_check_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_project.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_project_membership.py +43 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +107 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_project_storage.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_r2_data_connection.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_routing_telemetry.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_secret_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_type.py +2 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_sleep_server_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_trigger_filesystem_upgrade_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_upload_project_artifact_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_usage_report.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +347 -113
- lightning_sdk/lightning_cloud/openapi/models/v1_user_requested_compute_config.py +27 -1
- lightning_sdk/lightning_cloud/rest_client.py +4 -0
- lightning_sdk/llm/llm.py +132 -40
- lightning_sdk/services/__init__.py +1 -1
- lightning_sdk/services/license.py +236 -0
- lightning_sdk/studio.py +62 -1
- lightning_sdk/teamspace.py +68 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/RECORD +122 -86
- /lightning_sdk/services/{finetune/__init__.py → finetune_llm.py} +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.16.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import concurrent.futures
|
|
2
1
|
import os
|
|
3
2
|
import socket
|
|
4
3
|
import subprocess
|
|
5
|
-
import time
|
|
6
4
|
import webbrowser
|
|
7
5
|
from datetime import datetime
|
|
8
|
-
from enum import Enum
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
from threading import Thread
|
|
11
|
-
from typing import
|
|
12
|
-
from urllib.parse import urlencode
|
|
8
|
+
from typing import Optional, Union
|
|
13
9
|
|
|
14
10
|
import click
|
|
15
11
|
from rich.console import Console
|
|
@@ -17,26 +13,19 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
|
17
13
|
from rich.prompt import Confirm
|
|
18
14
|
|
|
19
15
|
from lightning_sdk import Machine, Teamspace
|
|
20
|
-
from lightning_sdk.api import UserApi
|
|
21
16
|
from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
22
17
|
from lightning_sdk.api.utils import _get_registry_url
|
|
23
|
-
from lightning_sdk.cli.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
from lightning_sdk.cli.deploy._auth import (
|
|
19
|
+
_AuthMode,
|
|
20
|
+
_Onboarding,
|
|
21
|
+
authenticate,
|
|
22
|
+
poll_verified_status,
|
|
23
|
+
select_teamspace,
|
|
28
24
|
)
|
|
29
|
-
from lightning_sdk.
|
|
30
|
-
from lightning_sdk.lightning_cloud.login import Auth, AuthServer
|
|
31
|
-
from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
|
|
32
|
-
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
25
|
+
from lightning_sdk.cli.deploy.devbox import _handle_devbox
|
|
33
26
|
from lightning_sdk.serve import _LitServeDeployer
|
|
34
|
-
from lightning_sdk.studio import Studio
|
|
35
|
-
from lightning_sdk.utils.resolve import _get_authed_user, _get_studio_url, _resolve_teamspace
|
|
36
27
|
|
|
37
28
|
_MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
|
|
38
|
-
_POLL_TIMEOUT = 600
|
|
39
|
-
LITSERVE_CODE = os.environ.get("LITSERVE_CODE", "j39bzk903h")
|
|
40
29
|
|
|
41
30
|
|
|
42
31
|
class _ServeGroup(click.Group):
|
|
@@ -53,12 +42,12 @@ def deploy() -> None:
|
|
|
53
42
|
"""Deploy a LitServe model.
|
|
54
43
|
|
|
55
44
|
Example:
|
|
56
|
-
lightning deploy server.py
|
|
45
|
+
lightning deploy server.py --cloud # deploy to the cloud
|
|
57
46
|
|
|
58
47
|
Example:
|
|
59
|
-
lightning deploy server.py
|
|
48
|
+
lightning deploy server.py # run locally
|
|
60
49
|
|
|
61
|
-
You can deploy the API to the cloud by running `lightning deploy server.py`.
|
|
50
|
+
You can deploy the API to the cloud by running `lightning deploy server.py --cloud`.
|
|
62
51
|
This will build a docker container for the server.py script and deploy it to the Lightning AI platform.
|
|
63
52
|
"""
|
|
64
53
|
|
|
@@ -73,11 +62,11 @@ def deploy() -> None:
|
|
|
73
62
|
help="Generate a client for the model",
|
|
74
63
|
)
|
|
75
64
|
@click.option(
|
|
76
|
-
"--
|
|
65
|
+
"--cloud",
|
|
77
66
|
is_flag=True,
|
|
78
67
|
default=False,
|
|
79
68
|
flag_value=True,
|
|
80
|
-
help="Run the model
|
|
69
|
+
help="Run the model on cloud",
|
|
81
70
|
)
|
|
82
71
|
@click.option("--name", default=None, help="Name of the deployed API (e.g., 'classification-api', 'Llama-api')")
|
|
83
72
|
@click.option(
|
|
@@ -133,7 +122,7 @@ def deploy() -> None:
|
|
|
133
122
|
@click.option("--port", default=8000, help="The port to expose the API on.")
|
|
134
123
|
@click.option("--min_replica", "--min-replica", default=0, help="Number of replicas to start with.")
|
|
135
124
|
@click.option("--max_replica", "--max-replica", default=1, help="Number of replicas to scale up to.")
|
|
136
|
-
@click.option("--replicas",
|
|
125
|
+
@click.option("--replicas", default=1, help="Deployment will start with this many replicas.")
|
|
137
126
|
@click.option(
|
|
138
127
|
"--no_credentials",
|
|
139
128
|
"--no-credentials",
|
|
@@ -145,7 +134,7 @@ def deploy() -> None:
|
|
|
145
134
|
def api(
|
|
146
135
|
script_path: str,
|
|
147
136
|
easy: bool,
|
|
148
|
-
|
|
137
|
+
cloud: bool,
|
|
149
138
|
name: Optional[str],
|
|
150
139
|
non_interactive: bool,
|
|
151
140
|
machine: Optional[str],
|
|
@@ -165,7 +154,7 @@ def api(
|
|
|
165
154
|
return api_impl(
|
|
166
155
|
script_path=script_path,
|
|
167
156
|
easy=easy,
|
|
168
|
-
|
|
157
|
+
cloud=cloud,
|
|
169
158
|
name=name,
|
|
170
159
|
non_interactive=non_interactive,
|
|
171
160
|
machine=machine,
|
|
@@ -186,7 +175,7 @@ def api(
|
|
|
186
175
|
def api_impl(
|
|
187
176
|
script_path: Union[str, Path],
|
|
188
177
|
easy: bool = False,
|
|
189
|
-
|
|
178
|
+
cloud: bool = False,
|
|
190
179
|
name: Optional[str] = None,
|
|
191
180
|
tag: Optional[str] = None,
|
|
192
181
|
non_interactive: bool = False,
|
|
@@ -217,7 +206,7 @@ def api_impl(
|
|
|
217
206
|
timestr = datetime.now().strftime("%b-%d-%H_%M")
|
|
218
207
|
name = f"litserve-{timestr}".lower()
|
|
219
208
|
|
|
220
|
-
if
|
|
209
|
+
if not cloud and not devbox:
|
|
221
210
|
try:
|
|
222
211
|
subprocess.run(
|
|
223
212
|
["python", str(script_path)],
|
|
@@ -230,7 +219,8 @@ def api_impl(
|
|
|
230
219
|
raise RuntimeError(error_msg) from None
|
|
231
220
|
|
|
232
221
|
if devbox:
|
|
233
|
-
|
|
222
|
+
machine = Machine.from_str(devbox)
|
|
223
|
+
return _handle_devbox(name, script_path, console, non_interactive, machine, interruptible, teamspace, org, user)
|
|
234
224
|
|
|
235
225
|
machine = Machine.from_str(machine)
|
|
236
226
|
return _handle_cloud(
|
|
@@ -253,165 +243,6 @@ def api_impl(
|
|
|
253
243
|
)
|
|
254
244
|
|
|
255
245
|
|
|
256
|
-
class _AuthServer(AuthServer):
|
|
257
|
-
def get_auth_url(self, port: int) -> str:
|
|
258
|
-
redirect_uri = f"http://localhost:{port}/login-complete"
|
|
259
|
-
params = urlencode({"redirectTo": redirect_uri, "okbhrt": LITSERVE_CODE})
|
|
260
|
-
return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
class _Auth(Auth):
|
|
264
|
-
def __init__(self, shall_confirm: bool = False) -> None:
|
|
265
|
-
super().__init__()
|
|
266
|
-
self._shall_confirm = shall_confirm
|
|
267
|
-
|
|
268
|
-
def _run_server(self) -> None:
|
|
269
|
-
if self._shall_confirm:
|
|
270
|
-
proceed = Confirm.ask(
|
|
271
|
-
"Authenticating with Lightning AI. This will open a browser window. Continue?", default=True
|
|
272
|
-
)
|
|
273
|
-
if not proceed:
|
|
274
|
-
raise RuntimeError(
|
|
275
|
-
"Login cancelled. Please login to Lightning AI to deploy the API."
|
|
276
|
-
" Run `lightning login` to login."
|
|
277
|
-
) from None
|
|
278
|
-
print("Opening browser for authentication...")
|
|
279
|
-
print("Please come back to the terminal after logging in.")
|
|
280
|
-
time.sleep(3)
|
|
281
|
-
_AuthServer().login_with_browser(self)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def authenticate(shall_confirm: bool = True) -> None:
|
|
285
|
-
auth = _Auth(shall_confirm)
|
|
286
|
-
auth.authenticate()
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
|
|
290
|
-
if teamspace is None:
|
|
291
|
-
user = _get_authed_user()
|
|
292
|
-
menu = _TeamspacesMenu()
|
|
293
|
-
possible_teamspaces = menu._get_possible_teamspaces(user)
|
|
294
|
-
if len(possible_teamspaces) == 1:
|
|
295
|
-
name = next(iter(possible_teamspaces.values()))["name"]
|
|
296
|
-
return Teamspace(name=name, org=org, user=user)
|
|
297
|
-
|
|
298
|
-
return menu._resolve_teamspace(teamspace)
|
|
299
|
-
|
|
300
|
-
return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
class _UserStatus(TypedDict):
|
|
304
|
-
verified: bool
|
|
305
|
-
onboarded: bool
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def poll_verified_status(timeout: int = _POLL_TIMEOUT) -> _UserStatus:
|
|
309
|
-
"""Polls the verified status of the user until it is True or a timeout occurs."""
|
|
310
|
-
user_api = UserApi()
|
|
311
|
-
user = _get_authed_user()
|
|
312
|
-
start_time = datetime.now()
|
|
313
|
-
result = {"onboarded": False, "verified": False}
|
|
314
|
-
while True:
|
|
315
|
-
user_resp = user_api.get_user(name=user.name)
|
|
316
|
-
result["onboarded"] = user_resp.status.completed_project_onboarding
|
|
317
|
-
result["verified"] = user_resp.status.verified
|
|
318
|
-
if user_resp.status.verified:
|
|
319
|
-
return result
|
|
320
|
-
if (datetime.now() - start_time).total_seconds() > timeout:
|
|
321
|
-
break
|
|
322
|
-
time.sleep(5)
|
|
323
|
-
return result
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
class _OnboardingStatus(Enum):
|
|
327
|
-
NOT_VERIFIED = "not_verified"
|
|
328
|
-
ONBOARDING = "onboarding"
|
|
329
|
-
ONBOARDED = "onboarded"
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
class _Onboarding:
|
|
333
|
-
def __init__(self, console: Console) -> None:
|
|
334
|
-
self.console = console
|
|
335
|
-
self.user = _get_authed_user()
|
|
336
|
-
self.user_api = UserApi()
|
|
337
|
-
self.client = LightningClient(max_tries=7)
|
|
338
|
-
|
|
339
|
-
@property
|
|
340
|
-
def verified(self) -> bool:
|
|
341
|
-
return self.user_api.get_user(name=self.user.name).status.verified
|
|
342
|
-
|
|
343
|
-
@property
|
|
344
|
-
def is_onboarded(self) -> bool:
|
|
345
|
-
return self.user_api.get_user(name=self.user.name).status.completed_project_onboarding
|
|
346
|
-
|
|
347
|
-
@property
|
|
348
|
-
def can_join_org(self) -> bool:
|
|
349
|
-
return len(self.client.organizations_service_list_joinable_organizations().joinable_organizations) > 0
|
|
350
|
-
|
|
351
|
-
@property
|
|
352
|
-
def status(self) -> _OnboardingStatus:
|
|
353
|
-
if not self.verified:
|
|
354
|
-
return _OnboardingStatus.NOT_VERIFIED
|
|
355
|
-
if self.is_onboarded:
|
|
356
|
-
return _OnboardingStatus.ONBOARDED
|
|
357
|
-
return _OnboardingStatus.ONBOARDING
|
|
358
|
-
|
|
359
|
-
def _wait_user_onboarding(self, timeout: int = _POLL_TIMEOUT) -> None:
|
|
360
|
-
"""Wait for user onboarding if they can join the teamspace otherwise move to select a teamspace."""
|
|
361
|
-
status = self.status
|
|
362
|
-
if status == _OnboardingStatus.ONBOARDED:
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
self.console.print("Waiting for account setup. Visit lightning.ai")
|
|
366
|
-
start_time = datetime.now()
|
|
367
|
-
while self.status != _OnboardingStatus.ONBOARDED:
|
|
368
|
-
time.sleep(5)
|
|
369
|
-
if self.is_onboarded:
|
|
370
|
-
return
|
|
371
|
-
if (datetime.now() - start_time).total_seconds() > timeout:
|
|
372
|
-
break
|
|
373
|
-
|
|
374
|
-
raise RuntimeError("Timed out waiting for onboarding status")
|
|
375
|
-
|
|
376
|
-
def get_cloudspace_id(self, teamspace: Teamspace) -> Optional[str]:
|
|
377
|
-
cloudspaces: List[V1CloudSpace] = self.client.cloud_space_service_list_cloud_spaces(teamspace.id).cloudspaces
|
|
378
|
-
cloudspaces = sorted(cloudspaces, key=lambda cloudspace: cloudspace.created_at, reverse=True)
|
|
379
|
-
if len(cloudspaces) == 0:
|
|
380
|
-
raise RuntimeError("Error creating deployment! Finish account setup at lightning.ai first.")
|
|
381
|
-
# get the first cloudspace
|
|
382
|
-
cloudspace = cloudspaces[0]
|
|
383
|
-
if "scratch-studio" in cloudspace.name or "scratch-studio" in cloudspace.display_name:
|
|
384
|
-
return cloudspace.id
|
|
385
|
-
return None
|
|
386
|
-
|
|
387
|
-
def select_teamspace(self, teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
|
|
388
|
-
"""Select a teamspace while onboarding.
|
|
389
|
-
|
|
390
|
-
If user is being onboarded and can't join any org, the teamspace it will be resolved to the default
|
|
391
|
-
personal teamspace.
|
|
392
|
-
If user is being onboarded and can join an org then it will select default teamspace from the org.
|
|
393
|
-
"""
|
|
394
|
-
if self.is_onboarded:
|
|
395
|
-
return select_teamspace(teamspace, org, user)
|
|
396
|
-
|
|
397
|
-
# Run only when user hasn't completed onboarding yet.
|
|
398
|
-
menu = _TeamspacesMenu()
|
|
399
|
-
self._wait_user_onboarding()
|
|
400
|
-
# Onboarding has been completed - user already selected organization if they could
|
|
401
|
-
possible_teamspaces = menu._get_possible_teamspaces(self.user)
|
|
402
|
-
if len(possible_teamspaces) == 1:
|
|
403
|
-
# User didn't select any org
|
|
404
|
-
value = next(iter(possible_teamspaces.values()))
|
|
405
|
-
return Teamspace(name=value["name"], org=value["org"], user=value["user"])
|
|
406
|
-
|
|
407
|
-
for _, value in possible_teamspaces.items():
|
|
408
|
-
# User select an org
|
|
409
|
-
# Onboarding teamspace will be the default teamspace in the selected org
|
|
410
|
-
if value["org"]:
|
|
411
|
-
return Teamspace(name=value["name"], org=value["org"], user=value["user"])
|
|
412
|
-
raise RuntimeError("Unable to select teamspace. Visit lightning.ai")
|
|
413
|
-
|
|
414
|
-
|
|
415
246
|
def is_connected(host: str = "8.8.8.8", port: int = 53, timeout: int = 10) -> bool:
|
|
416
247
|
try:
|
|
417
248
|
socket.setdefaulttimeout(timeout)
|
|
@@ -453,96 +284,6 @@ def _upload_container(
|
|
|
453
284
|
return True
|
|
454
285
|
|
|
455
286
|
|
|
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
|
-
|
|
546
287
|
def _handle_cloud(
|
|
547
288
|
script_path: Union[str, Path],
|
|
548
289
|
console: Console,
|
|
@@ -563,7 +304,7 @@ def _handle_cloud(
|
|
|
563
304
|
) -> None:
|
|
564
305
|
if not is_connected():
|
|
565
306
|
console.print("❌ Internet connection required to deploy to the cloud.", style="red")
|
|
566
|
-
console.print("To run locally instead, use: `lightning serve [SCRIPT | server.py]
|
|
307
|
+
console.print("To run locally instead, use: `lightning serve [SCRIPT | server.py]`")
|
|
567
308
|
return
|
|
568
309
|
|
|
569
310
|
deployment_name = os.path.basename(repository)
|
|
@@ -605,7 +346,7 @@ def _handle_cloud(
|
|
|
605
346
|
# Push the container to the registry
|
|
606
347
|
console.print("\nPushing container to registry. It may take a while...", style="bold")
|
|
607
348
|
# Authenticate with LitServe affiliate
|
|
608
|
-
authenticate(shall_confirm=not non_interactive)
|
|
349
|
+
authenticate(_AuthMode.DEPLOY, shall_confirm=not non_interactive)
|
|
609
350
|
user_status = poll_verified_status()
|
|
610
351
|
cloudspace_id: Optional[str] = None
|
|
611
352
|
from_onboarding = False
|
lightning_sdk/cli/download.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import re
|
|
3
4
|
from pathlib import Path
|
|
@@ -6,6 +7,7 @@ from typing import Optional
|
|
|
6
7
|
import click
|
|
7
8
|
from rich.console import Console
|
|
8
9
|
|
|
10
|
+
from lightning_sdk.api.license_api import LicenseApi
|
|
9
11
|
from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
10
12
|
from lightning_sdk.cli.exceptions import StudioCliError
|
|
11
13
|
from lightning_sdk.cli.studios_menu import _StudiosMenu
|
|
@@ -46,13 +48,18 @@ def model(name: str, download_dir: str = ".") -> None:
|
|
|
46
48
|
"--studio",
|
|
47
49
|
default=None,
|
|
48
50
|
help=(
|
|
49
|
-
"The name of the studio to
|
|
51
|
+
"The name of the studio to download from. "
|
|
50
52
|
"Will show a menu with user's owned studios for selection if not specified. "
|
|
51
53
|
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
|
|
52
54
|
"The teamspace and studio names can be regular expressions to match, "
|
|
53
55
|
"a menu filtered studios will be shown for final selection."
|
|
54
56
|
),
|
|
55
57
|
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--teamspace",
|
|
60
|
+
default=None,
|
|
61
|
+
help="The teamspace the drive folder is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
|
|
62
|
+
)
|
|
56
63
|
@click.option(
|
|
57
64
|
"--local-path",
|
|
58
65
|
"--local_path",
|
|
@@ -60,32 +67,41 @@ def model(name: str, download_dir: str = ".") -> None:
|
|
|
60
67
|
type=click.Path(file_okay=False, dir_okay=True),
|
|
61
68
|
help="The path to the directory you want to download the folder to.",
|
|
62
69
|
)
|
|
63
|
-
def folder(
|
|
64
|
-
""
|
|
70
|
+
def folder(
|
|
71
|
+
path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = "."
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Download a folder from a Studio or a Teamspace drive folder.
|
|
65
74
|
|
|
66
75
|
Example:
|
|
67
76
|
lightning download folder PATH
|
|
68
77
|
|
|
69
|
-
PATH: The relative path within the Studio you want to download.
|
|
70
|
-
Defaults to the entire
|
|
78
|
+
PATH: The relative path within the Studio or drive folder you want to download.
|
|
79
|
+
Defaults to the entire Studio or drive folder.
|
|
71
80
|
"""
|
|
72
81
|
local_path = Path(local_path)
|
|
73
82
|
if not local_path.is_dir():
|
|
74
83
|
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
if studio and teamspace:
|
|
86
|
+
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
87
|
+
|
|
88
|
+
if studio:
|
|
89
|
+
resolved_downloader = _resolve_studio(studio)
|
|
90
|
+
elif teamspace:
|
|
91
|
+
menu = _TeamspacesMenu()
|
|
92
|
+
resolved_downloader = menu._resolve_teamspace(teamspace)
|
|
77
93
|
|
|
78
94
|
if not path:
|
|
79
|
-
local_path /=
|
|
95
|
+
local_path /= resolved_downloader.name
|
|
80
96
|
path = ""
|
|
81
97
|
|
|
82
98
|
try:
|
|
83
99
|
if not path:
|
|
84
100
|
raise FileNotFoundError()
|
|
85
|
-
|
|
101
|
+
resolved_downloader.download_folder(remote_path=path, target_path=str(local_path))
|
|
86
102
|
except Exception as e:
|
|
87
103
|
raise StudioCliError(
|
|
88
|
-
f"Could not download the folder from the given Studio {studio}. "
|
|
104
|
+
f"Could not download the folder from the given Studio {studio} or Teamspace {teamspace}. "
|
|
89
105
|
"Please contact Lightning AI directly to resolve this issue."
|
|
90
106
|
) from e
|
|
91
107
|
|
|
@@ -103,6 +119,11 @@ def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".")
|
|
|
103
119
|
"a menu filtered studios will be shown for final selection."
|
|
104
120
|
),
|
|
105
121
|
)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--teamspace",
|
|
124
|
+
default=None,
|
|
125
|
+
help="The teamspace the file is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
|
|
126
|
+
)
|
|
106
127
|
@click.option(
|
|
107
128
|
"--local-path",
|
|
108
129
|
"--local_path",
|
|
@@ -110,31 +131,40 @@ def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".")
|
|
|
110
131
|
type=click.Path(file_okay=False, dir_okay=True),
|
|
111
132
|
help="The path to the directory you want to download the file to.",
|
|
112
133
|
)
|
|
113
|
-
def file(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
114
|
-
"""Download a file from a Studio.
|
|
134
|
+
def file(path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = ".") -> None:
|
|
135
|
+
"""Download a file from a Studio or Teamspace drive file.
|
|
115
136
|
|
|
116
137
|
Example:
|
|
117
138
|
lightning download file PATH
|
|
118
139
|
|
|
119
|
-
PATH: The relative path to the file within the Studio you want to download.
|
|
140
|
+
PATH: The relative path to the file within the Studio or Teamspace drive file you want to download.
|
|
120
141
|
"""
|
|
121
142
|
local_path = Path(local_path)
|
|
122
143
|
if not local_path.is_dir():
|
|
123
144
|
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
124
145
|
|
|
125
|
-
|
|
146
|
+
if studio and teamspace:
|
|
147
|
+
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
148
|
+
|
|
149
|
+
if studio:
|
|
150
|
+
resolved_downloader = _resolve_studio(studio)
|
|
151
|
+
elif teamspace:
|
|
152
|
+
menu = _TeamspacesMenu()
|
|
153
|
+
resolved_downloader = menu._resolve_teamspace(teamspace)
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError("Either --studio or --teamspace must be provided")
|
|
126
156
|
|
|
127
157
|
if not path:
|
|
128
|
-
local_path /=
|
|
158
|
+
local_path /= resolved_downloader.name
|
|
129
159
|
path = ""
|
|
130
160
|
|
|
131
161
|
try:
|
|
132
162
|
if not path:
|
|
133
163
|
raise FileNotFoundError()
|
|
134
|
-
|
|
164
|
+
resolved_downloader.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
|
|
135
165
|
except Exception as e:
|
|
136
166
|
raise StudioCliError(
|
|
137
|
-
f"Could not download the file from the given Studio {studio}. "
|
|
167
|
+
f"Could not download the file from the given Studio {studio} or Teamspace {teamspace}. "
|
|
138
168
|
"Please contact Lightning AI directly to resolve this issue."
|
|
139
169
|
) from e
|
|
140
170
|
|
|
@@ -208,3 +238,26 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
|
208
238
|
|
|
209
239
|
with skip_studio_init():
|
|
210
240
|
return Studio(**selected_studio)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@download.command(name="licenses")
|
|
244
|
+
def licenses() -> None:
|
|
245
|
+
"""Download licenses for all products/packages.
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
lightning download licenses
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
user = _get_authed_user()
|
|
252
|
+
api = LicenseApi()
|
|
253
|
+
licenses = api.list_user_licenses(user.id)
|
|
254
|
+
|
|
255
|
+
user_home = Path.home()
|
|
256
|
+
lit_dir = user_home / ".lightning"
|
|
257
|
+
lit_dir.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
licenses_file = lit_dir / "licenses.json"
|
|
259
|
+
|
|
260
|
+
licenses_short = {ll.product_name: ll.license_key for ll in licenses if ll.is_valid}
|
|
261
|
+
with licenses_file.open("w") as fp:
|
|
262
|
+
json.dump(licenses_short, fp, indent=4)
|
|
263
|
+
Console().print(f"Licenses downloaded to {licenses_file}", style="green")
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -14,6 +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.deploy.serve import deploy
|
|
17
18
|
from lightning_sdk.cli.docker_cli import dockerize
|
|
18
19
|
from lightning_sdk.cli.download import download
|
|
19
20
|
from lightning_sdk.cli.generate import generate
|
|
@@ -21,7 +22,6 @@ from lightning_sdk.cli.inspect import inspect
|
|
|
21
22
|
from lightning_sdk.cli.list import list_cli
|
|
22
23
|
from lightning_sdk.cli.open import open
|
|
23
24
|
from lightning_sdk.cli.run import run
|
|
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
|
lightning_sdk/cli/open.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import webbrowser
|
|
2
|
+
from contextlib import suppress
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
@@ -23,7 +24,17 @@ from lightning_sdk.utils.resolve import _get_studio_url
|
|
|
23
24
|
"If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
|
|
24
25
|
),
|
|
25
26
|
)
|
|
26
|
-
|
|
27
|
+
@click.option(
|
|
28
|
+
"--cloud-account",
|
|
29
|
+
"--cloud_account",
|
|
30
|
+
default=None,
|
|
31
|
+
help=(
|
|
32
|
+
"The cloud account to create the studio on. "
|
|
33
|
+
"If not specified, will try to infer from the environment (e.g. when run from within a Studio.) "
|
|
34
|
+
"or fall back to the teamspace default."
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
def open(path: str = ".", teamspace: Optional[str] = None, cloud_account: Optional[str] = None) -> None: # noqa: A001
|
|
27
38
|
"""Open a local file or folder in a Lightning Studio.
|
|
28
39
|
|
|
29
40
|
Example:
|
|
@@ -41,7 +52,15 @@ def open(path: str = ".", teamspace: Optional[str] = None) -> None: # noqa: A00
|
|
|
41
52
|
menu = _TeamspacesMenu()
|
|
42
53
|
resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
|
|
43
54
|
|
|
44
|
-
|
|
55
|
+
# default cloud account to current studios cloud account if run from studio
|
|
56
|
+
# else it will fall back to teamspace default in the backend
|
|
57
|
+
if cloud_account is None:
|
|
58
|
+
with suppress(ValueError):
|
|
59
|
+
s = Studio()
|
|
60
|
+
if s.teamspace.name == resolved_teamspace.name and s.teamspace.owner.name == resolved_teamspace.owner.name:
|
|
61
|
+
cloud_account = s.cloud_account
|
|
62
|
+
|
|
63
|
+
new_studio = Studio(name=pathlib_path.stem, teamspace=resolved_teamspace, cloud_account=cloud_account)
|
|
45
64
|
|
|
46
65
|
console.print(
|
|
47
66
|
f"[bold]Uploading {path} to {new_studio.owner.name}/{new_studio.teamspace.name}/{new_studio.name}[/bold]"
|
lightning_sdk/cli/start.py
CHANGED
|
@@ -4,8 +4,10 @@ import click
|
|
|
4
4
|
|
|
5
5
|
from lightning_sdk import Machine, Studio
|
|
6
6
|
from lightning_sdk.lightning_cloud.openapi.rest import ApiException
|
|
7
|
+
from lightning_sdk.studio import Provider
|
|
7
8
|
|
|
8
9
|
_MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
|
|
10
|
+
_PROVIDER_VALUES = tuple([provider.value for provider in Provider])
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@click.group("start")
|
|
@@ -33,7 +35,14 @@ def start() -> None:
|
|
|
33
35
|
type=click.Choice(_MACHINE_VALUES),
|
|
34
36
|
help="The machine type to start the studio on.",
|
|
35
37
|
)
|
|
36
|
-
|
|
38
|
+
@click.option(
|
|
39
|
+
"--provider",
|
|
40
|
+
default=None,
|
|
41
|
+
show_default=True,
|
|
42
|
+
type=click.Choice(_PROVIDER_VALUES),
|
|
43
|
+
help="The provider to start the studio on.",
|
|
44
|
+
)
|
|
45
|
+
def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU", provider: Optional[str] = None) -> None:
|
|
37
46
|
"""Start a studio on a given machine.
|
|
38
47
|
|
|
39
48
|
Example:
|
|
@@ -50,9 +59,9 @@ def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU") ->
|
|
|
50
59
|
owner, teamspace = None, None
|
|
51
60
|
|
|
52
61
|
try:
|
|
53
|
-
studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False)
|
|
62
|
+
studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False, provider=provider)
|
|
54
63
|
except (RuntimeError, ValueError, ApiException):
|
|
55
|
-
studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
|
|
64
|
+
studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False, provider=provider)
|
|
56
65
|
|
|
57
66
|
try:
|
|
58
67
|
resolved_machine = getattr(Machine, machine.upper(), Machine(machine, machine))
|