lightning-sdk 0.1.55__py3-none-any.whl → 0.1.57__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 (72) hide show
  1. lightning_sdk/__init__.py +3 -2
  2. lightning_sdk/ai_hub.py +22 -0
  3. lightning_sdk/api/ai_hub_api.py +21 -2
  4. lightning_sdk/api/deployment_api.py +4 -3
  5. lightning_sdk/api/job_api.py +5 -10
  6. lightning_sdk/api/mmt_api.py +1 -4
  7. lightning_sdk/api/studio_api.py +5 -7
  8. lightning_sdk/api/teamspace_api.py +7 -0
  9. lightning_sdk/api/utils.py +1 -27
  10. lightning_sdk/cli/ai_hub.py +61 -10
  11. lightning_sdk/cli/configure.py +137 -0
  12. lightning_sdk/cli/connect.py +47 -0
  13. lightning_sdk/cli/delete.py +83 -32
  14. lightning_sdk/cli/download.py +177 -90
  15. lightning_sdk/cli/entrypoint.py +50 -15
  16. lightning_sdk/cli/generate.py +51 -42
  17. lightning_sdk/cli/inspect.py +45 -3
  18. lightning_sdk/cli/jobs_menu.py +2 -1
  19. lightning_sdk/cli/list.py +139 -55
  20. lightning_sdk/cli/mmts_menu.py +2 -1
  21. lightning_sdk/cli/run.py +3 -9
  22. lightning_sdk/cli/serve.py +1 -2
  23. lightning_sdk/cli/start.py +2 -2
  24. lightning_sdk/cli/stop.py +5 -3
  25. lightning_sdk/cli/studios_menu.py +24 -1
  26. lightning_sdk/cli/switch.py +2 -2
  27. lightning_sdk/cli/teamspace_menu.py +2 -1
  28. lightning_sdk/cli/upload.py +6 -4
  29. lightning_sdk/helpers.py +20 -0
  30. lightning_sdk/job/job.py +1 -1
  31. lightning_sdk/lightning_cloud/openapi/__init__.py +9 -0
  32. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
  33. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +105 -0
  34. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +226 -0
  35. lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +4 -4
  36. lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +7 -3
  37. lightning_sdk/lightning_cloud/openapi/api/projects_service_api.py +1 -5
  38. lightning_sdk/lightning_cloud/openapi/models/__init__.py +9 -0
  39. lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +105 -1
  40. lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +29 -3
  41. lightning_sdk/lightning_cloud/openapi/models/id_reportrestarttimings_body.py +123 -0
  42. lightning_sdk/lightning_cloud/openapi/models/id_visibility_body1.py +149 -0
  43. lightning_sdk/lightning_cloud/openapi/models/model_id_visibility_body.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/project_id_litregistry_body.py +2 -0
  45. lightning_sdk/lightning_cloud/openapi/models/setup.py +149 -0
  46. lightning_sdk/lightning_cloud/openapi/models/v1_assistant.py +105 -1
  47. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  48. lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +29 -3
  49. lightning_sdk/lightning_cloud/openapi/models/v1_gcp_data_connection_setup.py +123 -0
  50. lightning_sdk/lightning_cloud/openapi/models/v1_get_cluster_accelerator_demand_response.py +123 -0
  51. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  52. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
  53. lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_artifact.py +27 -1
  54. lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_project.py +8 -0
  55. lightning_sdk/lightning_cloud/openapi/models/v1_lit_repository.py +27 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_report_restart_timings_response.py +97 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_restart_timing.py +175 -0
  58. lightning_sdk/lightning_cloud/openapi/models/v1_setup_data_connection_response.py +123 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_update_deployment_visibility_response.py +97 -0
  60. lightning_sdk/lightning_cloud/openapi/models/v1_update_metrics_stream_visibility_response.py +27 -1
  61. lightning_sdk/lightning_cloud/openapi/models/v1_update_model_visibility_response.py +27 -1
  62. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_validate_deployment_image_request.py +27 -1
  64. lightning_sdk/machine.py +59 -27
  65. lightning_sdk/studio.py +5 -1
  66. lightning_sdk/teamspace.py +25 -0
  67. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/METADATA +3 -1
  68. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/RECORD +72 -61
  69. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/LICENSE +0 -0
  70. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/WHEEL +0 -0
  71. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/entry_points.txt +0 -0
  72. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,12 @@
