lightning-sdk 2025.12.16__py3-none-any.whl → 2026.1.22__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/__version__.py +1 -1
- lightning_sdk/api/k8s_api.py +75 -29
- lightning_sdk/api/org_api.py +7 -0
- lightning_sdk/api/studio_api.py +223 -36
- lightning_sdk/api/teamspace_api.py +28 -9
- lightning_sdk/api/user_api.py +5 -0
- lightning_sdk/cli/cp/__init__.py +64 -0
- lightning_sdk/cli/entrypoint.py +2 -0
- lightning_sdk/cli/groups.py +22 -0
- lightning_sdk/cli/legacy/clusters_menu.py +2 -2
- lightning_sdk/cli/legacy/deploy/_auth.py +7 -6
- lightning_sdk/cli/legacy/download.py +2 -1
- lightning_sdk/cli/legacy/run.py +13 -2
- lightning_sdk/cli/legacy/studios_menu.py +8 -1
- lightning_sdk/cli/studio/__init__.py +4 -0
- lightning_sdk/cli/studio/cp.py +20 -64
- lightning_sdk/cli/studio/ls.py +57 -0
- lightning_sdk/cli/studio/rm.py +71 -0
- lightning_sdk/cli/utils/logging.py +2 -1
- lightning_sdk/cli/utils/studio_filesystem.py +65 -0
- lightning_sdk/cli/utils/teamspace_selection.py +5 -0
- lightning_sdk/exceptions.py +4 -0
- lightning_sdk/job/base.py +24 -5
- lightning_sdk/job/job.py +12 -5
- lightning_sdk/job/v1.py +5 -2
- lightning_sdk/job/v2.py +9 -1
- lightning_sdk/k8s_cluster.py +9 -10
- lightning_sdk/lightning_cloud/__version__.py +1 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +46 -3
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +118 -1
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +787 -125
- lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +246 -19
- lightning_sdk/lightning_cloud/openapi/api/lightningwork_service_api.py +116 -11
- lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +588 -2
- lightning_sdk/lightning_cloud/openapi/api/models_store_api.py +9 -1
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/virtual_machine_service_api.py +557 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +45 -3
- lightning_sdk/lightning_cloud/openapi/models/cloud_space_environment_template_service_update_cloud_space_environment_template_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_add_container_registry_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_cluster_capacity_reservation_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_machine_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_org_cluster_capacity_reservation_body.py +409 -0
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_refresh_container_registry_credentials_body.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_report_machine_system_metrics_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_validate_container_registry_body.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_create_lit_logger_media_body.py +305 -0
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_lit_logger_media_body.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_metrics_stream_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_add_container_registry_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_capacity_reservation_used_by.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_specialized_view.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +53 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +0 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +281 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_integration.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_status.py +105 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_lit_logger_media_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_org_cluster_capacity_reservation_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_sdk_command_history_request.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cudo_direct_v1.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_container_registry_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_lit_logger_media_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_virtual_machine_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ai_pod_v1.py → v1_ecr_registry_config.py} +49 -49
- lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_config_input.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_details.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_get_artifacts_page_response.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_get_kubernetes_pod_logs_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_get_machine_response.py → v1_get_kubernetes_pod_response.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_joinable_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_setting.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_type.py +108 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_settings_v1.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod_logs_page.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_config.py +357 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_response.py +43 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_sort_order.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_lit_logger_media_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_models_response.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_list_virtual_machines_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_logger_media.py +513 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +53 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +107 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_media_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_report_cloud_space_instance_idle_state_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_report_machine_system_metrics_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_tenant_credentials.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +217 -113
- lightning_sdk/lightning_cloud/openapi/models/v1_validate_container_registry_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_virtual_machine.py +409 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_vm_configuration.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_vm_provider_configuration.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_create_virtual_machine_body.py +565 -0
- lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_update_virtual_machine_body.py +97 -0
- lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
- lightning_sdk/lightning_cloud/rest_client.py +0 -2
- lightning_sdk/machine.py +3 -3
- lightning_sdk/mmt/base.py +26 -7
- lightning_sdk/mmt/mmt.py +11 -6
- lightning_sdk/mmt/v1.py +5 -2
- lightning_sdk/mmt/v2.py +5 -2
- lightning_sdk/organization.py +10 -1
- lightning_sdk/owner.py +4 -0
- lightning_sdk/plugin.py +2 -2
- lightning_sdk/studio.py +47 -6
- lightning_sdk/user.py +22 -2
- lightning_sdk/utils/logging.py +2 -1
- lightning_sdk/utils/resolve.py +9 -7
- {lightning_sdk-2025.12.16.dist-info → lightning_sdk-2026.1.22.dist-info}/METADATA +1 -5
- {lightning_sdk-2025.12.16.dist-info → lightning_sdk-2026.1.22.dist-info}/RECORD +139 -94
- {lightning_sdk-2025.12.16.dist-info → lightning_sdk-2026.1.22.dist-info}/WHEEL +1 -1
- lightning_sdk/lightning_cloud/cli/__main__.py +0 -29
- lightning_sdk/lightning_cloud/source_code/logs_socket_api.py +0 -103
- /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mm_ts_response.py → v1_list_filesystem_mmts_response.py} +0 -0
- {lightning_sdk-2025.12.16.dist-info → lightning_sdk-2026.1.22.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.12.16.dist-info → lightning_sdk-2026.1.22.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.12.16.dist-info → lightning_sdk-2026.1.22.dist-info}/top_level.txt +0 -0
lightning_sdk/cli/groups.py
CHANGED
|
@@ -4,6 +4,7 @@ import click
|
|
|
4
4
|
|
|
5
5
|
from lightning_sdk.cli.base_studio import register_commands as register_base_studio_commands
|
|
6
6
|
from lightning_sdk.cli.config import register_commands as register_config_commands
|
|
7
|
+
from lightning_sdk.cli.cp import register_commands as register_cp_commands
|
|
7
8
|
from lightning_sdk.cli.job import register_commands as register_job_commands
|
|
8
9
|
from lightning_sdk.cli.license import register_commands as register_license_commands
|
|
9
10
|
from lightning_sdk.cli.mmt import register_commands as register_mmt_commands
|
|
@@ -46,6 +47,26 @@ def license() -> None: # noqa: A001
|
|
|
46
47
|
"""Manage Lightning AI Product Licenses."""
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
@click.command(name="cp")
|
|
51
|
+
@click.argument("source")
|
|
52
|
+
@click.argument("destination", required=False)
|
|
53
|
+
@click.option("--recursive", "-r", is_flag=True, help="Copy directories recursively")
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def cp() -> None:
|
|
56
|
+
"""Copy files between local filesystem, Studios, and teamspace drives.
|
|
57
|
+
|
|
58
|
+
\b
|
|
59
|
+
URL formats:
|
|
60
|
+
Studios: lit://<owner>/<teamspace>/studios/<studio-name>/<path>
|
|
61
|
+
Teamspace drives: lit://<owner>/<teamspace>/uploads/<path>
|
|
62
|
+
|
|
63
|
+
\b
|
|
64
|
+
Examples:
|
|
65
|
+
lightning studio cp source.txt lit://<owner>/<my-teamspace>/studios/<my-studio>/destination.txt
|
|
66
|
+
lightning studio cp -r source_folder/ lit://<owner>/<my-teamspace>/studios/<my-studio>/destination_folder/
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
49
70
|
# Register config commands with the main config group
|
|
50
71
|
register_job_commands(job)
|
|
51
72
|
register_mmt_commands(mmt)
|
|
@@ -54,3 +75,4 @@ register_config_commands(config)
|
|
|
54
75
|
register_vm_commands(vm)
|
|
55
76
|
register_base_studio_commands(base_studio)
|
|
56
77
|
register_license_commands(license)
|
|
78
|
+
register_cp_commands(cp)
|
|
@@ -32,8 +32,8 @@ class _ClustersMenu:
|
|
|
32
32
|
|
|
33
33
|
cloud_account_api = CloudAccountApi()
|
|
34
34
|
|
|
35
|
-
resolved_cluster_obj = cloud_account_api.
|
|
36
|
-
cloud_account_id=selected_cluster_id,
|
|
35
|
+
resolved_cluster_obj = cloud_account_api.get_cloud_account_non_org(
|
|
36
|
+
cloud_account_id=selected_cluster_id, teamspace_id=teamspace.id
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
return None if resolved_cluster_obj.spec.cluster_type == V1ClusterType.GLOBAL else resolved_cluster_obj.id
|
|
@@ -8,7 +8,7 @@ from urllib.parse import urlencode
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.prompt import Confirm
|
|
10
10
|
|
|
11
|
-
from lightning_sdk import Teamspace
|
|
11
|
+
from lightning_sdk import Organization, Teamspace
|
|
12
12
|
from lightning_sdk.api import UserApi
|
|
13
13
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
14
14
|
from lightning_sdk.lightning_cloud import env
|
|
@@ -74,13 +74,14 @@ def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
|
|
|
74
74
|
def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
|
|
75
75
|
if teamspace is None:
|
|
76
76
|
menu = TeamspacesMenu()
|
|
77
|
-
|
|
77
|
+
auth_user = _get_authed_user()
|
|
78
|
+
possible_teamspaces = menu._get_possible_teamspaces(auth_user)
|
|
78
79
|
if len(possible_teamspaces) == 1:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
teamspace_name = next(iter(possible_teamspaces.values()))
|
|
81
|
+
if isinstance(menu._owner, Organization):
|
|
82
|
+
return Teamspace(name=teamspace_name, org=menu._owner, user=None)
|
|
83
|
+
return Teamspace(name=teamspace_name, org=None, user=menu._owner)
|
|
82
84
|
return menu(teamspace)
|
|
83
|
-
|
|
84
85
|
return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
|
|
85
86
|
|
|
86
87
|
|
|
@@ -242,7 +242,8 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
|
242
242
|
and (re.match(studio_name, st["name"]) or studio_name in st["name"]),
|
|
243
243
|
possible_studios,
|
|
244
244
|
)
|
|
245
|
-
|
|
245
|
+
possible_studios = list(possible_studios)
|
|
246
|
+
if len(possible_studios) == 0:
|
|
246
247
|
raise ValueError(
|
|
247
248
|
f"Could not find Studio like '{studio}', please consider update your filtering pattern."
|
|
248
249
|
)
|
lightning_sdk/cli/legacy/run.py
CHANGED
|
@@ -50,6 +50,15 @@ def run() -> None:
|
|
|
50
50
|
help="The organization owning the teamspace (if any). Defaults to the current organization.",
|
|
51
51
|
)
|
|
52
52
|
@click.option("--user", default=None, help="The user owning the teamspace (if any). Defaults to the current user.")
|
|
53
|
+
@click.option(
|
|
54
|
+
"--cloud-provider",
|
|
55
|
+
"--cloud_provider",
|
|
56
|
+
default=None,
|
|
57
|
+
help=(
|
|
58
|
+
"The provider to create the studio on. If set, must be in agreement with the provider from the "
|
|
59
|
+
"cloud_account (if specified)."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
53
62
|
@click.option(
|
|
54
63
|
"--cloud-account",
|
|
55
64
|
"--cloud_account",
|
|
@@ -96,7 +105,7 @@ def run() -> None:
|
|
|
96
105
|
)
|
|
97
106
|
@click.option(
|
|
98
107
|
"--entrypoint",
|
|
99
|
-
default=
|
|
108
|
+
default=None,
|
|
100
109
|
show_default=True,
|
|
101
110
|
help=(
|
|
102
111
|
"The entrypoint of your docker container. "
|
|
@@ -141,6 +150,7 @@ def job(
|
|
|
141
150
|
teamspace: Optional[str] = None,
|
|
142
151
|
org: Optional[str] = None,
|
|
143
152
|
user: Optional[str] = None,
|
|
153
|
+
cloud_provider: Optional[str] = None,
|
|
144
154
|
cloud_account: Optional[str] = None,
|
|
145
155
|
env: Sequence[str] = (),
|
|
146
156
|
interruptible: bool = False,
|
|
@@ -184,6 +194,7 @@ def job(
|
|
|
184
194
|
teamspace=resolved_teamspace,
|
|
185
195
|
org=org,
|
|
186
196
|
user=user,
|
|
197
|
+
cloud_provider=cloud_provider,
|
|
187
198
|
cloud_account=cloud_account,
|
|
188
199
|
env=env_dict,
|
|
189
200
|
interruptible=interruptible,
|
|
@@ -289,7 +300,7 @@ def job(
|
|
|
289
300
|
)
|
|
290
301
|
@click.option(
|
|
291
302
|
"--entrypoint",
|
|
292
|
-
default=
|
|
303
|
+
default=None,
|
|
293
304
|
show_default=True,
|
|
294
305
|
help=(
|
|
295
306
|
"The entrypoint of your docker container. "
|
|
@@ -31,7 +31,14 @@ class _StudiosMenu:
|
|
|
31
31
|
title = "Please select a Studio of the following studios:"
|
|
32
32
|
|
|
33
33
|
return TerminalMenu(
|
|
34
|
-
[
|
|
34
|
+
[
|
|
35
|
+
f"{s['org']}/{s['teamspace']}/{s['name']}"
|
|
36
|
+
if s.get("org") is not None
|
|
37
|
+
else f"{s['teamspace']}/{s['name']}"
|
|
38
|
+
for s in possible_studios
|
|
39
|
+
],
|
|
40
|
+
title=title,
|
|
41
|
+
clear_menu_on_exit=True,
|
|
35
42
|
)
|
|
36
43
|
|
|
37
44
|
@staticmethod
|
|
@@ -10,6 +10,8 @@ def register_commands(group: click.Group) -> None:
|
|
|
10
10
|
from lightning_sdk.cli.studio.create import create_studio
|
|
11
11
|
from lightning_sdk.cli.studio.delete import delete_studio
|
|
12
12
|
from lightning_sdk.cli.studio.list import list_studios
|
|
13
|
+
from lightning_sdk.cli.studio.ls import ls_studio
|
|
14
|
+
from lightning_sdk.cli.studio.rm import rm_studio_file
|
|
13
15
|
from lightning_sdk.cli.studio.ssh import ssh_studio
|
|
14
16
|
from lightning_sdk.cli.studio.start import start_studio
|
|
15
17
|
from lightning_sdk.cli.studio.stop import stop_studio
|
|
@@ -24,3 +26,5 @@ def register_commands(group: click.Group) -> None:
|
|
|
24
26
|
group.add_command(switch_studio)
|
|
25
27
|
group.add_command(connect_studio)
|
|
26
28
|
group.add_command(cp_studio_file)
|
|
29
|
+
group.add_command(ls_studio)
|
|
30
|
+
group.add_command(rm_studio_file)
|
lightning_sdk/cli/studio/cp.py
CHANGED
|
@@ -2,22 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
5
|
+
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
10
|
from lightning_sdk.api.utils import _get_cloud_url
|
|
11
|
-
from lightning_sdk.cli.utils.
|
|
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
|
|
11
|
+
from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
@click.command("cp")
|
|
18
15
|
@click.argument("source", nargs=1)
|
|
19
16
|
@click.argument("destination", nargs=1)
|
|
20
|
-
|
|
17
|
+
@click.option("-r", "--recursive", is_flag=True, help="Copy directories recursively")
|
|
18
|
+
def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = None, recursive: bool = False) -> None:
|
|
21
19
|
"""Copy a Studio file.
|
|
22
20
|
|
|
23
21
|
SOURCE: Source file to copy from. For Studio files, use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
|
|
@@ -26,70 +24,29 @@ def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = Non
|
|
|
26
24
|
|
|
27
25
|
Example:
|
|
28
26
|
lightning studio cp source.txt lit://<owner>/<my-teamspace>/studios/<my-studio>/destination.txt
|
|
27
|
+
lightning studio cp -r source_folder/ lit://<owner>/<my-teamspace>/studios/<my-studio>/destination_folder/
|
|
29
28
|
|
|
30
29
|
"""
|
|
31
|
-
return cp_impl(source=source, destination=destination)
|
|
30
|
+
return cp_impl(source=source, destination=destination, recursive=recursive)
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
def cp_impl(source: str, destination: str) -> None:
|
|
33
|
+
def cp_impl(source: str, destination: str, recursive: bool = False) -> None:
|
|
35
34
|
if "lit://" in source and "lit://" in destination:
|
|
36
35
|
raise ValueError("Both source and destination cannot be Studio files.")
|
|
37
36
|
elif "lit://" not in source and "lit://" not in destination:
|
|
38
37
|
raise ValueError("Either source or destination must be a Studio file.")
|
|
39
38
|
elif "lit://" in source:
|
|
40
39
|
# Download from Studio to local
|
|
41
|
-
cp_download(studio_path=source, local_path=destination)
|
|
40
|
+
cp_download(studio_path=source, local_path=destination, recursive=recursive)
|
|
42
41
|
else:
|
|
43
42
|
# 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
|
|
43
|
+
cp_upload(local_file_path=source, studio_file_path=destination, recursive=recursive)
|
|
88
44
|
|
|
89
45
|
|
|
90
46
|
def cp_upload(
|
|
91
47
|
local_file_path: str,
|
|
92
48
|
studio_file_path: str,
|
|
49
|
+
recursive: bool = False,
|
|
93
50
|
) -> None:
|
|
94
51
|
console = Console()
|
|
95
52
|
if not Path(local_file_path).exists():
|
|
@@ -103,6 +60,8 @@ def cp_upload(
|
|
|
103
60
|
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
104
61
|
|
|
105
62
|
if Path(local_file_path).is_dir():
|
|
63
|
+
if not recursive:
|
|
64
|
+
raise ValueError(f"'{local_file_path}' is a directory. Use -r flag to copy directories recursively.")
|
|
106
65
|
selected_studio.upload_folder(local_file_path, studio_path_result["destination"])
|
|
107
66
|
else:
|
|
108
67
|
if studio_file_path.endswith(("/", "\\")):
|
|
@@ -126,6 +85,7 @@ def cp_upload(
|
|
|
126
85
|
def cp_download(
|
|
127
86
|
studio_path: str,
|
|
128
87
|
local_path: str,
|
|
88
|
+
recursive: bool = False,
|
|
129
89
|
) -> None:
|
|
130
90
|
console = Console()
|
|
131
91
|
studio_path_result = parse_studio_path(studio_path)
|
|
@@ -146,7 +106,14 @@ def cp_download(
|
|
|
146
106
|
|
|
147
107
|
console.print(f"Downloading from {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
148
108
|
if path_info["type"] == "directory":
|
|
109
|
+
if not recursive:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"'{studio_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
|
|
112
|
+
)
|
|
149
113
|
folder_name = os.path.basename(studio_path_result["destination"].rstrip("/"))
|
|
114
|
+
if folder_name == "" and local_path in ("./", "."):
|
|
115
|
+
# handle root directory case (e.g. lit://lightning-ai/gpt-oss/studios/manual-lime-ylu2/)
|
|
116
|
+
folder_name = selected_studio.name
|
|
150
117
|
target_path = os.path.join(local_path, folder_name)
|
|
151
118
|
|
|
152
119
|
selected_studio.download_folder(studio_path_result["destination"], target_path)
|
|
@@ -161,14 +128,3 @@ def cp_download(
|
|
|
161
128
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
162
129
|
selected_studio.download_file(studio_path_result["destination"], target_path)
|
|
163
130
|
console.print(f"See your file at {target_path}")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
|
|
167
|
-
owner_menu = OwnerMenu()
|
|
168
|
-
resolved_owner = owner_menu(owner=owner)
|
|
169
|
-
|
|
170
|
-
teamspace_menu = TeamspacesMenu(resolved_owner)
|
|
171
|
-
resolved_teamspace = teamspace_menu(teamspace=teamspace)
|
|
172
|
-
|
|
173
|
-
studio_menu = StudiosMenu(resolved_teamspace)
|
|
174
|
-
return studio_menu(studio=studio_name)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Studio ls command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command("ls")
|
|
9
|
+
@click.argument("path", nargs=1)
|
|
10
|
+
def ls_studio(path: str) -> None:
|
|
11
|
+
"""List contents of a directory in Studio.
|
|
12
|
+
|
|
13
|
+
PATH: Studio path in the format
|
|
14
|
+
lit://<owner>/<teamspace>/studios/<studio>/<directory-path>
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
lightning studio ls lit://<owner>/<my-teamspace>/studios/<my-studio>/data
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
return ls_impl(path=path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ls_impl(path: str) -> None:
|
|
24
|
+
if not path.startswith("lit://"):
|
|
25
|
+
raise ValueError("Path must be a Studio path starting with 'lit://'.")
|
|
26
|
+
|
|
27
|
+
studio_path_result = parse_studio_path(path)
|
|
28
|
+
selected_studio = resolve_studio(
|
|
29
|
+
studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
path_info = selected_studio._studio_api.get_path_info(
|
|
33
|
+
selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not path_info["exists"]:
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f"The provided path does not exist in the studio: {studio_path_result['destination']} "
|
|
39
|
+
"Note that empty folders may not be detected as existing."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if path_info["type"] == "file":
|
|
43
|
+
# print the file name if it's a file (bash-like behavior)
|
|
44
|
+
print(studio_path_result["destination"])
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
tree = selected_studio._studio_api.get_tree(
|
|
48
|
+
selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
tree_items = tree.get("tree", [])
|
|
52
|
+
|
|
53
|
+
for item in tree_items:
|
|
54
|
+
name = item.get("path", "")
|
|
55
|
+
if item.get("type") == "tree":
|
|
56
|
+
name += "/"
|
|
57
|
+
print(name)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Studio rm command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from lightning_sdk.cli.utils.studio_filesystem import parse_studio_path, resolve_studio
|
|
7
|
+
from lightning_sdk.studio import Studio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("rm")
|
|
11
|
+
@click.argument("path", nargs=1)
|
|
12
|
+
@click.option("-r", "--recursive", is_flag=True, help="Remove directories recursively")
|
|
13
|
+
@click.option("-f", "--force", is_flag=True, help="Ignore nonexistent files, never prompt")
|
|
14
|
+
def rm_studio_file(path: str, recursive: bool = False, force: bool = False) -> None:
|
|
15
|
+
"""Remove a Studio file or directory.
|
|
16
|
+
|
|
17
|
+
PATH: Studio path to remove. Use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
lightning studio rm lit://<owner>/<my-teamspace>/studios/<my-studio>/file.txt
|
|
21
|
+
lightning studio rm -r lit://<owner>/<my-teamspace>/studios/<my-studio>/folder/
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
return rm_impl(path=path, recursive=recursive, force=force)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rm_impl(path: str, recursive: bool = False, force: bool = False) -> None:
|
|
28
|
+
if "lit://" not in path:
|
|
29
|
+
raise ValueError("Path must be a Studio path starting with 'lit://'.")
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
studio_path_result = parse_studio_path(path)
|
|
33
|
+
|
|
34
|
+
selected_studio = resolve_studio(
|
|
35
|
+
studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# check if file/folder exists
|
|
39
|
+
path_info = selected_studio._studio_api.get_path_info(
|
|
40
|
+
selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not path_info["exists"]:
|
|
44
|
+
if force:
|
|
45
|
+
# silently ignore nonexistent files with -f flag
|
|
46
|
+
return
|
|
47
|
+
raise FileNotFoundError(
|
|
48
|
+
f"The provided path does not exist in the studio: '{studio_path_result['destination']}'. "
|
|
49
|
+
"Note that empty folders may not be detected as existing."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
console.print(f"Removing from {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
53
|
+
|
|
54
|
+
if path_info["type"] == "directory":
|
|
55
|
+
if not recursive:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"'{studio_path_result['destination']}' is a directory. Use -r flag to remove directories recursively."
|
|
58
|
+
)
|
|
59
|
+
rm_folder(selected_studio=selected_studio, path=studio_path_result["destination"], console=console)
|
|
60
|
+
else:
|
|
61
|
+
rm_file(selected_studio=selected_studio, path=studio_path_result["destination"], console=console)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rm_file(selected_studio: Studio, path: str, console: Console) -> None:
|
|
65
|
+
selected_studio._studio_api.remove_file(selected_studio._studio.id, selected_studio._teamspace.id, path)
|
|
66
|
+
console.print(f"Removed file: {path}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rm_folder(selected_studio: Studio, path: str, console: Console) -> None:
|
|
70
|
+
selected_studio._studio_api.remove_folder(selected_studio._studio.id, selected_studio._teamspace.id, path)
|
|
71
|
+
console.print(f"Removed directory: {path}")
|
|
@@ -30,10 +30,11 @@ def _log_command(message: str = "", duration: int = 0, error: Optional[str] = No
|
|
|
30
30
|
body = V1CreateSDKCommandHistoryRequest(
|
|
31
31
|
command=original_command,
|
|
32
32
|
duration=duration,
|
|
33
|
-
message=f"
|
|
33
|
+
message=f"{message}",
|
|
34
34
|
project_id=None,
|
|
35
35
|
severity=V1SDKCommandHistorySeverity.INFO,
|
|
36
36
|
type=V1SDKCommandHistoryType.CLI,
|
|
37
|
+
version=__version__,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
if error:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Optional, TypedDict
|
|
2
|
+
|
|
3
|
+
from lightning_sdk.cli.utils.owner_selection import OwnerMenu
|
|
4
|
+
from lightning_sdk.cli.utils.studio_selection import StudiosMenu
|
|
5
|
+
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
6
|
+
from lightning_sdk.studio import Studio
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StudioPathResult(TypedDict):
|
|
10
|
+
owner: Optional[str]
|
|
11
|
+
teamspace: Optional[str]
|
|
12
|
+
studio: Optional[str]
|
|
13
|
+
destination: Optional[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_studio_path(studio_path: str) -> StudioPathResult:
|
|
17
|
+
path_string = studio_path.removeprefix("lit://")
|
|
18
|
+
if not path_string:
|
|
19
|
+
raise ValueError("Studio path cannot be empty after prefix")
|
|
20
|
+
|
|
21
|
+
result: StudioPathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
|
|
22
|
+
|
|
23
|
+
if "/studios/" in path_string:
|
|
24
|
+
prefix_part, suffix_part = path_string.split("/studios/", 1)
|
|
25
|
+
|
|
26
|
+
# org and teamspace
|
|
27
|
+
if prefix_part:
|
|
28
|
+
org_ts_components = prefix_part.split("/")
|
|
29
|
+
if len(org_ts_components) == 2:
|
|
30
|
+
result["owner"], result["teamspace"] = org_ts_components
|
|
31
|
+
elif len(org_ts_components) == 1:
|
|
32
|
+
result["teamspace"] = org_ts_components[0]
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError(f"Invalid format: '{prefix_part}'")
|
|
35
|
+
|
|
36
|
+
# studio and destination
|
|
37
|
+
path_parts = suffix_part.split("/")
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
# studio and destination
|
|
41
|
+
path_parts = path_string.split("/")
|
|
42
|
+
|
|
43
|
+
if not path_parts:
|
|
44
|
+
raise ValueError("Invalid: Missing studio name.")
|
|
45
|
+
|
|
46
|
+
if len(path_parts) == 1:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"Invalid: Invalid studio path. To refer to the studio root, add a trailing '/' (e.g., 'lit://<owner>/<my-teamspace>/studios/<my-studio>/')"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
result["studio"] = path_parts[0]
|
|
52
|
+
result["destination"] = "/".join(path_parts[1:])
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
|
|
58
|
+
owner_menu = OwnerMenu()
|
|
59
|
+
resolved_owner = owner_menu(owner=owner)
|
|
60
|
+
|
|
61
|
+
teamspace_menu = TeamspacesMenu(resolved_owner)
|
|
62
|
+
resolved_teamspace = teamspace_menu(teamspace=teamspace)
|
|
63
|
+
|
|
64
|
+
studio_menu = StudiosMenu(resolved_teamspace)
|
|
65
|
+
return studio_menu(studio=studio_name)
|
|
@@ -51,6 +51,11 @@ class TeamspacesMenu:
|
|
|
51
51
|
return TerminalMenu(possible_teamspaces, title=title, clear_menu_on_exit=True)
|
|
52
52
|
|
|
53
53
|
def _get_possible_teamspaces(self, user: User) -> Dict[str, str]:
|
|
54
|
+
if self._owner is None:
|
|
55
|
+
from lightning_sdk.cli.utils.owner_selection import OwnerMenu
|
|
56
|
+
|
|
57
|
+
menu = OwnerMenu()
|
|
58
|
+
self._owner = menu()
|
|
54
59
|
user_api = user._user_api
|
|
55
60
|
|
|
56
61
|
memberships = user_api._get_all_teamspace_memberships(
|
lightning_sdk/exceptions.py
CHANGED
lightning_sdk/job/base.py
CHANGED
|
@@ -87,7 +87,7 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
|
87
87
|
cloud_account_auth: bool = False,
|
|
88
88
|
artifacts_local: Optional[str] = None,
|
|
89
89
|
artifacts_remote: Optional[str] = None,
|
|
90
|
-
entrypoint: str =
|
|
90
|
+
entrypoint: Optional[str] = None,
|
|
91
91
|
path_mappings: Optional[Dict[str, str]] = None,
|
|
92
92
|
max_runtime: Optional[int] = None,
|
|
93
93
|
cluster: Optional[str] = None, # deprecated in favor of cloud_account
|
|
@@ -193,7 +193,7 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
|
193
193
|
"Other jobs will automatically persist artifacts to the teamspace distributed filesystem."
|
|
194
194
|
)
|
|
195
195
|
|
|
196
|
-
if entrypoint
|
|
196
|
+
if entrypoint is not None:
|
|
197
197
|
raise ValueError("Specifying the entrypoint has no effect for jobs with Studio envs.")
|
|
198
198
|
|
|
199
199
|
else:
|
|
@@ -201,7 +201,7 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
|
201
201
|
raise RuntimeError(
|
|
202
202
|
"image and studio are mutually exclusive as both define the environment to run the job in"
|
|
203
203
|
)
|
|
204
|
-
if cloud_account is None and in_studio():
|
|
204
|
+
if cloud_account is None and cloud_provider is None and in_studio():
|
|
205
205
|
try:
|
|
206
206
|
with skip_studio_setup():
|
|
207
207
|
resolve_studio = Studio(teamspace=teamspace, user=user, org=org)
|
|
@@ -219,6 +219,20 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
|
219
219
|
f"<CONNECTION_TYPE>:<CONNECTION_NAME>:<PATH_WITHIN_CONNECTION>, got {artifacts_local}"
|
|
220
220
|
)
|
|
221
221
|
|
|
222
|
+
# command specified, so use the default entrypoint of sh -c
|
|
223
|
+
if command is not None and entrypoint is None:
|
|
224
|
+
entrypoint = "sh -c"
|
|
225
|
+
|
|
226
|
+
# entrypoint specifically set to empty string, so set to None here to fall back to the image entrypoint
|
|
227
|
+
elif entrypoint == "": # noqa: SIM114
|
|
228
|
+
entrypoint = None
|
|
229
|
+
|
|
230
|
+
# entrypoint not specified, but also no command specified, so use the image entrypoint
|
|
231
|
+
elif entrypoint is None:
|
|
232
|
+
entrypoint = None
|
|
233
|
+
|
|
234
|
+
# all other cases, the entrypoint has been specifically set, so use it as is
|
|
235
|
+
|
|
222
236
|
inst = cls(name=name, teamspace=teamspace, org=org, user=user, _fetch_job=False)
|
|
223
237
|
return inst._submit(
|
|
224
238
|
machine=machine,
|
|
@@ -255,7 +269,7 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
|
255
269
|
cloud_account_auth: bool = False,
|
|
256
270
|
artifacts_local: Optional[str] = None,
|
|
257
271
|
artifacts_remote: Optional[str] = None,
|
|
258
|
-
entrypoint: str =
|
|
272
|
+
entrypoint: Optional[str] = None,
|
|
259
273
|
path_mappings: Optional[Dict[str, str]] = None,
|
|
260
274
|
max_runtime: Optional[int] = None,
|
|
261
275
|
reuse_snapshot: bool = True,
|
|
@@ -292,7 +306,12 @@ class _BaseJob(ABC, metaclass=TrackCallsABCMeta):
|
|
|
292
306
|
within it.
|
|
293
307
|
Note that the connection needs to be added to the teamspace already in order for it to be found.
|
|
294
308
|
Only supported for jobs with a docker image compute environment.
|
|
295
|
-
entrypoint: The entrypoint of your docker container. Defaults to sh -c
|
|
309
|
+
entrypoint: The entrypoint of your docker container. Defaults to `sh -c` which
|
|
310
|
+
just runs the provided command in a standard shell if a command is provided.
|
|
311
|
+
If no command is provided, it will run the pre-defined entrypoint of the provided image.
|
|
312
|
+
To use the pre-defined entrypoint of the provided image with a specified command,
|
|
313
|
+
set this to an empty string.
|
|
314
|
+
Only applicable when submitting docker jobs.
|
|
296
315
|
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
297
316
|
Only applicable when submitting docker jobs.
|
|
298
317
|
max_runtime: the duration (in seconds) for which to allocate the machine.
|