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.
Files changed (38) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/job_api.py +25 -5
  3. lightning_sdk/api/lit_registry_api.py +12 -0
  4. lightning_sdk/api/mmt_api.py +18 -1
  5. lightning_sdk/api/teamspace_api.py +19 -1
  6. lightning_sdk/cli/entrypoint.py +2 -0
  7. lightning_sdk/cli/list.py +54 -0
  8. lightning_sdk/cli/teamspace_menu.py +94 -0
  9. lightning_sdk/job/base.py +66 -2
  10. lightning_sdk/job/job.py +25 -0
  11. lightning_sdk/job/v1.py +28 -0
  12. lightning_sdk/job/v2.py +29 -1
  13. lightning_sdk/job/work.py +10 -1
  14. lightning_sdk/lightning_cloud/openapi/__init__.py +1 -0
  15. lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +101 -0
  16. lightning_sdk/lightning_cloud/openapi/models/__init__.py +1 -0
  17. lightning_sdk/lightning_cloud/openapi/models/v1_delete_container_response.py +97 -0
  18. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_api.py +27 -1
  19. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +1 -0
  20. lightning_sdk/lightning_cloud/openapi/models/v1_google_cloud_direct_v1.py +1 -27
  21. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  22. lightning_sdk/lightning_cloud/openapi/models/v1_resource_visibility.py +29 -3
  23. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +27 -1
  24. lightning_sdk/lightning_cloud/utils/data_connection.py +1 -2
  25. lightning_sdk/lit_registry.py +39 -0
  26. lightning_sdk/machine.py +4 -0
  27. lightning_sdk/mmt/base.py +39 -1
  28. lightning_sdk/mmt/mmt.py +24 -0
  29. lightning_sdk/mmt/v1.py +25 -1
  30. lightning_sdk/mmt/v2.py +28 -0
  31. lightning_sdk/status.py +11 -7
  32. lightning_sdk/teamspace.py +51 -1
  33. {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/METADATA +1 -1
  34. {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/RECORD +38 -33
  35. {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/LICENSE +0 -0
  36. {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/WHEEL +0 -0
  37. {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/entry_points.txt +0 -0
  38. {lightning_sdk-0.1.46.dist-info → lightning_sdk-0.1.47.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py CHANGED
@@ -27,5 +27,5 @@ __all__ = [
27
27
  "AIHub",
28
28
  ]
29
29
 
30
- __version__ = "0.1.46"
30
+ __version__ = "0.1.47"
31
31
  _check_version_and_prompt_upgrade(__version__)
@@ -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
- cs: V1CloudSpace = self._client.cloud_space_service_get_cloud_space(
281
- project_id=job.project_id, id=job.spec.cloudspace_id
282
- )
283
- return cs.name
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
@@ -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="sh -c",
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
@@ -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
- return f"https://lightning.ai/{self.teamspace.owner.name}/{self.teamspace.name}/studios/{self._job_api.get_studio_name(self._guaranteed_job)}/app?app_id=jobs&job_name={self.name}"
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._guaranteed_job.spec.image:
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
+ }