1
1
  from typing import Optional
2
2
 
3
+ import click
4
+ from rich.console import Console
5
+
3
6
  from lightning_sdk.cli.job_and_mmt_action import _JobAndMMTAction
4
7
 
5
8
 
6
- class _Inspect(_JobAndMMTAction):
9
+ class _Inspect:
7
10
  """Inspect resources of the Lightning AI platform to get additional details as JSON."""
8
11
 
9
12
  def job(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
@@ -16,7 +19,7 @@ class _Inspect(_JobAndMMTAction):
16
19
  If not specified can be selected interactively.
17
20
 
18
21
  """
19
- print(super().job(name=name, teamspace=teamspace).json())
22
+ job(name=name, teamspace=teamspace)
20
23
 
21
24
  def mmt(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
22
25
  """Inspect a multi-machine job for further details as JSON.
@@ -28,4 +31,43 @@ class _Inspect(_JobAndMMTAction):
28
31
  If not specified can be selected interactively.
29
32
 
30
33
  """
31
- print(super().mmt(name=name, teamspace=teamspace).json())
34
+ mmt(name=name, teamspace=teamspace)
35
+
36
+
37
+ @click.group(name="inspect")
38
+ def inspect() -> None:
39
+ """Inspect resources of the Lightning AI platform to get additional details as JSON."""
40
+
41
+
42
+ # @inspect.command(name="job")
43
+ # @click.option("--name", default=None, help="the name of the job. If not specified can be selected interactively.")
44
+ # @click.option(
45
+ # "--teamspace",
46
+ # default=None,
47
+ # help=(
48
+ # "the name of the teamspace the job lives in."
49
+ # "Should be specified as {teamspace_owner}/{teamspace_name} (e.g my-org/my-teamspace). "
50
+ # "If not specified can be selected interactively."
51
+ # ),
52
+ # )
53
+ def job(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
54
+ """Inspect a job for further details as JSON."""
55
+ menu = _JobAndMMTAction()
56
+ Console().print(menu.job(name=name, teamspace=teamspace).json())
57
+
58
+
59
+ # @inspect.command(name="mmt")
60
+ # @click.option("--name", default=None, help="the name of the job. If not specified can be selected interactively.")
61
+ # @click.option(
62
+ # "--teamspace",
63
+ # default=None,
64
+ # help=(
65
+ # "the name of the teamspace the job lives in."
66
+ # "Should be specified as {teamspace_owner}/{teamspace_name} (e.g my-org/my-teamspace). "
67
+ # "If not specified can be selected interactively."
68
+ # ),
69
+ # )
70
+ def mmt(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
71
+ """Inspect a multi-machine job for further details as JSON."""
72
+ menu = _JobAndMMTAction()
73
+ Console().print(menu.mmt(name=name, teamspace=teamspace).json())
@@ -1,5 +1,6 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
+ from rich.console import Console
3
4
  from simple_term_menu import TerminalMenu
4
5
 
5
6
  from lightning_sdk.cli.exceptions import StudioCliError
@@ -20,7 +21,7 @@ class _JobsMenu:
20
21
  if j.name == job:
21
22
  return j
22
23
 
23
- print("Could not find Job {job}, please select it from the list:")
24
+ Console().print("Could not find Job {job}, please select it from the list:")
24
25
  return self._get_job_from_interactive_menu(possible_jobs)
25
26
 
26
27
  @staticmethod
lightning_sdk/cli/list.py CHANGED
@@ -1,27 +1,58 @@
1
- from typing import Optional
1
+ from contextlib import suppress
2
+ from typing import Callable, Optional
2
3
 
3
4
  from rich.console import Console
4
5
  from rich.table import Table
6
+ from typing_extensions import Literal
5
7
 
6
- from lightning_sdk import Machine
8
+ from lightning_sdk import Job, Machine, Studio, Teamspace
7
9
  from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
10
+ from lightning_sdk.lightning_cloud.openapi import V1MultiMachineJob
8
11
  from lightning_sdk.lit_container import LitContainer
12
+ from lightning_sdk.utils.resolve import _get_authed_user
9
13
 
10
14
 
11
15
  class _List(_TeamspacesMenu):
12
16
  """List resources on the Lightning AI platform."""
13
17
 
14
- def studios(self, teamspace: Optional[str] = None) -> None:
18
+ @staticmethod
19
+ def _sort_studios_key(sort_by: str) -> Callable[[Studio], str]:
20
+ """Return a key function to sort studios by a given attribute."""
21
+ sort_key_map = {
22
+ "name": lambda s: str(s.name or ""),
23
+ "teamspace": lambda s: str(s.teamspace.name or ""),
24
+ "status": lambda s: str(s.status or ""),
25
+ "machine": lambda s: str(s.machine or ""),
26
+ "cloud-account": lambda s: str(s.cloud_account or ""),
27
+ }
28
+ return sort_key_map.get(sort_by, lambda s: s.name)
29
+
30
+ def studios(
31
+ self,
32
+ teamspace: Optional[str] = None,
33
+ all: bool = False, # noqa: A002
34
+ sort_by: Optional[Literal["name", "teamspace", "status", "machine", "cloud-account"]] = None,
35
+ ) -> None:
15
36
  """List studios for a given teamspace.
16
37
 
17
38
  Args:
18
39
  teamspace: the teamspace to list studios from. Should be specified as {owner}/{name}
19
40
  If not provided, can be selected in an interactive menu.
41
+ all: if teamspece is not provided, list all studios in all teamspaces.
42
+ sort_by: the attribute to sort the studios by.
43
+ Can be one of "name", "teamspace", "status", "machine", "cloud-account".
20
44
 
21
45
  """
22
- resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
23
-
24
- studios = resolved_teamspace.studios
46
+ studios = []
47
+ if all and not teamspace:
48
+ user = _get_authed_user()
49
+ possible_teamspaces = self._get_possible_teamspaces(user)
50
+ for ts in possible_teamspaces.values():
51
+ teamspace = Teamspace(**ts)
52
+ studios.extend(teamspace.studios)
53
+ else:
54
+ resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
55
+ studios = resolved_teamspace.studios
25
56
 
26
57
  table = Table(
27
58
  pad_edge=True,
@@ -31,33 +62,59 @@ class _List(_TeamspacesMenu):
31
62
  table.add_column("Status")
32
63
  table.add_column("Machine")
33
64
  table.add_column("Cloud account")
34
- for studio in studios:
65
+ for studio in sorted(studios, key=self._sort_studios_key(sort_by)):
35
66
  table.add_row(
36
67
  studio.name,
37
68
  f"{studio.teamspace.owner.name}/{studio.teamspace.name}",
38
69
  str(studio.status),
39
- str(studio.machine) if studio.machine is not None else None,
70
+ str(studio.machine) if studio.machine is not None else None, # when None the cell is empty
40
71
  str(studio.cloud_account),
41
72
  )
42
73
 
43
- console = Console()
44
- console.print(table)
45
-
46
- def jobs(self, teamspace: Optional[str] = None) -> None:
74
+ Console().print(table)
75
+
76
+ @staticmethod
77
+ def _sort_jobs_key(sort_by: str) -> Callable[[Job], str]:
78
+ """Return a key function to sort studios by a given attribute."""
79
+ sort_key_map = {
80
+ "name": lambda j: str(j.name or ""),
81
+ "teamspace": lambda j: str(j.teamspace.name or ""),
82
+ "status": lambda j: str(j.status or ""),
83
+ "machine": lambda j: str(j.machine or ""),
84
+ "studio": lambda j: str(j.studio or ""),
85
+ "image": lambda j: str(j.image or ""),
86
+ "cloud-account": lambda j: str(j.cloud_account or ""),
87
+ }
88
+ return sort_key_map.get(sort_by, lambda j: j.name)
89
+
90
+ def jobs(
91
+ self,
92
+ teamspace: Optional[str] = None,
93
+ all: bool = False, # noqa: A002
94
+ sort_by: Optional[Literal["name", "teamspace", "status", "studio", "machine", "image", "cloud-account"]] = None,
95
+ ) -> None:
47
96
  """List jobs for a given teamspace.
48
97
 
49
98
  Args:
50
99
  teamspace: the teamspace to list jobs from. Should be specified as {owner}/{name}
51
100
  If not provided, can be selected in an interactive menu.
101
+ all: if teamspece is not provided, list all jobs in all teamspaces.
102
+ sort_by: the attribute to sort the jobs by.
103
+ Can be one of "name", "teamspace", "status", "studio", "machine", "image", "cloud-account".
52
104
 
53
105
  """
54
- resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
55
-
56
- jobs = resolved_teamspace.jobs
106
+ jobs = []
107
+ if all and not teamspace:
108
+ user = _get_authed_user()
109
+ possible_teamspaces = self._get_possible_teamspaces(user)
110
+ for ts in possible_teamspaces.values():
111
+ teamspace = Teamspace(**ts)
112
+ jobs.extend(teamspace.jobs)
113
+ else:
114
+ resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
115
+ jobs = resolved_teamspace.jobs
57
116
 
58
- table = Table(
59
- pad_edge=True,
60
- )
117
+ table = Table(pad_edge=True)
61
118
  table.add_column("Name")
62
119
  table.add_column("Teamspace")
63
120
  table.add_column("Studio")
@@ -65,36 +122,64 @@ class _List(_TeamspacesMenu):
65
122
  table.add_column("Status")
66
123
  table.add_column("Machine")
67
124
  table.add_column("Total Cost")
68
- for j in jobs:
125
+ for j in sorted(jobs, key=self._sort_jobs_key(sort_by)):
69
126
  # we know we just fetched these, so no need to refetch
70
127
  j._prevent_refetch_latest = True
71
128
  j._internal_job._prevent_refetch_latest = True
72
129
 
73
130
  studio = j.studio
74
- table.add_row(
75
- j.name,
76
- f"{j.teamspace.owner.name}/{j.teamspace.name}",
77
- studio.name if studio else None,
78
- j.image,
79
- str(j.status),
80
- str(j.machine),
81
- f"{j.total_cost:.3f}",
82
- )
83
-
84
- console = Console()
85
- console.print(table)
86
-
87
- def mmts(self, teamspace: Optional[str] = None) -> None:
131
+ with suppress(RuntimeError):
132
+ table.add_row(
133
+ j.name,
134
+ f"{j.teamspace.owner.name}/{j.teamspace.name}",
135
+ studio.name if studio else None,
136
+ j.image,
137
+ str(j.status) if j.status is not None else None,
138
+ str(j.machine),
139
+ f"{j.total_cost:.3f}",
140
+ )
141
+
142
+ Console().print(table)
143
+
144
+ def _sort_mmts_key(self, sort_by: str) -> Callable[[V1MultiMachineJob], str]:
145
+ """Return a key function to sort multi-machine jobs by a given attribute."""
146
+ sort_key_map = {
147
+ "name": lambda j: str(j.name or ""),
148
+ "teamspace": lambda j: str(j.teamspace.name or ""),
149
+ "studio": lambda j: str(j.studio.name or ""),
150
+ "image": lambda j: str(j.image or ""),
151
+ "status": lambda j: str(j.status or ""),
152
+ "machine": lambda j: str(j.machine or ""),
153
+ "cloud-account": lambda j: str(j.cloud_account or ""),
154
+ }
155
+ return sort_key_map.get(sort_by, lambda j: j.name)
156
+
157
+ def mmts(
158
+ self,
159
+ teamspace: Optional[str] = None,
160
+ all: bool = False, # noqa: A002
161
+ sort_by: Optional[Literal["name", "teamspace", "studio", "image", "status", "machine", "cloud-account"]] = None,
162
+ ) -> None:
88
163
  """List multi-machine jobs for a given teamspace.
89
164
 
90
165
  Args:
91
166
  teamspace: the teamspace to list jobs from. Should be specified as {owner}/{name}
92
167
  If not provided, can be selected in an interactive menu.
168
+ all: if teamspece is not provided, list all multi-machine jobs in all teamspaces.
169
+ sort_by: the attribute to sort the multi-machine jobs by.
170
+ Can be one of "name", "teamspace", "studio", "image", "status", "machine", "cloud-account".
93
171
 
94
172
  """
95
- resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
96
-
97
- jobs = resolved_teamspace.multi_machine_jobs
173
+ jobs = []
174
+ if all and not teamspace:
175
+ user = _get_authed_user()
176
+ possible_teamspaces = self._get_possible_teamspaces(user)
177
+ for ts in possible_teamspaces.values():
178
+ teamspace = Teamspace(**ts)
179
+ jobs.extend(teamspace.multi_machine_jobs)
180
+ else:
181
+ resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
182
+ jobs = resolved_teamspace.multi_machine_jobs
98
183
 
99
184
  table = Table(pad_edge=True)
100
185
  table.add_column("Name")
@@ -105,25 +190,26 @@ class _List(_TeamspacesMenu):
105
190
  table.add_column("Machine")
106
191
  table.add_column("Num Machines")
107
192
  table.add_column("Total Cost")
108
- for j in jobs:
193
+ for j in sorted(jobs, key=self._sort_mmts_key(sort_by)):
109
194
  # we know we just fetched these, so no need to refetch
110
195
  j._prevent_refetch_latest = True
111
- j._internal_job._prevent_refetch_latest = True
196
+ with suppress(AttributeError):
197
+ j._internal_job._prevent_refetch_latest = True
112
198
 
113
199
  studio = j.studio
114
- table.add_row(
115
- j.name,
116
- f"{j.teamspace.owner.name}/{j.teamspace.name}",
117
- studio.name if studio else None,
118
- j.image,
119
- str(j.status),
120
- str(j.machine),
121
- str(j.num_machines),
122
- str(j.total_cost),
123
- )
124
-
125
- console = Console()
126
- console.print(table)
200
+ with suppress(RuntimeError):
201
+ table.add_row(
202
+ j.name,
203
+ f"{j.teamspace.owner.name}/{j.teamspace.name}",
204
+ studio.name if studio else None,
205
+ j.image,
206
+ str(j.status),
207
+ str(j.machine),
208
+ str(j.num_machines),
209
+ str(j.total_cost),
210
+ )
211
+
212
+ Console().print(table)
127
213
 
128
214
  def containers(self, teamspace: Optional[str] = None) -> None:
129
215
  """Display the list of available containers.
@@ -141,8 +227,7 @@ class _List(_TeamspacesMenu):
141
227
  table.add_column("CREATED")
142
228
  for repo in result:
143
229
  table.add_row(repo["REPOSITORY"], repo["IMAGE ID"], repo["CREATED"])
144
- console = Console()
145
- console.print(table)
230
+ Console().print(table)
146
231
 
147
232
  def machines(self) -> None:
148
233
  """Display the list of available machines."""
@@ -156,5 +241,4 @@ class _List(_TeamspacesMenu):
156
241
  for name in sorted(machine_types):
157
242
  table.add_row(name)
158
243
 
159
- console = Console()
160
- console.print(table)
244
+ Console().print(table)
@@ -1,5 +1,6 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
+ from rich.console import Console
3
4
  from simple_term_menu import TerminalMenu
4
5
 
5
6
  from lightning_sdk.cli.exceptions import StudioCliError
@@ -20,7 +21,7 @@ class _MMTsMenu:
20
21
  if j.name == mmt:
21
22
  return j
22
23
 
23
- print("Could not find Multi-Machine Job {mmt}, please select it from the list:")
24
+ Console().print("Could not find Multi-Machine Job {mmt}, please select it from the list:")
24
25
  return self._get_mmt_from_interactive_menu(possible_mmts)
25
26
 
26
27
  @staticmethod
lightning_sdk/cli/run.py CHANGED
@@ -8,7 +8,7 @@ from lightning_sdk.teamspace import Teamspace
8
8
  if TYPE_CHECKING:
9
9
  from lightning_sdk.cli.legacy import _LegacyLightningCLI
10
10
 
11
- _MACHINE_VALUES = tuple([machine.value for machine in Machine])
11
+ _MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
12
12
 
13
13
 
14
14
  class _Run:
@@ -156,16 +156,12 @@ class _Run:
156
156
  machine = "CPU"
157
157
  machine_enum: Union[str, Machine]
158
158
  try:
159
- machine_enum = Machine[machine.upper()]
159
+ machine_enum = getattr(Machine, machine.upper(), Machine(machine, machine))
160
160
  except KeyError:
161
161
  machine_enum = machine
162
162
 
163
163
  resolved_teamspace = Teamspace(name=teamspace, org=org, user=user)
164
164
 
165
- if cloud_account is None:
166
- cloud_account = resolved_teamspace.default_cloud_account
167
- machine_enum = Machine(machine.upper())
168
-
169
165
  path_mappings_dict = self._resolve_path_mapping(path_mappings=path_mappings)
170
166
 
171
167
  Job.run(
@@ -222,13 +218,11 @@ class _Run:
222
218
  machine = "CPU"
223
219
  machine_enum: Union[str, Machine]
224
220
  try:
225
- machine_enum = Machine[machine.upper()]
221
+ machine_enum = getattr(Machine, machine.upper(), Machine(machine, machine))
226
222
  except KeyError:
227
223
  machine_enum = machine
228
224
 
229
225
  resolved_teamspace = Teamspace(name=teamspace, org=org, user=user)
230
- if cloud_account is None:
231
- cloud_account = resolved_teamspace.default_cloud_account
232
226
 
233
227
  if image is None:
234
228
  raise RuntimeError("Image needs to be specified to run a multi-machine job")
@@ -167,7 +167,6 @@ class _Docker:
167
167
  import litserve as ls
168
168
  from litserve import docker_builder
169
169
 
170
- console = Console()
171
170
  requirements = ""
172
171
  if os.path.exists("requirements.txt"):
173
172
  requirements = "-r requirements.txt"
@@ -210,5 +209,5 @@ Update [underline]{os.path.abspath("Dockerfile")}[/underline] to add any additio
210
209
  [bold]To push the container to a registry:[/bold]
211
210
  > [underline]docker push {tag}[/underline]
212
211
  """
213
- console.print(success_msg)
212
+ Console().print(success_msg)
214
213
  return os.path.abspath("Dockerfile")
@@ -7,7 +7,7 @@ class _Start:
7
7
  """Start resources on the Lightning AI platform."""
8
8
 
9
9
  def __init__(self) -> None:
10
- _machine_values = tuple([machine.value for machine in Machine])
10
+ _machine_values = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
11
11
 
12
12
  docstr_studio = f"""Start a studio on a given machine.
13
13
 
@@ -36,7 +36,7 @@ class _Start:
36
36
  studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
37
37
 
38
38
  try:
39
- resolved_machine = Machine[machine.upper()]
39
+ resolved_machine = getattr(Machine, machine.upper(), Machine(machine, machine))
40
40
  except KeyError:
41
41
  resolved_machine = machine
42
42
 
lightning_sdk/cli/stop.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
+ from rich.console import Console
4
+
3
5
  from lightning_sdk.cli.job_and_mmt_action import _JobAndMMTAction
4
6
  from lightning_sdk.studio import Studio
5
7
 
@@ -20,7 +22,7 @@ class _Stop(_JobAndMMTAction):
20
22
  job = super().job(name=name, teamspace=teamspace)
21
23
 
22
24
  job.stop()
23
- print(f"Successfully stopped {job.name}!")
25
+ Console().print(f"Successfully stopped {job.name}!")
24
26
 
25
27
  def mmt(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
26
28
  """Stop a multi-machine job.
@@ -35,7 +37,7 @@ class _Stop(_JobAndMMTAction):
35
37
  mmt = super().mmt(name=name, teamspace=teamspace)
36
38
 
37
39
  mmt.stop()
38
- print(f"Successfully stopped {mmt.name}!")
40
+ Console().print(f"Successfully stopped {mmt.name}!")
39
41
 
40
42
  def studio(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
41
43
  """Stop a running studio.
@@ -60,4 +62,4 @@ class _Stop(_JobAndMMTAction):
60
62
  studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
61
63
 
62
64
  studio.stop()
63
- print("Studio successfully stopped")
65
+ Console().print("Studio successfully stopped")
@@ -1,7 +1,9 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
+ from rich.console import Console
3
4
  from simple_term_menu import TerminalMenu
4
5
 
6
+ from lightning_sdk import Studio
5
7
  from lightning_sdk.api import OrgApi, TeamspaceApi
6
8
  from lightning_sdk.user import User
7
9
 
@@ -18,7 +20,7 @@ class _StudiosMenu:
18
20
  if st["teamspace"] == teamspace and name == st["name"]:
19
21
  return st
20
22
 
21
- print("Could not find Studio {studio}, please select it from the list:")
23
+ Console().print("Could not find Studio {studio}, please select it from the list:")
22
24
  return self._get_studio_from_interactive_menu(possible_studios)
23
25
 
24
26
  @staticmethod
@@ -76,3 +78,24 @@ class _StudiosMenu:
76
78
  possible_studios.append({"name": st.name, **teamspaces[teamspace_id]})
77
79
 
78
80
  return possible_studios
81
+
82
+ def _get_studio(self, name: str, teamspace: str) -> Studio:
83
+ """Get studio object from name and teamspace.
84
+
85
+ Args:
86
+ name: Name of the studio
87
+ teamspace: Name of the teamspace
88
+ """
89
+ if teamspace:
90
+ ts_splits = teamspace.split("/")
91
+ if len(ts_splits) != 2:
92
+ raise ValueError(f"Teamspace should be of format <OWNER>/<TEAMSPACE_NAME> but got {teamspace}")
93
+ owner, teamspace = ts_splits
94
+ else:
95
+ owner, teamspace = None, None
96
+
97
+ try:
98
+ studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False)
99
+ except (RuntimeError, ValueError):
100
+ studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
101
+ return studio
@@ -7,7 +7,7 @@ class _Switch:
7
7
  """Switch machines for resources on the Lightning AI platform."""
8
8
 
9
9
  def __init__(self) -> None:
10
- _machine_values = tuple([machine.value for machine in Machine])
10
+ _machine_values = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
11
11
 
12
12
  docstr_studio = f"""Switch a studio to a given machine.
13
13
 
@@ -36,7 +36,7 @@ class _Switch:
36
36
  studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
37
37
 
38
38
  try:
39
- resolved_machine = Machine[machine.upper()]
39
+ resolved_machine = getattr(Machine, machine.upper(), Machine(machine, machine))
40
40
  except KeyError:
41
41
  resolved_machine = machine
42
42
 
@@ -1,5 +1,6 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
+ from rich.console import Console
3
4
  from simple_term_menu import TerminalMenu
4
5
 
5
6
  from lightning_sdk.api import OrgApi
@@ -26,7 +27,7 @@ class _TeamspacesMenu:
26
27
  if ts["name"] == name and (ts["user"] == owner or ts["org"] == owner):
27
28
  return ts
28
29
 
29
- print("Could not find Teamspace {teamspace}, please select it from the list:")
30
+ Console().print(f"Could not find Teamspace {teamspace}, please select it from the list:")
30
31
  return self._get_teamspace_from_interactive_menu(possible_teamspaces)
31
32
 
32
33
  @staticmethod
@@ -71,6 +71,7 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
71
71
  If not specified, will use the file or directory name of the path you want to upload
72
72
  and place it in your home directory.
73
73
  """
74
+ console = Console()
74
75
  if remote_path is None:
75
76
  remote_path = os.path.basename(path)
76
77
 
@@ -81,7 +82,7 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
81
82
 
82
83
  selected_studio = self._resolve_studio(studio)
83
84
 
84
- print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
85
+ console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
85
86
 
86
87
  pairs = {}
87
88
  for root, _, files in os.walk(path):
@@ -112,7 +113,7 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
112
113
  + "/studios/"
113
114
  + selected_studio.name
114
115
  )
115
- print(f"See your files at {studio_url}")
116
+ console.print(f"See your files at {studio_url}")
116
117
 
117
118
  def file(self, path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
118
119
  """Upload a file to a Studio.
@@ -126,6 +127,7 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
126
127
  If not specified, will use the name of the file you want to upload
127
128
  and place it in your home directory.
128
129
  """
130
+ console = Console()
129
131
  if remote_path is None:
130
132
  remote_path = os.path.basename(path)
131
133
 
@@ -136,7 +138,7 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
136
138
 
137
139
  selected_studio = self._resolve_studio(studio)
138
140
 
139
- print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
141
+ console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
140
142
 
141
143
  self._single_file_upload(selected_studio, path, remote_path, True)
142
144
 
@@ -149,7 +151,7 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
149
151
  + "/studios/"
150
152
  + selected_studio.name
151
153
  )
152
- print(f"See your file at {studio_url}")
154
+ console.print(f"See your file at {studio_url}")
153
155
 
154
156
  def container(self, container: str, tag: str = "latest", teamspace: Optional[str] = None) -> None:
155
157
  teamspace = self._resolve_teamspace(teamspace)
lightning_sdk/helpers.py CHANGED
@@ -1,8 +1,13 @@
1
1
  import functools
2
+ import importlib
3
+ import os
4
+ import sys
2
5
  import warnings
3
6
  from typing import Optional
4
7
 
5
8
  import requests
9
+ import tqdm
10
+ import tqdm.std
6
11
  from packaging import version as packaging_version
7
12
 
8
13
  __package_name__ = "lightning-sdk"
@@ -47,3 +52,18 @@ def _check_version_and_prompt_upgrade(curr_version: str) -> None:
47
52
  UserWarning,
48
53
  )
49
54
  return
55
+
56
+
57
+ def _set_tqdm_envvars_noninteractive() -> None:
58
+ # note: stderr is the default stream tqdm writes progressbars to
59
+ # so we check that one.
60
+ if os.isatty(sys.stderr.fileno()):
61
+ os.unsetenv("TQDM_POSITION")
62
+ os.unsetenv("TQDM_MININTERVAL")
63
+ else:
64
+ # makes use of https://github.com/tqdm/tqdm/blob/master/tqdm/utils.py#L34 to set defaults
65
+ os.environ.update({"TQDM_POSITION": "-1", "TQDM_MININTERVAL": "1"})
66
+
67
+ # reload to make sure env vars are parsed again
68
+ importlib.reload(tqdm.std)
69
+ importlib.reload(tqdm)
lightning_sdk/job/job.py CHANGED
@@ -235,7 +235,7 @@ class Job(_BaseJob):
235
235
  return self._internal_job.delete()
236
236
 
237
237
  @property
238
- def status(self) -> "Status":
238
+ def status(self) -> Optional["Status"]:
239
239
  """The current status of the job."""
240
240
  return self._internal_job.status
241
241