lightning-sdk 2025.10.31__py3-none-any.whl → 2025.11.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 +6 -6
- lightning_sdk/__version__.py +3 -0
- lightning_sdk/agents.py +2 -1
- lightning_sdk/ai_hub.py +2 -1
- lightning_sdk/api/cloud_account_api.py +2 -2
- lightning_sdk/api/deployment_api.py +11 -1
- lightning_sdk/api/utils.py +58 -1
- lightning_sdk/cli/legacy/deploy/serve.py +16 -2
- lightning_sdk/cli/studio/__init__.py +2 -0
- lightning_sdk/cli/studio/cp.py +138 -0
- lightning_sdk/cli/utils/logging.py +2 -1
- lightning_sdk/cli/utils/studio_selection.py +3 -3
- lightning_sdk/deployment/__init__.py +2 -0
- lightning_sdk/deployment/deployment.py +33 -5
- lightning_sdk/helpers.py +1 -1
- lightning_sdk/job/base.py +2 -1
- lightning_sdk/job/job.py +7 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +5 -2
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/incidents_service_api.py +1058 -0
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +415 -1508
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +4 -2
- lightning_sdk/lightning_cloud/openapi/models/{project_id_kubernetestemplates_body.py → cluster_id_kubernetestemplates_body.py} +23 -49
- lightning_sdk/lightning_cloud/openapi/models/incident_id_messages_body.py +3 -29
- lightning_sdk/lightning_cloud/openapi/models/kubernetestemplates_id_body.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/messages_message_id_body.py +3 -29
- lightning_sdk/lightning_cloud/openapi/models/storagetransfers_validate_body.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_specialized_view.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +287 -1
- lightning_sdk/lightning_cloud/openapi/models/{project_id_incidents_body.py → v1_create_incident_request.py} +61 -35
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_template.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +27 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_validate_storage_transfer_response.py +123 -0
- lightning_sdk/lit_container.py +9 -0
- lightning_sdk/machine.py +8 -0
- lightning_sdk/mmt/mmt.py +8 -4
- lightning_sdk/models.py +8 -0
- lightning_sdk/owner.py +2 -1
- lightning_sdk/pipeline/pipeline.py +3 -0
- lightning_sdk/plugin.py +2 -1
- lightning_sdk/serve.py +3 -1
- lightning_sdk/studio.py +10 -5
- lightning_sdk/teamspace.py +17 -1
- lightning_sdk/utils/logging.py +8 -1
- {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/RECORD +53 -48
- {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
from lightning_sdk.__version__ import __version__
|
|
1
2
|
from lightning_sdk.agents import Agent
|
|
2
3
|
from lightning_sdk.ai_hub import AIHub
|
|
3
4
|
from lightning_sdk.constants import __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__ # noqa: F401
|
|
4
5
|
from lightning_sdk.deployment import Deployment
|
|
5
|
-
from lightning_sdk.helpers import VersionChecker,
|
|
6
|
+
from lightning_sdk.helpers import VersionChecker, set_tqdm_envvars_noninteractive
|
|
6
7
|
from lightning_sdk.job import Job
|
|
7
8
|
from lightning_sdk.machine import CloudProvider, Machine
|
|
8
9
|
from lightning_sdk.mmt import MMT
|
|
@@ -14,6 +15,8 @@ from lightning_sdk.teamspace import ConnectionType, FolderLocation, Teamspace
|
|
|
14
15
|
from lightning_sdk.user import User
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
18
|
+
"MMT",
|
|
19
|
+
"VM",
|
|
17
20
|
"AIHub",
|
|
18
21
|
"Agent",
|
|
19
22
|
"CloudProvider",
|
|
@@ -23,7 +26,6 @@ __all__ = [
|
|
|
23
26
|
"Job",
|
|
24
27
|
"JobsPlugin",
|
|
25
28
|
"Machine",
|
|
26
|
-
"MMT",
|
|
27
29
|
"MultiMachineTrainingPlugin",
|
|
28
30
|
"Organization",
|
|
29
31
|
"Plugin",
|
|
@@ -32,12 +34,10 @@ __all__ = [
|
|
|
32
34
|
"Studio",
|
|
33
35
|
"Teamspace",
|
|
34
36
|
"User",
|
|
35
|
-
"
|
|
37
|
+
"__version__",
|
|
36
38
|
]
|
|
37
39
|
|
|
38
|
-
__version__ = "2025.10.31"
|
|
39
|
-
|
|
40
40
|
_version_checker = VersionChecker()
|
|
41
41
|
_version_checker.check_and_prompt_upgrade(__version__)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
set_tqdm_envvars_noninteractive()
|
lightning_sdk/agents.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from typing import List, Optional
|
|
2
2
|
|
|
3
3
|
from lightning_sdk.api.agents_api import AgentApi
|
|
4
|
+
from lightning_sdk.utils.logging import TrackCallsMeta
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
class Agent:
|
|
7
|
+
class Agent(metaclass=TrackCallsMeta):
|
|
7
8
|
def __init__(self, agent_id: str) -> None:
|
|
8
9
|
self.id = agent_id
|
|
9
10
|
self._agent_api = AgentApi()
|
lightning_sdk/ai_hub.py
CHANGED
|
@@ -5,6 +5,7 @@ from lightning_sdk.api import AIHubApi
|
|
|
5
5
|
from lightning_sdk.api.utils import _get_cloud_url
|
|
6
6
|
from lightning_sdk.lightning_cloud.openapi.models import V1Deployment
|
|
7
7
|
from lightning_sdk.user import User
|
|
8
|
+
from lightning_sdk.utils.logging import TrackCallsMeta
|
|
8
9
|
from lightning_sdk.utils.resolve import _resolve_teamspace
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -12,7 +13,7 @@ if TYPE_CHECKING:
|
|
|
12
13
|
from lightning_sdk.machine import Machine
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
class AIHub:
|
|
16
|
+
class AIHub(metaclass=TrackCallsMeta):
|
|
16
17
|
"""An interface to interact with the AI Hub.
|
|
17
18
|
|
|
18
19
|
Example:
|
|
@@ -198,8 +198,8 @@ class CloudAccountApi:
|
|
|
198
198
|
cloud_provider_resp = self._get_cloud_account_provider(cloud_account_resp)
|
|
199
199
|
if cloud_provider_resp != cloud_provider:
|
|
200
200
|
raise RuntimeError(
|
|
201
|
-
f"Specified both cloud_provider ({cloud_provider}) and "
|
|
202
|
-
"
|
|
201
|
+
f"Specified both cloud_provider ({cloud_provider}) and cloud_account ({cloud_account} "
|
|
202
|
+
f"has cloud provider {cloud_provider_resp}) which don't match!"
|
|
203
203
|
)
|
|
204
204
|
|
|
205
205
|
return cloud_account
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from time import sleep
|
|
2
2
|
from typing import Any, Dict, List, Literal, Optional, Union
|
|
3
3
|
|
|
4
|
-
from lightning_sdk.api.utils import _machine_to_compute_name
|
|
4
|
+
from lightning_sdk.api.utils import _machine_to_compute_name, resolve_path_mappings
|
|
5
5
|
from lightning_sdk.lightning_cloud.openapi import (
|
|
6
6
|
CreateDeploymentRequestDefinesASpecForTheJobThatAllowsForAutoscalingJobs,
|
|
7
7
|
V1AutoscalingSpec,
|
|
@@ -269,6 +269,7 @@ class DeploymentApi:
|
|
|
269
269
|
quantity: Optional[int] = None,
|
|
270
270
|
include_credentials: Optional[bool] = None,
|
|
271
271
|
max_runtime: Optional[int] = None,
|
|
272
|
+
path_mappings: Optional[Dict[str, str]] = None,
|
|
272
273
|
) -> V1Deployment:
|
|
273
274
|
# Update the deployment in place
|
|
274
275
|
|
|
@@ -288,6 +289,11 @@ class DeploymentApi:
|
|
|
288
289
|
requires_release = False
|
|
289
290
|
requires_release |= apply_change(deployment.spec, "image", image)
|
|
290
291
|
|
|
292
|
+
if path_mappings:
|
|
293
|
+
requires_release |= apply_change(
|
|
294
|
+
deployment.spec, "path_mappings", resolve_path_mappings(path_mappings, None, None)
|
|
295
|
+
)
|
|
296
|
+
|
|
291
297
|
requires_release |= apply_change(deployment.spec, "entrypoint", entrypoint)
|
|
292
298
|
requires_release |= apply_change(deployment.spec, "command", command)
|
|
293
299
|
requires_release |= apply_change(deployment.spec, "env", to_env(env))
|
|
@@ -591,6 +597,7 @@ def to_spec(
|
|
|
591
597
|
cloudspace_id: Optional[None] = None,
|
|
592
598
|
max_runtime: Optional[int] = None,
|
|
593
599
|
machine_image_version: Optional[str] = None,
|
|
600
|
+
path_mappings: Optional[Dict[str, str]] = None,
|
|
594
601
|
) -> V1JobSpec:
|
|
595
602
|
if cloud_account is None:
|
|
596
603
|
raise ValueError("The cloud account should be defined.")
|
|
@@ -612,6 +619,8 @@ def to_spec(
|
|
|
612
619
|
if max_runtime:
|
|
613
620
|
optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
|
|
614
621
|
|
|
622
|
+
path_mapping_list = resolve_path_mappings(path_mappings or {}, None, None)
|
|
623
|
+
|
|
615
624
|
return V1JobSpec(
|
|
616
625
|
cluster_id=cloud_account,
|
|
617
626
|
command=command,
|
|
@@ -625,6 +634,7 @@ def to_spec(
|
|
|
625
634
|
include_credentials=include_credentials,
|
|
626
635
|
cloudspace_id=cloudspace_id,
|
|
627
636
|
machine_image_version=machine_image_version,
|
|
637
|
+
path_mappings=path_mapping_list,
|
|
628
638
|
**optional_spec_kwargs,
|
|
629
639
|
)
|
|
630
640
|
|
lightning_sdk/api/utils.py
CHANGED
|
@@ -4,7 +4,8 @@ import math
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
-
from
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from functools import lru_cache, partial
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, Union
|
|
10
11
|
|
|
@@ -747,3 +748,59 @@ def resolve_path_mappings(
|
|
|
747
748
|
)
|
|
748
749
|
|
|
749
750
|
return path_mappings_list
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class AccessibleResource(Enum):
|
|
754
|
+
Studios = "studio"
|
|
755
|
+
Drive = "drive"
|
|
756
|
+
Jobs = "jobs"
|
|
757
|
+
Deployments = "deployments"
|
|
758
|
+
Pipelines = "pipelines"
|
|
759
|
+
Models = "models"
|
|
760
|
+
Containers = "containers"
|
|
761
|
+
Settings = "settings"
|
|
762
|
+
|
|
763
|
+
def __str__(self) -> str:
|
|
764
|
+
"""Return the string representation of the resource type."""
|
|
765
|
+
return self.value
|
|
766
|
+
|
|
767
|
+
def __repr__(self) -> str:
|
|
768
|
+
"""Return the string representation of the resource type."""
|
|
769
|
+
return self.value
|
|
770
|
+
|
|
771
|
+
def __eq__(self, other: object) -> bool:
|
|
772
|
+
"""Return True if the resource type is equal to the other resource type."""
|
|
773
|
+
if isinstance(other, AccessibleResource):
|
|
774
|
+
return self.value == other.value
|
|
775
|
+
return str(other) == self.value
|
|
776
|
+
|
|
777
|
+
def __hash__(self) -> int:
|
|
778
|
+
"""Return the hash of the resource type."""
|
|
779
|
+
return hash(self.value)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@lru_cache
|
|
783
|
+
def allowed_resource_access(resource_type: AccessibleResource, teamspace_id: str) -> bool:
|
|
784
|
+
# TODO: change this to proper API
|
|
785
|
+
from lightning_sdk.api.teamspace_api import TeamspaceApi
|
|
786
|
+
|
|
787
|
+
teamspace_api = TeamspaceApi()
|
|
788
|
+
teamspace = teamspace_api._get_teamspace_by_id(teamspace_id=teamspace_id)
|
|
789
|
+
|
|
790
|
+
# when we find the tab, check if it is enabled
|
|
791
|
+
if teamspace.layout_config:
|
|
792
|
+
for tab in teamspace.layout_config:
|
|
793
|
+
if tab.slug == resource_type:
|
|
794
|
+
return tab.is_enabled
|
|
795
|
+
|
|
796
|
+
# tab isn't found, allow access by default for backwards compatibility
|
|
797
|
+
# TODO: add additional checks here if required
|
|
798
|
+
return True
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def raise_access_error_if_not_allowed(resource_type: AccessibleResource, teamspace_id: str) -> None:
|
|
802
|
+
if not allowed_resource_access(resource_type, teamspace_id):
|
|
803
|
+
raise PermissionError(
|
|
804
|
+
f"Access to {resource_type.name} has been disabled for this teamspace. "
|
|
805
|
+
"Contact a teamspace administrator to enable it."
|
|
806
|
+
)
|
|
@@ -12,7 +12,7 @@ from rich.console import Console
|
|
|
12
12
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
13
13
|
from rich.prompt import Confirm
|
|
14
14
|
|
|
15
|
-
from lightning_sdk import Machine, Teamspace
|
|
15
|
+
from lightning_sdk import CloudProvider, Machine, Teamspace
|
|
16
16
|
from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
17
17
|
from lightning_sdk.api.utils import _get_registry_url
|
|
18
18
|
from lightning_sdk.cli.legacy.clusters_menu import _ClustersMenu
|
|
@@ -122,6 +122,12 @@ def deploy() -> None:
|
|
|
122
122
|
"If not provided will fall back to the teamspaces default cloud account."
|
|
123
123
|
),
|
|
124
124
|
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"--cloud-provider",
|
|
127
|
+
"--cloud_provider",
|
|
128
|
+
default=None,
|
|
129
|
+
help="The provider to create the studio on. If --cloud-account is specified, this option is prioritized.",
|
|
130
|
+
)
|
|
125
131
|
@click.option("--port", default=8000, help="The port to expose the API on.")
|
|
126
132
|
@click.option("--min_replica", "--min-replica", default=0, help="Number of replicas to start with.")
|
|
127
133
|
@click.option("--max_replica", "--max-replica", default=1, help="Number of replicas to scale up to.")
|
|
@@ -152,6 +158,7 @@ def api(
|
|
|
152
158
|
max_replica: Optional[int],
|
|
153
159
|
replicas: Optional[int],
|
|
154
160
|
no_credentials: Optional[bool],
|
|
161
|
+
cloud_provider: Optional[str],
|
|
155
162
|
) -> None:
|
|
156
163
|
"""Deploy a LitServe model script."""
|
|
157
164
|
return api_impl(
|
|
@@ -172,6 +179,7 @@ def api(
|
|
|
172
179
|
min_replica=min_replica,
|
|
173
180
|
max_replica=max_replica,
|
|
174
181
|
include_credentials=not no_credentials,
|
|
182
|
+
cloud_provider=cloud_provider,
|
|
175
183
|
)
|
|
176
184
|
|
|
177
185
|
|
|
@@ -194,6 +202,7 @@ def api_impl(
|
|
|
194
202
|
max_replica: Optional[int] = 1,
|
|
195
203
|
replicas: Optional[int] = 1,
|
|
196
204
|
include_credentials: Optional[bool] = True,
|
|
205
|
+
cloud_provider: Optional[str] = None,
|
|
197
206
|
) -> None:
|
|
198
207
|
"""Deploy a LitServe model script."""
|
|
199
208
|
console = Console()
|
|
@@ -226,6 +235,7 @@ def api_impl(
|
|
|
226
235
|
return _handle_devbox(name, script_path, console, non_interactive, machine, interruptible, teamspace, org, user)
|
|
227
236
|
|
|
228
237
|
machine = Machine.from_str(machine)
|
|
238
|
+
cloud_provider = CloudProvider.from_str(cloud_provider) if cloud_provider else None
|
|
229
239
|
return _handle_cloud(
|
|
230
240
|
script_path,
|
|
231
241
|
console,
|
|
@@ -243,6 +253,7 @@ def api_impl(
|
|
|
243
253
|
max_replica=max_replica,
|
|
244
254
|
replicas=replicas,
|
|
245
255
|
include_credentials=include_credentials,
|
|
256
|
+
cloud_provider=cloud_provider,
|
|
246
257
|
)
|
|
247
258
|
|
|
248
259
|
|
|
@@ -304,6 +315,7 @@ def _handle_cloud(
|
|
|
304
315
|
max_replica: Optional[int] = 1,
|
|
305
316
|
replicas: Optional[int] = 1,
|
|
306
317
|
include_credentials: Optional[bool] = True,
|
|
318
|
+
cloud_provider: Optional[CloudProvider] = None,
|
|
307
319
|
) -> None:
|
|
308
320
|
if not is_connected():
|
|
309
321
|
console.print("❌ Internet connection required to deploy to the cloud.", style="red")
|
|
@@ -379,7 +391,7 @@ def _handle_cloud(
|
|
|
379
391
|
resolved_teamspace = select_teamspace(teamspace, org, user)
|
|
380
392
|
|
|
381
393
|
lightning_containers_cloud_account = cloud_account
|
|
382
|
-
if not cloud_account:
|
|
394
|
+
if not cloud_account and not cloud_provider:
|
|
383
395
|
clusters_menu = _ClustersMenu()
|
|
384
396
|
lightning_containers_cloud_account = clusters_menu._resolve_cluster(resolved_teamspace)
|
|
385
397
|
cloud_account = resolved_teamspace.default_cloud_account
|
|
@@ -414,6 +426,7 @@ def _handle_cloud(
|
|
|
414
426
|
"include_credentials": include_credentials,
|
|
415
427
|
"cloudspace_id": cloudspace_id,
|
|
416
428
|
"from_onboarding": from_onboarding,
|
|
429
|
+
"cloud_provider": cloud_provider,
|
|
417
430
|
},
|
|
418
431
|
)
|
|
419
432
|
thread.start()
|
|
@@ -446,6 +459,7 @@ def _handle_cloud(
|
|
|
446
459
|
include_credentials=include_credentials,
|
|
447
460
|
cloudspace_id=cloudspace_id,
|
|
448
461
|
from_onboarding=from_onboarding,
|
|
462
|
+
cloud_provider=cloud_provider,
|
|
449
463
|
)
|
|
450
464
|
console.print(f"🚀 Deployment started, access at [i]{deployment_status.get('url')}[/i]")
|
|
451
465
|
if user_status["onboarded"]:
|
|
@@ -6,6 +6,7 @@ import click
|
|
|
6
6
|
def register_commands(group: click.Group) -> None:
|
|
7
7
|
"""Register studio commands with the given group."""
|
|
8
8
|
from lightning_sdk.cli.studio.connect import connect_studio
|
|
9
|
+
from lightning_sdk.cli.studio.cp import cp_studio_file
|
|
9
10
|
from lightning_sdk.cli.studio.create import create_studio
|
|
10
11
|
from lightning_sdk.cli.studio.delete import delete_studio
|
|
11
12
|
from lightning_sdk.cli.studio.list import list_studios
|
|
@@ -22,3 +23,4 @@ def register_commands(group: click.Group) -> None:
|
|
|
22
23
|
group.add_command(stop_studio)
|
|
23
24
|
group.add_command(switch_studio)
|
|
24
25
|
group.add_command(connect_studio)
|
|
26
|
+
group.add_command(cp_studio_file)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Studio cp command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from lightning_sdk.api.utils import _get_cloud_url
|
|
10
|
+
from lightning_sdk.cli.legacy.exceptions import StudioCliError
|
|
11
|
+
from lightning_sdk.cli.utils.owner_selection import OwnerMenu
|
|
12
|
+
from lightning_sdk.cli.utils.studio_selection import StudiosMenu
|
|
13
|
+
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
14
|
+
from lightning_sdk.studio import Studio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command("cp")
|
|
18
|
+
@click.argument("source", nargs=1)
|
|
19
|
+
@click.argument("destination", nargs=1)
|
|
20
|
+
def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = None) -> None:
|
|
21
|
+
"""Copy a Studio file.
|
|
22
|
+
|
|
23
|
+
SOURCE: Source file to copy from. For Studio files, use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
|
|
24
|
+
|
|
25
|
+
DESTINATION: Destination file to copy to. For Studio files, use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
lightning studio cp source.txt lit://<owner>/<my-teamspace>/studios/<my-studio>/destination.txt
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
return cp_impl(source=source, destination=destination)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cp_impl(source: str, destination: str) -> None:
|
|
35
|
+
if "lit://" in source and "lit://" in destination:
|
|
36
|
+
raise ValueError("Both source and destination cannot be Studio files.")
|
|
37
|
+
elif "lit://" not in source and "lit://" not in destination:
|
|
38
|
+
raise ValueError("Either source or destination must be a Studio file.")
|
|
39
|
+
elif "lit://" in source:
|
|
40
|
+
# Download from Studio to local
|
|
41
|
+
cp_download(studio_file_path=source, local_file_path=destination)
|
|
42
|
+
else:
|
|
43
|
+
# Upload from local to Studio
|
|
44
|
+
cp_upload(local_file_path=source, studio_file_path=destination)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StudioPathResult(TypedDict):
|
|
48
|
+
owner: Optional[str]
|
|
49
|
+
teamspace: Optional[str]
|
|
50
|
+
studio: Optional[str]
|
|
51
|
+
destination: Optional[str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_studio_path(studio_path: str) -> StudioPathResult:
|
|
55
|
+
path_string = studio_path.removeprefix("lit://")
|
|
56
|
+
if not path_string:
|
|
57
|
+
raise ValueError("Studio path cannot be empty after prefix")
|
|
58
|
+
|
|
59
|
+
result: StudioPathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
|
|
60
|
+
|
|
61
|
+
if "/studios/" in path_string:
|
|
62
|
+
prefix_part, suffix_part = path_string.split("/studios/", 1)
|
|
63
|
+
|
|
64
|
+
# org and teamspace
|
|
65
|
+
if prefix_part:
|
|
66
|
+
org_ts_components = prefix_part.split("/")
|
|
67
|
+
if len(org_ts_components) == 2:
|
|
68
|
+
result["owner"], result["teamspace"] = org_ts_components
|
|
69
|
+
elif len(org_ts_components) == 1:
|
|
70
|
+
result["teamspace"] = org_ts_components[0]
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(f"Invalid format: '{prefix_part}'")
|
|
73
|
+
|
|
74
|
+
# studio and destination
|
|
75
|
+
path_parts = suffix_part.split("/")
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
# studio and destination
|
|
79
|
+
path_parts = path_string.split("/")
|
|
80
|
+
|
|
81
|
+
if not path_parts or len(path_parts) < 2:
|
|
82
|
+
raise ValueError("Invalid: Missing studio name.")
|
|
83
|
+
|
|
84
|
+
result["studio"] = path_parts[0]
|
|
85
|
+
result["destination"] = "/".join(path_parts[1:])
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cp_upload(
|
|
91
|
+
local_file_path: str,
|
|
92
|
+
studio_file_path: str,
|
|
93
|
+
) -> None:
|
|
94
|
+
console = Console()
|
|
95
|
+
if Path(local_file_path).is_dir():
|
|
96
|
+
raise StudioCliError(
|
|
97
|
+
f"The provided path is a folder: {local_file_path}. Use `lightning upload folder` instead."
|
|
98
|
+
)
|
|
99
|
+
if not Path(local_file_path).exists():
|
|
100
|
+
raise FileNotFoundError(f"The provided path does not exist: {local_file_path}.")
|
|
101
|
+
|
|
102
|
+
studio_path_result = parse_studio_path(studio_file_path)
|
|
103
|
+
|
|
104
|
+
selected_studio = resolve_studio(
|
|
105
|
+
studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
|
|
106
|
+
)
|
|
107
|
+
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
108
|
+
|
|
109
|
+
selected_studio.upload_file(local_file_path, studio_file_path)
|
|
110
|
+
|
|
111
|
+
studio_url = (
|
|
112
|
+
_get_cloud_url().replace(":443", "")
|
|
113
|
+
+ "/"
|
|
114
|
+
+ selected_studio.owner.name
|
|
115
|
+
+ "/"
|
|
116
|
+
+ selected_studio.teamspace.name
|
|
117
|
+
+ "/studios/"
|
|
118
|
+
+ selected_studio.name
|
|
119
|
+
)
|
|
120
|
+
console.print(f"See your file at {studio_url}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cp_download(
|
|
124
|
+
studio_file_path: str,
|
|
125
|
+
local_file_path: str,
|
|
126
|
+
) -> None:
|
|
127
|
+
raise NotImplementedError("Download functionality is not implemented yet.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
|
|
131
|
+
owner_menu = OwnerMenu()
|
|
132
|
+
resolved_owner = owner_menu(owner=owner)
|
|
133
|
+
|
|
134
|
+
teamspace_menu = TeamspacesMenu(resolved_owner)
|
|
135
|
+
resolved_teamspace = teamspace_menu(teamspace=teamspace)
|
|
136
|
+
|
|
137
|
+
studio_menu = StudiosMenu(resolved_teamspace)
|
|
138
|
+
return studio_menu(studio=studio_name)
|
|
@@ -12,6 +12,7 @@ from rich.panel import Panel
|
|
|
12
12
|
from rich.syntax import Syntax
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
|
+
from lightning_sdk.__version__ import __version__
|
|
15
16
|
from lightning_sdk.cli.utils import rich_to_str
|
|
16
17
|
from lightning_sdk.constants import _LIGHTNING_DEBUG
|
|
17
18
|
from lightning_sdk.lightning_cloud.openapi.models.v1_create_sdk_command_history_request import (
|
|
@@ -29,7 +30,7 @@ def _log_command(message: str = "", duration: int = 0, error: Optional[str] = No
|
|
|
29
30
|
body = V1CreateSDKCommandHistoryRequest(
|
|
30
31
|
command=original_command,
|
|
31
32
|
duration=duration,
|
|
32
|
-
message=message,
|
|
33
|
+
message=f"VERSION: {__version__} | {message}",
|
|
33
34
|
project_id=None,
|
|
34
35
|
severity=V1SDKCommandHistorySeverity.INFO,
|
|
35
36
|
type=V1SDKCommandHistoryType.CLI,
|
|
@@ -52,11 +52,11 @@ class StudiosMenu:
|
|
|
52
52
|
|
|
53
53
|
def _get_possible_studios(self) -> Dict[str, Union[Studio, VM]]:
|
|
54
54
|
"""Get all available studios in the teamspace."""
|
|
55
|
-
studios = {}
|
|
55
|
+
studios: Dict[str, Union[Studio, VM]] = {}
|
|
56
56
|
|
|
57
57
|
user = _get_authed_user()
|
|
58
|
-
|
|
59
|
-
for studio in
|
|
58
|
+
teamspace_studios = self.teamspace.vms if self.vm else self.teamspace.studios
|
|
59
|
+
for studio in teamspace_studios:
|
|
60
60
|
if studio._studio.user_id == user.id:
|
|
61
61
|
studios[studio.name] = studio
|
|
62
62
|
return studios
|
|
@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
|
-
from lightning_sdk.api import UserApi
|
|
7
|
+
from lightning_sdk.api import CloudAccountApi, UserApi
|
|
8
8
|
from lightning_sdk.api.deployment_api import (
|
|
9
9
|
ApiKeyAuth,
|
|
10
10
|
Auth,
|
|
@@ -28,18 +28,20 @@ from lightning_sdk.api.deployment_api import (
|
|
|
28
28
|
to_spec,
|
|
29
29
|
to_strategy,
|
|
30
30
|
)
|
|
31
|
+
from lightning_sdk.api.utils import AccessibleResource, raise_access_error_if_not_allowed
|
|
31
32
|
from lightning_sdk.lightning_cloud import login
|
|
32
33
|
from lightning_sdk.lightning_cloud.openapi import V1Deployment
|
|
33
|
-
from lightning_sdk.machine import Machine
|
|
34
|
+
from lightning_sdk.machine import CloudProvider, Machine
|
|
34
35
|
from lightning_sdk.organization import Organization
|
|
35
36
|
from lightning_sdk.services.utilities import _get_cluster
|
|
36
37
|
from lightning_sdk.studio import Studio
|
|
37
38
|
from lightning_sdk.teamspace import Teamspace
|
|
38
39
|
from lightning_sdk.user import User
|
|
40
|
+
from lightning_sdk.utils.logging import TrackCallsMeta
|
|
39
41
|
from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_org, _resolve_teamspace, _resolve_user
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
class Deployment:
|
|
44
|
+
class Deployment(metaclass=TrackCallsMeta):
|
|
43
45
|
"""The Lightning AI Deployment.
|
|
44
46
|
|
|
45
47
|
Allows to fully control a deployment, including retrieving the status, making new release
|
|
@@ -65,6 +67,7 @@ class Deployment:
|
|
|
65
67
|
user: Optional[Union[str, User]] = None,
|
|
66
68
|
) -> None:
|
|
67
69
|
self._request_session = None
|
|
70
|
+
self._cloud_account_api = CloudAccountApi()
|
|
68
71
|
|
|
69
72
|
self._auth = login.Auth()
|
|
70
73
|
self._user = None
|
|
@@ -91,6 +94,8 @@ class Deployment:
|
|
|
91
94
|
if self._teamspace is None:
|
|
92
95
|
raise ValueError("You need to pass a teamspace or an org for your deployment.")
|
|
93
96
|
|
|
97
|
+
raise_access_error_if_not_allowed(AccessibleResource.Deployments, self._teamspace.id)
|
|
98
|
+
|
|
94
99
|
self._deployment_api = DeploymentApi()
|
|
95
100
|
self._cloud_account = _get_cluster(client=self._deployment_api._client, project_id=self._teamspace.id)
|
|
96
101
|
self._is_created = False
|
|
@@ -132,6 +137,8 @@ class Deployment:
|
|
|
132
137
|
from_onboarding: Optional[bool] = None,
|
|
133
138
|
from_litserve: Optional[bool] = None,
|
|
134
139
|
max_runtime: Optional[int] = None,
|
|
140
|
+
path_mappings: Optional[Dict[str, str]] = None,
|
|
141
|
+
cloud_provider: Optional[CloudProvider] = None,
|
|
135
142
|
) -> None:
|
|
136
143
|
"""The Lightning AI Deployment.
|
|
137
144
|
|
|
@@ -163,12 +170,22 @@ class Deployment:
|
|
|
163
170
|
Irrelevant for most machines, required for some of the top-end machines on GCP.
|
|
164
171
|
If in doubt, set it. Won't have an effect on machines not requiring it.
|
|
165
172
|
Defaults to 3h
|
|
173
|
+
path_mappings: Dictionary of path mappings. The keys are the path inside the container whereas the value
|
|
174
|
+
represents the data-connection name and the path inside that connection.
|
|
175
|
+
Should be of form
|
|
176
|
+
{
|
|
177
|
+
"<CONTAINER_PATH_1>": "<CONNECTION_NAME_1>:<PATH_WITHIN_CONNECTION_1>",
|
|
178
|
+
"<CONTAINER_PATH_2>": "<CONNECTION_NAME_2>"
|
|
179
|
+
}
|
|
180
|
+
If the path inside the connection is omitted it's assumed to be the root path of that connection.
|
|
181
|
+
Only applicable when deploying docker containers.
|
|
166
182
|
|
|
167
183
|
Note:
|
|
168
184
|
Since a teamspace can either be owned by an org or by a user directly,
|
|
169
185
|
only one of the arguments can be provided.
|
|
170
186
|
|
|
171
187
|
"""
|
|
188
|
+
raise_access_error_if_not_allowed(AccessibleResource.Deployments, self._teamspace.id)
|
|
172
189
|
if self._is_created:
|
|
173
190
|
raise RuntimeError("This deployment has already been started.")
|
|
174
191
|
|
|
@@ -188,10 +205,17 @@ class Deployment:
|
|
|
188
205
|
if cloud_account is None:
|
|
189
206
|
cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
|
|
190
207
|
|
|
191
|
-
if cloud_account is None and self._cloud_account is not None:
|
|
208
|
+
if cloud_account is None and self._cloud_account is not None and cloud_provider is None:
|
|
192
209
|
print(f"No cloud account was provided, defaulting to {self._cloud_account.cluster_id}")
|
|
193
210
|
cloud_account = os.getenv("LIGHTNING_CLUSTER_ID") or self._cloud_account.cluster_id
|
|
194
211
|
|
|
212
|
+
_cloud_account = self._cloud_account_api.resolve_cloud_account(
|
|
213
|
+
self.teamspace.id,
|
|
214
|
+
cloud_account=cloud_account,
|
|
215
|
+
cloud_provider=cloud_provider,
|
|
216
|
+
default_cloud_account=self._teamspace.default_cloud_account,
|
|
217
|
+
)
|
|
218
|
+
|
|
195
219
|
if isinstance(ports, float):
|
|
196
220
|
ports = [ports]
|
|
197
221
|
|
|
@@ -226,7 +250,7 @@ class Deployment:
|
|
|
226
250
|
replicas=replicas,
|
|
227
251
|
cloudspace_id=cloudspace_id,
|
|
228
252
|
spec=to_spec(
|
|
229
|
-
cloud_account=
|
|
253
|
+
cloud_account=_cloud_account,
|
|
230
254
|
command=command,
|
|
231
255
|
entrypoint=entrypoint,
|
|
232
256
|
env=env,
|
|
@@ -239,6 +263,7 @@ class Deployment:
|
|
|
239
263
|
cloudspace_id=cloudspace_id,
|
|
240
264
|
max_runtime=max_runtime,
|
|
241
265
|
machine_image_version=machine_image_version,
|
|
266
|
+
path_mappings=path_mappings,
|
|
242
267
|
),
|
|
243
268
|
strategy=to_strategy(release_strategy),
|
|
244
269
|
),
|
|
@@ -275,7 +300,9 @@ class Deployment:
|
|
|
275
300
|
quantity: Optional[int] = None,
|
|
276
301
|
include_credentials: Optional[bool] = None,
|
|
277
302
|
max_runtime: Optional[int] = None,
|
|
303
|
+
path_mappings: Optional[Dict[str, str]] = None,
|
|
278
304
|
) -> None:
|
|
305
|
+
raise_access_error_if_not_allowed(AccessibleResource.Deployments, self._teamspace.id)
|
|
279
306
|
cloud_account = _resolve_deprecated_cluster(cloud_account, cluster)
|
|
280
307
|
|
|
281
308
|
if command is None and commands is not None:
|
|
@@ -302,6 +329,7 @@ class Deployment:
|
|
|
302
329
|
quantity=quantity,
|
|
303
330
|
include_credentials=include_credentials,
|
|
304
331
|
max_runtime=max_runtime,
|
|
332
|
+
path_mappings=path_mappings,
|
|
305
333
|
)
|
|
306
334
|
|
|
307
335
|
def stop(self) -> None:
|
lightning_sdk/helpers.py
CHANGED
|
@@ -73,7 +73,7 @@ class VersionChecker:
|
|
|
73
73
|
self._warning_shown = True
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def
|
|
76
|
+
def set_tqdm_envvars_noninteractive() -> None:
|
|
77
77
|
# note: stderr is the default stream tqdm writes progressbars to
|
|
78
78
|
# so we check that one.
|
|
79
79
|
if os.isatty(sys.stderr.fileno()):
|
lightning_sdk/job/base.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict, Union
|
|
|
4
4
|
|
|
5
5
|
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
6
6
|
from lightning_sdk.api.utils import _get_cloud_url
|
|
7
|
+
from lightning_sdk.utils.logging import TrackCallsABCMeta
|
|
7
8
|
from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_teamspace, in_studio, skip_studio_setup
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
@@ -29,7 +30,7 @@ class JobDict(MachineDict):
|
|
|
29
30
|
total_cost: float
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
class _BaseJob(ABC):
|
|
33
|
+
class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
33
34
|
"""Base interface to all job types."""
|
|
34
35
|
|
|
35
36
|
def __init__(
|