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.
Files changed (53) hide show
  1. lightning_sdk/__init__.py +6 -6
  2. lightning_sdk/__version__.py +3 -0
  3. lightning_sdk/agents.py +2 -1
  4. lightning_sdk/ai_hub.py +2 -1
  5. lightning_sdk/api/cloud_account_api.py +2 -2
  6. lightning_sdk/api/deployment_api.py +11 -1
  7. lightning_sdk/api/utils.py +58 -1
  8. lightning_sdk/cli/legacy/deploy/serve.py +16 -2
  9. lightning_sdk/cli/studio/__init__.py +2 -0
  10. lightning_sdk/cli/studio/cp.py +138 -0
  11. lightning_sdk/cli/utils/logging.py +2 -1
  12. lightning_sdk/cli/utils/studio_selection.py +3 -3
  13. lightning_sdk/deployment/__init__.py +2 -0
  14. lightning_sdk/deployment/deployment.py +33 -5
  15. lightning_sdk/helpers.py +1 -1
  16. lightning_sdk/job/base.py +2 -1
  17. lightning_sdk/job/job.py +7 -1
  18. lightning_sdk/lightning_cloud/openapi/__init__.py +5 -2
  19. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  20. lightning_sdk/lightning_cloud/openapi/api/incidents_service_api.py +1058 -0
  21. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +415 -1508
  22. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +105 -0
  23. lightning_sdk/lightning_cloud/openapi/models/__init__.py +4 -2
  24. lightning_sdk/lightning_cloud/openapi/models/{project_id_kubernetestemplates_body.py → cluster_id_kubernetestemplates_body.py} +23 -49
  25. lightning_sdk/lightning_cloud/openapi/models/incident_id_messages_body.py +3 -29
  26. lightning_sdk/lightning_cloud/openapi/models/kubernetestemplates_id_body.py +1 -27
  27. lightning_sdk/lightning_cloud/openapi/models/messages_message_id_body.py +3 -29
  28. lightning_sdk/lightning_cloud/openapi/models/storagetransfers_validate_body.py +149 -0
  29. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_specialized_view.py +1 -1
  30. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +287 -1
  31. lightning_sdk/lightning_cloud/openapi/models/{project_id_incidents_body.py → v1_create_incident_request.py} +61 -35
  32. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
  33. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_template.py +1 -27
  34. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +27 -53
  36. lightning_sdk/lightning_cloud/openapi/models/v1_validate_storage_transfer_response.py +123 -0
  37. lightning_sdk/lit_container.py +9 -0
  38. lightning_sdk/machine.py +8 -0
  39. lightning_sdk/mmt/mmt.py +8 -4
  40. lightning_sdk/models.py +8 -0
  41. lightning_sdk/owner.py +2 -1
  42. lightning_sdk/pipeline/pipeline.py +3 -0
  43. lightning_sdk/plugin.py +2 -1
  44. lightning_sdk/serve.py +3 -1
  45. lightning_sdk/studio.py +10 -5
  46. lightning_sdk/teamspace.py +17 -1
  47. lightning_sdk/utils/logging.py +8 -1
  48. {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/METADATA +1 -1
  49. {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/RECORD +53 -48
  50. {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/LICENSE +0 -0
  51. {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/WHEEL +0 -0
  52. {lightning_sdk-2025.10.31.dist-info → lightning_sdk-2025.11.13.dist-info}/entry_points.txt +0 -0
  53. {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, _set_tqdm_envvars_noninteractive
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
- "VM",
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
- _set_tqdm_envvars_noninteractive()
43
+ set_tqdm_envvars_noninteractive()
@@ -0,0 +1,3 @@
1
+ """Version information for lightning_sdk."""
2
+
3
+ __version__ = "2025.11.13"
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
- "cloud_account ({cloud_account} has cloud provider {cloud_provider_resp}) which don't match!"
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
 
@@ -4,7 +4,8 @@ import math
4
4
  import os
5
5
  import re
6
6
  from concurrent.futures import ThreadPoolExecutor
7
- from functools import partial
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
- studios = self.teamspace.vms if self.vm else self.teamspace.studios
59
- for studio in self.teamspace.studios:
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
@@ -1,4 +1,5 @@
1
1
  from lightning_sdk.api.deployment_api import (
2
+ ApiKeyAuth,
2
3
  AutoScaleConfig,
3
4
  AutoScalingMetric,
4
5
  BasicAuth,
@@ -24,4 +25,5 @@ __all__ = [
24
25
  "Secret",
25
26
  "TokenAuth",
26
27
  "Deployment",
28
+ "ApiKeyAuth",
27
29
  ]
@@ -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=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 _set_tqdm_envvars_noninteractive() -> None:
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__(