lightning-sdk 0.1.46__py3-none-any.whl → 0.1.47__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightning_sdk/__init__.py +1 -1
- lightning_sdk/api/job_api.py +25 -5
- lightning_sdk/api/lit_registry_api.py +12 -0
- lightning_sdk/api/mmt_api.py +18 -1
- lightning_sdk/api/teamspace_api.py +19 -1
- lightning_sdk/cli/entrypoint.py +2 -0
- lightning_sdk/cli/list.py +54 -0
- lightning_sdk/cli/teamspace_menu.py +94 -0
- lightning_sdk/job/base.py +66 -2
- lightning_sdk/job/job.py +25 -0
- lightning_sdk/job/v1.py +28 -0
- lightning_sdk/job/v2.py +29 -1
- lightning_sdk/job/work.py +10 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +101 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_container_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_api.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_resource_visibility.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +27 -1
- lightning_sdk/lightning_cloud/utils/data_connection.py +1 -2
- lightning_sdk/lit_registry.py +39 -0
- lightning_sdk/machine.py +4 -0
- lightning_sdk/mmt/base.py +39 -1
- lightning_sdk/mmt/mmt.py +24 -0
- lightning_sdk/mmt/v1.py +25 -1
- lightning_sdk/mmt/v2.py +28 -0
- lightning_sdk/status.py +11 -7
- lightning_sdk/teamspace.py +51 -1
- {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/METADATA +1 -1
- {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/RECORD +38 -33
- {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
lightning_sdk/api/job_api.py
CHANGED
|
@@ -171,6 +171,15 @@ class JobApiV1:
|
|
|
171
171
|
data = urlopen(resp.url).read().decode("utf-8")
|
|
172
172
|
return remove_datetime_prefix(str(data))
|
|
173
173
|
|
|
174
|
+
def get_command(self, job: Externalv1LightningappInstance) -> str:
|
|
175
|
+
env = job.spec.env
|
|
176
|
+
|
|
177
|
+
for e in env:
|
|
178
|
+
if e.name == "COMMAND":
|
|
179
|
+
return e.value
|
|
180
|
+
|
|
181
|
+
raise RuntimeError("Could not extract command from app")
|
|
182
|
+
|
|
174
183
|
|
|
175
184
|
class JobApiV2:
|
|
176
185
|
v2_job_state_pending = "pending"
|
|
@@ -199,6 +208,7 @@ class JobApiV2:
|
|
|
199
208
|
cloud_account_auth: bool,
|
|
200
209
|
artifacts_local: Optional[str],
|
|
201
210
|
artifacts_remote: Optional[str],
|
|
211
|
+
entrypoint: str,
|
|
202
212
|
) -> V1Job:
|
|
203
213
|
env_vars = []
|
|
204
214
|
if env is not None:
|
|
@@ -213,6 +223,7 @@ class JobApiV2:
|
|
|
213
223
|
cloudspace_id=studio_id or "",
|
|
214
224
|
cluster_id=cloud_account or "",
|
|
215
225
|
command=command or "",
|
|
226
|
+
entrypoint=entrypoint,
|
|
216
227
|
env=env_vars,
|
|
217
228
|
image=image or "",
|
|
218
229
|
instance_name=instance_name,
|
|
@@ -276,11 +287,20 @@ class JobApiV2:
|
|
|
276
287
|
data = urlopen(resp.url).read().decode("utf-8")
|
|
277
288
|
return remove_datetime_prefix(str(data))
|
|
278
289
|
|
|
279
|
-
def get_studio_name(self, job: V1Job) -> str:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
def get_studio_name(self, job: V1Job) -> Optional[str]:
|
|
291
|
+
if job.spec.cloudspace_id:
|
|
292
|
+
cs: V1CloudSpace = self._client.cloud_space_service_get_cloud_space(
|
|
293
|
+
project_id=job.project_id, id=job.spec.cloudspace_id
|
|
294
|
+
)
|
|
295
|
+
return cs.name
|
|
296
|
+
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
def get_image_name(self, job: V1Job) -> Optional[str]:
|
|
300
|
+
return job.spec.image or None
|
|
301
|
+
|
|
302
|
+
def get_command(self, job: V1Job) -> str:
|
|
303
|
+
return job.spec.command
|
|
284
304
|
|
|
285
305
|
def _job_state_to_external(self, state: str) -> "Status":
|
|
286
306
|
from lightning_sdk.status import Status
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LitRegistryApi:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._client = LightningClient(max_tries=3)
|
|
9
|
+
|
|
10
|
+
def list_containers(self, project_id: str) -> List:
|
|
11
|
+
project = self._client.lit_registry_service_get_lit_project_registry(project_id)
|
|
12
|
+
return project.repositories
|
lightning_sdk/api/mmt_api.py
CHANGED
|
@@ -16,6 +16,7 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
16
16
|
Externalv1LightningappInstance,
|
|
17
17
|
MultimachinejobsIdBody,
|
|
18
18
|
ProjectIdMultimachinejobsBody,
|
|
19
|
+
V1CloudSpace,
|
|
19
20
|
V1EnvVar,
|
|
20
21
|
V1Job,
|
|
21
22
|
V1JobSpec,
|
|
@@ -86,6 +87,7 @@ class MMTApiV2:
|
|
|
86
87
|
cloud_account_auth: bool,
|
|
87
88
|
artifacts_local: Optional[str],
|
|
88
89
|
artifacts_remote: Optional[str],
|
|
90
|
+
entrypoint: str,
|
|
89
91
|
) -> V1MultiMachineJob:
|
|
90
92
|
env_vars = []
|
|
91
93
|
if env is not None:
|
|
@@ -100,7 +102,7 @@ class MMTApiV2:
|
|
|
100
102
|
cloudspace_id=studio_id or "",
|
|
101
103
|
cluster_id=cloud_account or "",
|
|
102
104
|
command=command or "",
|
|
103
|
-
entrypoint=
|
|
105
|
+
entrypoint=entrypoint,
|
|
104
106
|
env=env_vars,
|
|
105
107
|
image=image or "",
|
|
106
108
|
instance_name=instance_name,
|
|
@@ -179,6 +181,21 @@ class MMTApiV2:
|
|
|
179
181
|
return Status.Failed
|
|
180
182
|
return Status.Pending
|
|
181
183
|
|
|
184
|
+
def get_studio_name(self, job: V1MultiMachineJob) -> Optional[str]:
|
|
185
|
+
if job.spec.cloudspace_id:
|
|
186
|
+
cs: V1CloudSpace = self._client.cloud_space_service_get_cloud_space(
|
|
187
|
+
project_id=job.project_id, id=job.spec.cloudspace_id
|
|
188
|
+
)
|
|
189
|
+
return cs.name
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def get_image_name(self, job: V1MultiMachineJob) -> Optional[str]:
|
|
194
|
+
return job.spec.image or None
|
|
195
|
+
|
|
196
|
+
def get_command(self, job: V1MultiMachineJob) -> str:
|
|
197
|
+
return job.spec.command
|
|
198
|
+
|
|
182
199
|
def _get_job_machine_from_spec(self, spec: V1JobSpec) -> "Machine":
|
|
183
200
|
instance_name = spec.instance_name
|
|
184
201
|
instance_type = spec.instance_type
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Dict, List, Optional
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
4
|
|
|
5
5
|
from tqdm.auto import tqdm
|
|
6
6
|
|
|
7
7
|
from lightning_sdk.api.utils import _download_model_files, _DummyBody, _get_model_version, _ModelFileUploader
|
|
8
8
|
from lightning_sdk.lightning_cloud.login import Auth
|
|
9
9
|
from lightning_sdk.lightning_cloud.openapi import (
|
|
10
|
+
Externalv1LightningappInstance,
|
|
10
11
|
ModelIdVersionsBody,
|
|
11
12
|
ModelsStoreApi,
|
|
12
13
|
ProjectIdAgentsBody,
|
|
@@ -14,7 +15,9 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
14
15
|
V1Assistant,
|
|
15
16
|
V1CloudSpace,
|
|
16
17
|
V1Endpoint,
|
|
18
|
+
V1Job,
|
|
17
19
|
V1ModelVersionArchive,
|
|
20
|
+
V1MultiMachineJob,
|
|
18
21
|
V1Project,
|
|
19
22
|
V1ProjectClusterBinding,
|
|
20
23
|
V1PromptSuggestion,
|
|
@@ -277,3 +280,18 @@ class TeamspaceApi:
|
|
|
277
280
|
download_dir=download_dir,
|
|
278
281
|
progress_bar=progress_bar,
|
|
279
282
|
)
|
|
283
|
+
|
|
284
|
+
def list_jobs(self, teamspace_id: str) -> Tuple[List[Externalv1LightningappInstance], List[V1Job]]:
|
|
285
|
+
apps = self._client.lightningapp_instance_service_list_lightningapp_instances(
|
|
286
|
+
project_id=teamspace_id, source_app="job_run_plugin"
|
|
287
|
+
).lightningapps
|
|
288
|
+
jobs = self._client.jobs_service_list_jobs(project_id=teamspace_id, standalone=True).jobs
|
|
289
|
+
|
|
290
|
+
return apps, jobs
|
|
291
|
+
|
|
292
|
+
def list_mmts(self, teamspace_id: str) -> Tuple[List[Externalv1LightningappInstance], List[V1MultiMachineJob]]:
|
|
293
|
+
apps = self._client.lightningapp_instance_service_list_lightningapp_instances(
|
|
294
|
+
project_id=teamspace_id, source_app="distributed_plugin"
|
|
295
|
+
).lightningapps
|
|
296
|
+
jobs = self._client.jobs_service_list_multi_machine_jobs(project_id=teamspace_id).multi_machine_jobs
|
|
297
|
+
return apps, jobs
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -5,6 +5,7 @@ from lightning_sdk.api.studio_api import _cloud_url
|
|
|
5
5
|
from lightning_sdk.cli.ai_hub import _AIHub
|
|
6
6
|
from lightning_sdk.cli.download import _Downloads
|
|
7
7
|
from lightning_sdk.cli.legacy import _LegacyLightningCLI
|
|
8
|
+
from lightning_sdk.cli.list import _List
|
|
8
9
|
from lightning_sdk.cli.run import _Run
|
|
9
10
|
from lightning_sdk.cli.serve import _Docker, _LitServe
|
|
10
11
|
from lightning_sdk.cli.upload import _Uploads
|
|
@@ -23,6 +24,7 @@ class StudioCLI:
|
|
|
23
24
|
self.run = _Run(legacy_run=_LegacyLightningCLI() if _LIGHTNING_AVAILABLE else None)
|
|
24
25
|
self.serve = _LitServe()
|
|
25
26
|
self.dockerize = _Docker()
|
|
27
|
+
self.list = _List()
|
|
26
28
|
|
|
27
29
|
def login(self) -> None:
|
|
28
30
|
"""Login to Lightning AI Studios."""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
7
|
+
from lightning_sdk.lit_registry import LitRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _List(_TeamspacesMenu):
|
|
11
|
+
"""List resources on the Lightning AI platform."""
|
|
12
|
+
|
|
13
|
+
def jobs(self, teamspace: Optional[str] = None) -> None:
|
|
14
|
+
"""List jobs for a given teamspace.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
teamspace: the teamspace to list jobs from. Should be specified as {owner}/{name}
|
|
18
|
+
If not provided, can be selected in an interactive menu.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
|
|
22
|
+
|
|
23
|
+
print("Available Jobs:\n" + "\n".join([j.name for j in resolved_teamspace.jobs]))
|
|
24
|
+
|
|
25
|
+
def mmts(self, teamspace: Optional[str] = None) -> None:
|
|
26
|
+
"""List multi-machine jobs for a given teamspace.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
teamspace: the teamspace to list jobs from. Should be specified as {owner}/{name}
|
|
30
|
+
If not provided, can be selected in an interactive menu.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
|
|
34
|
+
|
|
35
|
+
print("Available MMTs:\n" + "\n".join([j.name for j in resolved_teamspace.multi_machine_jobs]))
|
|
36
|
+
|
|
37
|
+
def containers(self, teamspace: Optional[str] = None) -> None:
|
|
38
|
+
"""Display the list of available containers.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
teamspace: The teamspace to list containers from. Should be specified as {owner}/{name}
|
|
42
|
+
If not provided, can be selected in an interactive menu.
|
|
43
|
+
"""
|
|
44
|
+
api = LitRegistry()
|
|
45
|
+
resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
|
|
46
|
+
result = api.list_containers(teamspace=resolved_teamspace.name, org=resolved_teamspace.owner.name)
|
|
47
|
+
table = Table(pad_edge=True, box=None)
|
|
48
|
+
table.add_column("REPOSITORY")
|
|
49
|
+
table.add_column("IMAGE ID")
|
|
50
|
+
table.add_column("CREATED")
|
|
51
|
+
for repo in result:
|
|
52
|
+
table.add_row(repo["REPOSITORY"], repo["IMAGE ID"], repo["CREATED"])
|
|
53
|
+
console = Console()
|
|
54
|
+
console.print(table)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from simple_term_menu import TerminalMenu
|
|
4
|
+
|
|
5
|
+
from lightning_sdk.api import OrgApi
|
|
6
|
+
from lightning_sdk.cli.exceptions import StudioCliError
|
|
7
|
+
from lightning_sdk.teamspace import Teamspace
|
|
8
|
+
from lightning_sdk.user import User
|
|
9
|
+
from lightning_sdk.utils.resolve import _get_authed_user
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _TeamspacesMenu:
|
|
13
|
+
def _get_teamspace_from_interactive_menu(self, possible_teamspaces: Dict[str, Dict[str, str]]) -> Dict[str, str]:
|
|
14
|
+
teamspace_ids = sorted(possible_teamspaces.keys())
|
|
15
|
+
terminal_menu = self._prepare_terminal_menu_teamspaces([possible_teamspaces[k] for k in teamspace_ids])
|
|
16
|
+
terminal_menu.show()
|
|
17
|
+
|
|
18
|
+
selected_id = teamspace_ids[terminal_menu.chosen_menu_index]
|
|
19
|
+
return possible_teamspaces[selected_id]
|
|
20
|
+
|
|
21
|
+
def _get_teamspace_from_name(
|
|
22
|
+
self, teamspace: str, possible_teamspaces: Dict[str, Dict[str, str]]
|
|
23
|
+
) -> Dict[str, str]:
|
|
24
|
+
owner, name = teamspace.split("/", maxsplit=1)
|
|
25
|
+
for _, ts in possible_teamspaces.items():
|
|
26
|
+
if ts["name"] == name and (ts["user"] == owner or ts["org"] == owner):
|
|
27
|
+
return ts
|
|
28
|
+
|
|
29
|
+
print("Could not find Teamspace {teamspace}, please select it from the list:")
|
|
30
|
+
return self._get_teamspace_from_interactive_menu(possible_teamspaces)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _prepare_terminal_menu_teamspaces(
|
|
34
|
+
possible_teamspaces: List[Dict[str, str]], title: Optional[str] = None
|
|
35
|
+
) -> TerminalMenu:
|
|
36
|
+
if title is None:
|
|
37
|
+
title = "Please select a Teamspace of the following:"
|
|
38
|
+
|
|
39
|
+
return TerminalMenu(
|
|
40
|
+
[f"{t['user'] or t['org']}/{t['name']}" for t in possible_teamspaces], title=title, clear_menu_on_exit=True
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _get_possible_teamspaces(user: User, is_owner: bool = True) -> Dict[str, Dict[str, str]]:
|
|
45
|
+
org_api = OrgApi()
|
|
46
|
+
user_api = user._user_api
|
|
47
|
+
|
|
48
|
+
user_api._get_organizations_for_authed_user()
|
|
49
|
+
memberships = user_api._get_all_teamspace_memberships(user_id=user.id)
|
|
50
|
+
|
|
51
|
+
teamspaces = {}
|
|
52
|
+
# get all teamspace memberships
|
|
53
|
+
for membership in memberships:
|
|
54
|
+
teamspace_id = membership.project_id
|
|
55
|
+
teamspace_name = membership.name
|
|
56
|
+
|
|
57
|
+
# get organization if necessary
|
|
58
|
+
if membership.owner_type == "organization":
|
|
59
|
+
org_name = org_api._get_org_by_id(membership.owner_id).name
|
|
60
|
+
user_name = None
|
|
61
|
+
else:
|
|
62
|
+
org_name = None
|
|
63
|
+
|
|
64
|
+
# don't do a request if not necessary
|
|
65
|
+
if membership.owner_id == user.id:
|
|
66
|
+
user_name = user.name
|
|
67
|
+
else:
|
|
68
|
+
user_name = user_api._get_user_by_id(membership.owner_id).username
|
|
69
|
+
|
|
70
|
+
teamspaces[teamspace_id] = {"user": user_name, "org": org_name, "name": teamspace_name}
|
|
71
|
+
|
|
72
|
+
return teamspaces
|
|
73
|
+
|
|
74
|
+
def _resolve_teamspace(self, teamspace: Optional[str]) -> Teamspace:
|
|
75
|
+
try:
|
|
76
|
+
user = _get_authed_user()
|
|
77
|
+
|
|
78
|
+
possible_teamspaces = self._get_possible_teamspaces(user)
|
|
79
|
+
if teamspace is None:
|
|
80
|
+
teamspace_dict = self._get_teamspace_from_interactive_menu(possible_teamspaces=possible_teamspaces)
|
|
81
|
+
else:
|
|
82
|
+
teamspace_dict = self._get_teamspace_from_name(
|
|
83
|
+
teamspace=teamspace, possible_teamspaces=possible_teamspaces
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return Teamspace(**teamspace_dict)
|
|
87
|
+
except KeyboardInterrupt:
|
|
88
|
+
raise KeyboardInterrupt from None
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise StudioCliError(
|
|
92
|
+
f"Could not find the given Teamspace {teamspace}. "
|
|
93
|
+
"Please contact Lightning AI directly to resolve this issue."
|
|
94
|
+
) from e
|
lightning_sdk/job/base.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict, Union
|
|
3
3
|
|
|
4
4
|
from lightning_sdk.utils.resolve import _resolve_deprecated_cluster, _resolve_teamspace
|
|
5
5
|
|
|
@@ -12,6 +12,19 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from lightning_sdk.user import User
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class MachineDict(TypedDict):
|
|
16
|
+
name: str
|
|
17
|
+
status: "Status"
|
|
18
|
+
machine: "Machine"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JobDict(MachineDict):
|
|
22
|
+
command: str
|
|
23
|
+
teamspace: str
|
|
24
|
+
studio: Optional[str]
|
|
25
|
+
image: Optional[str]
|
|
26
|
+
|
|
27
|
+
|
|
15
28
|
class _BaseJob(ABC):
|
|
16
29
|
"""Base interface to all job types."""
|
|
17
30
|
|
|
@@ -65,6 +78,7 @@ class _BaseJob(ABC):
|
|
|
65
78
|
cloud_account_auth: bool = False,
|
|
66
79
|
artifacts_local: Optional[str] = None,
|
|
67
80
|
artifacts_remote: Optional[str] = None,
|
|
81
|
+
entrypoint: str = "sh -c",
|
|
68
82
|
cluster: Optional[str] = None, # deprecated in favor of cloud_account
|
|
69
83
|
) -> "_BaseJob":
|
|
70
84
|
"""Run async workloads using a docker image or a compute environment from your studio.
|
|
@@ -98,6 +112,10 @@ class _BaseJob(ABC):
|
|
|
98
112
|
within it.
|
|
99
113
|
Note that the connection needs to be added to the teamspace already in order for it to be found.
|
|
100
114
|
Only supported for jobs with a docker image compute environment.
|
|
115
|
+
entrypoint: The entrypoint of your docker container. Defaults to `sh -c` which
|
|
116
|
+
just runs the provided command in a standard shell.
|
|
117
|
+
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
118
|
+
Only applicable when submitting docker jobs.
|
|
101
119
|
"""
|
|
102
120
|
from lightning_sdk.studio import Studio
|
|
103
121
|
|
|
@@ -145,6 +163,9 @@ class _BaseJob(ABC):
|
|
|
145
163
|
"Other jobs will automatically persist artifacts to the teamspace distributed filesystem."
|
|
146
164
|
)
|
|
147
165
|
|
|
166
|
+
if entrypoint != "sh -c":
|
|
167
|
+
raise ValueError("Specifying the entrypoint has no effect for jobs with Studio envs.")
|
|
168
|
+
|
|
148
169
|
else:
|
|
149
170
|
if studio is not None:
|
|
150
171
|
raise RuntimeError(
|
|
@@ -174,6 +195,7 @@ class _BaseJob(ABC):
|
|
|
174
195
|
cloud_account_auth=cloud_account_auth,
|
|
175
196
|
artifacts_local=artifacts_local,
|
|
176
197
|
artifacts_remote=artifacts_remote,
|
|
198
|
+
entrypoint=entrypoint,
|
|
177
199
|
)
|
|
178
200
|
|
|
179
201
|
@abstractmethod
|
|
@@ -190,6 +212,7 @@ class _BaseJob(ABC):
|
|
|
190
212
|
cloud_account_auth: bool = False,
|
|
191
213
|
artifacts_local: Optional[str] = None,
|
|
192
214
|
artifacts_remote: Optional[str] = None,
|
|
215
|
+
entrypoint: str = "sh -c",
|
|
193
216
|
) -> "_BaseJob":
|
|
194
217
|
"""Submit a new job to the Lightning AI platform.
|
|
195
218
|
|
|
@@ -218,6 +241,9 @@ class _BaseJob(ABC):
|
|
|
218
241
|
within it.
|
|
219
242
|
Note that the connection needs to be added to the teamspace already in order for it to be found.
|
|
220
243
|
Only supported for jobs with a docker image compute environment.
|
|
244
|
+
entrypoint: The entrypoint of your docker container. Defaults to sh -c.
|
|
245
|
+
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
246
|
+
Only applicable when submitting docker jobs.
|
|
221
247
|
"""
|
|
222
248
|
|
|
223
249
|
@abstractmethod
|
|
@@ -278,10 +304,48 @@ class _BaseJob(ABC):
|
|
|
278
304
|
def logs(self) -> str:
|
|
279
305
|
"""The logs of the job."""
|
|
280
306
|
|
|
307
|
+
@property
|
|
308
|
+
@abstractmethod
|
|
309
|
+
def image(self) -> Optional[str]:
|
|
310
|
+
"""The image used to submit the job."""
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
@abstractmethod
|
|
314
|
+
def studio(self) -> Optional["Studio"]:
|
|
315
|
+
"""The studio used to submit the job."""
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
@abstractmethod
|
|
319
|
+
def command(self) -> str:
|
|
320
|
+
"""The command the job is running."""
|
|
321
|
+
|
|
322
|
+
def dict(self) -> JobDict:
|
|
323
|
+
"""Dict representation of this job."""
|
|
324
|
+
studio = self.studio
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"name": self.name,
|
|
328
|
+
"teamspace": f"{self.teamspace.owner.name}/{self.teamspace.name}",
|
|
329
|
+
"studio": studio.name if studio else None,
|
|
330
|
+
"image": self.image,
|
|
331
|
+
"command": self.command,
|
|
332
|
+
"status": self.status,
|
|
333
|
+
"machine": self.machine,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def json(self) -> str:
|
|
337
|
+
"""JSON representation of this job."""
|
|
338
|
+
import json
|
|
339
|
+
|
|
340
|
+
return json.dumps(self.dict(), indent=4, sort_keys=True, default=str)
|
|
341
|
+
|
|
281
342
|
@property
|
|
282
343
|
def link(self) -> str:
|
|
283
344
|
"""A link to view the current job in the UI."""
|
|
284
|
-
|
|
345
|
+
studio_name = self._job_api.get_studio_name(self._guaranteed_job)
|
|
346
|
+
if not studio_name:
|
|
347
|
+
raise RuntimeError("Cannot extract studio name from job")
|
|
348
|
+
return f"https://lightning.ai/{self.teamspace.owner.name}/{self.teamspace.name}/studios/{studio_name}/app?app_id=jobs&job_name={self.name}"
|
|
285
349
|
|
|
286
350
|
@property
|
|
287
351
|
def _guaranteed_job(self) -> Any:
|
lightning_sdk/job/job.py
CHANGED
|
@@ -78,6 +78,7 @@ class Job(_BaseJob):
|
|
|
78
78
|
cloud_account_auth: bool = False,
|
|
79
79
|
artifacts_local: Optional[str] = None,
|
|
80
80
|
artifacts_remote: Optional[str] = None,
|
|
81
|
+
entrypoint: str = "sh -c",
|
|
81
82
|
cluster: Optional[str] = None, # deprecated in favor of cloud_account
|
|
82
83
|
) -> "Job":
|
|
83
84
|
"""Run async workloads using a docker image or a compute environment from your studio.
|
|
@@ -111,6 +112,10 @@ class Job(_BaseJob):
|
|
|
111
112
|
within it.
|
|
112
113
|
Note that the connection needs to be added to the teamspace already in order for it to be found.
|
|
113
114
|
Only supported for jobs with a docker image compute environment.
|
|
115
|
+
entrypoint: The entrypoint of your docker container. Defaults to `sh -c` which
|
|
116
|
+
just runs the provided command in a standard shell.
|
|
117
|
+
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
118
|
+
Only applicable when submitting docker jobs.
|
|
114
119
|
"""
|
|
115
120
|
ret_val = super().run(
|
|
116
121
|
name=name,
|
|
@@ -128,6 +133,7 @@ class Job(_BaseJob):
|
|
|
128
133
|
cloud_account_auth=cloud_account_auth,
|
|
129
134
|
artifacts_local=artifacts_local,
|
|
130
135
|
artifacts_remote=artifacts_remote,
|
|
136
|
+
entrypoint=entrypoint,
|
|
131
137
|
cluster=cluster,
|
|
132
138
|
)
|
|
133
139
|
# required for typing with "Job"
|
|
@@ -149,6 +155,7 @@ class Job(_BaseJob):
|
|
|
149
155
|
cloud_account_auth: bool = False,
|
|
150
156
|
artifacts_local: Optional[str] = None,
|
|
151
157
|
artifacts_remote: Optional[str] = None,
|
|
158
|
+
entrypoint: str = "sh -c",
|
|
152
159
|
) -> "Job":
|
|
153
160
|
"""Submit a new job to the Lightning AI platform.
|
|
154
161
|
|
|
@@ -177,6 +184,9 @@ class Job(_BaseJob):
|
|
|
177
184
|
within it.
|
|
178
185
|
Note that the connection needs to be added to the teamspace already in order for it to be found.
|
|
179
186
|
Only supported for jobs with a docker image compute environment.
|
|
187
|
+
entrypoint: The entrypoint of your docker container. Defaults to sh -c.
|
|
188
|
+
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
189
|
+
Only applicable when submitting docker jobs.
|
|
180
190
|
"""
|
|
181
191
|
self._job = self._internal_job._submit(
|
|
182
192
|
machine=machine,
|
|
@@ -245,6 +255,21 @@ class Job(_BaseJob):
|
|
|
245
255
|
"""The teamspace the job is part of."""
|
|
246
256
|
return self._internal_job._teamspace
|
|
247
257
|
|
|
258
|
+
@property
|
|
259
|
+
def studio(self) -> Optional["Studio"]:
|
|
260
|
+
"""The studio used to submit the job."""
|
|
261
|
+
return self._internal_job.studio
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def image(self) -> Optional[str]:
|
|
265
|
+
"""The image used to submit the job."""
|
|
266
|
+
return self._internal_job.image
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def command(self) -> str:
|
|
270
|
+
"""The command the job is running."""
|
|
271
|
+
return self._internal_job.command
|
|
272
|
+
|
|
248
273
|
@property
|
|
249
274
|
def logs(self) -> str:
|
|
250
275
|
"""The logs of the job."""
|
lightning_sdk/job/v1.py
CHANGED
|
@@ -100,6 +100,7 @@ class _JobV1(_BaseJob):
|
|
|
100
100
|
cloud_account_auth: bool = False,
|
|
101
101
|
artifacts_local: Optional[str] = None,
|
|
102
102
|
artifacts_remote: Optional[str] = None,
|
|
103
|
+
entrypoint: str = "sh -c",
|
|
103
104
|
) -> "_JobV1":
|
|
104
105
|
"""Submit a job to run on a machine.
|
|
105
106
|
|
|
@@ -115,6 +116,10 @@ class _JobV1(_BaseJob):
|
|
|
115
116
|
cloud_account_auth: Whether to use cloud account authentication for the job (not supported).
|
|
116
117
|
artifacts_local: The local path for persisting artifacts (not supported).
|
|
117
118
|
artifacts_remote: The remote path for persisting artifacts (not supported).
|
|
119
|
+
entrypoint: The entrypoint of your docker container (not supported).
|
|
120
|
+
Defaults to `sh -c` which just runs the provided command in a standard shell.
|
|
121
|
+
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
122
|
+
Only applicable when submitting docker jobs.
|
|
118
123
|
|
|
119
124
|
Returns:
|
|
120
125
|
The submitted job.
|
|
@@ -132,6 +137,10 @@ class _JobV1(_BaseJob):
|
|
|
132
137
|
raise ValueError("Environment variables are not supported for submitting jobs")
|
|
133
138
|
if command is None:
|
|
134
139
|
raise ValueError("Command is required for submitting jobs")
|
|
140
|
+
|
|
141
|
+
if entrypoint != "sh -c":
|
|
142
|
+
raise ValueError("Specifying the entrypoint is not yet supported with jobs")
|
|
143
|
+
|
|
135
144
|
# TODO: add support for empty names (will give an empty string)
|
|
136
145
|
_submitted = self._job_api.submit_job(
|
|
137
146
|
name=self._name,
|
|
@@ -215,6 +224,25 @@ class _JobV1(_BaseJob):
|
|
|
215
224
|
"""The logs of the job."""
|
|
216
225
|
return self.work.logs
|
|
217
226
|
|
|
227
|
+
@property
|
|
228
|
+
def image(self) -> Optional[str]:
|
|
229
|
+
"""The image used to submit the job."""
|
|
230
|
+
# jobsv1 don't support images, so return None here
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def studio(self) -> Optional["Studio"]:
|
|
235
|
+
"""The studio used to submit the job."""
|
|
236
|
+
from lightning_sdk.studio import Studio
|
|
237
|
+
|
|
238
|
+
studio_name = self._job_api.get_studio_name(self._guaranteed_job)
|
|
239
|
+
return Studio(studio_name, teamspace=self.teamspace)
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def command(self) -> str:
|
|
243
|
+
"""The command the job is running."""
|
|
244
|
+
return self._job_api.get_command(self._guaranteed_job)
|
|
245
|
+
|
|
218
246
|
# the following and functions are solely to make the Work class function
|
|
219
247
|
@property
|
|
220
248
|
def _id(self) -> str:
|
lightning_sdk/job/v2.py
CHANGED
|
@@ -47,6 +47,7 @@ class _JobV2(_BaseJob):
|
|
|
47
47
|
cloud_account_auth: bool = False,
|
|
48
48
|
artifacts_local: Optional[str] = None,
|
|
49
49
|
artifacts_remote: Optional[str] = None,
|
|
50
|
+
entrypoint: str = "sh -c",
|
|
50
51
|
) -> "_JobV2":
|
|
51
52
|
"""Submit a new job to the Lightning AI platform.
|
|
52
53
|
|
|
@@ -75,6 +76,10 @@ class _JobV2(_BaseJob):
|
|
|
75
76
|
within it.
|
|
76
77
|
Note that the connection needs to be added to the teamspace already in order for it to be found.
|
|
77
78
|
Only supported for jobs with a docker image compute environment.
|
|
79
|
+
entrypoint: The entrypoint of your docker container. Defaults to `sh -c` which
|
|
80
|
+
just runs the provided command in a standard shell.
|
|
81
|
+
To use the pre-defined entrypoint of the provided image, set this to an empty string.
|
|
82
|
+
Only applicable when submitting docker jobs.
|
|
78
83
|
"""
|
|
79
84
|
# Command is required if Studio is provided to know what to run
|
|
80
85
|
# Image is mutually exclusive with Studio
|
|
@@ -107,6 +112,7 @@ class _JobV2(_BaseJob):
|
|
|
107
112
|
cloud_account_auth=cloud_account_auth,
|
|
108
113
|
artifacts_local=artifacts_local,
|
|
109
114
|
artifacts_remote=artifacts_remote,
|
|
115
|
+
entrypoint=entrypoint,
|
|
110
116
|
)
|
|
111
117
|
self._job = submitted
|
|
112
118
|
self._name = submitted.name
|
|
@@ -178,13 +184,35 @@ class _JobV2(_BaseJob):
|
|
|
178
184
|
|
|
179
185
|
@property
|
|
180
186
|
def link(self) -> str:
|
|
181
|
-
if self.
|
|
187
|
+
if self._job_api.get_image_name(self._guaranteed_job):
|
|
182
188
|
return (
|
|
183
189
|
f"https://lightning.ai/{self.teamspace.owner.name}/{self.teamspace.name}/jobs/{self.name}?app_id=jobs"
|
|
184
190
|
)
|
|
185
191
|
|
|
186
192
|
return super().link
|
|
187
193
|
|
|
194
|
+
@property
|
|
195
|
+
def image(self) -> Optional[str]:
|
|
196
|
+
"""The image used to submit the job."""
|
|
197
|
+
return self._job_api.get_image_name(self._guaranteed_job)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def studio(self) -> Optional["Studio"]:
|
|
201
|
+
"""The studio used to submit the job."""
|
|
202
|
+
from lightning_sdk.studio import Studio
|
|
203
|
+
|
|
204
|
+
studio_name = self._job_api.get_studio_name(self._guaranteed_job)
|
|
205
|
+
|
|
206
|
+
# if job was submitted with image, studio will be None
|
|
207
|
+
if not studio_name:
|
|
208
|
+
return None
|
|
209
|
+
return Studio(studio_name, teamspace=self.teamspace)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def command(self) -> str:
|
|
213
|
+
"""The command the job is running."""
|
|
214
|
+
return self._job_api.get_command(self._guaranteed_job)
|
|
215
|
+
|
|
188
216
|
def _update_internal_job(self) -> None:
|
|
189
217
|
if getattr(self, "_job", None) is None:
|
|
190
218
|
self._job = self._job_api.get_job_by_name(name=self._name, teamspace_id=self._teamspace.id)
|
lightning_sdk/job/work.py
CHANGED
|
@@ -3,9 +3,10 @@ from typing import TYPE_CHECKING, Any, Optional, Protocol
|
|
|
3
3
|
from lightning_sdk.api.job_api import JobApiV1
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
+
from lightning_sdk.job.base import MachineDict
|
|
7
|
+
from lightning_sdk.machine import Machine
|
|
6
8
|
from lightning_sdk.status import Status
|
|
7
9
|
from lightning_sdk.teamspace import Teamspace
|
|
8
|
-
from lightning_sdk.machine import Machine
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class _WorkHolder(Protocol):
|
|
@@ -70,3 +71,11 @@ class Work:
|
|
|
70
71
|
raise RuntimeError("Getting jobs logs while the job is pending or running is not supported yet!")
|
|
71
72
|
|
|
72
73
|
return self._job_api.get_logs_finished(job_id=self._job._id, work_id=self._id, teamspace_id=self._teamspace.id)
|
|
74
|
+
|
|
75
|
+
def dict(self) -> "MachineDict":
|
|
76
|
+
"""Dict representation of the work."""
|
|
77
|
+
return {
|
|
78
|
+
"name": self.name,
|
|
79
|
+
"status": self.status,
|
|
80
|
+
"machine": self.machine,
|
|
81
|
+
}
|