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
lightning_sdk/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from lightning_sdk.agents import Agent
2
2
  from lightning_sdk.ai_hub import AIHub
3
3
  from lightning_sdk.constants import __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__ # noqa: F401
4
- from lightning_sdk.helpers import _check_version_and_prompt_upgrade
4
+ from lightning_sdk.helpers import _check_version_and_prompt_upgrade, _set_tqdm_envvars_noninteractive
5
5
  from lightning_sdk.job import Job
6
6
  from lightning_sdk.machine import Machine
7
7
  from lightning_sdk.mmt import MMT
@@ -29,5 +29,6 @@ __all__ = [
29
29
  "AIHub",
30
30
  ]
31
31
 
32
- __version__ = "0.1.55"
32
+ __version__ = "0.1.57"
33
33
  _check_version_and_prompt_upgrade(__version__)
34
+ _set_tqdm_envvars_noninteractive()
lightning_sdk/ai_hub.py CHANGED
@@ -8,6 +8,7 @@ from lightning_sdk.utils.resolve import _resolve_teamspace
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from lightning_sdk import Organization, Teamspace
11
+ from lightning_sdk.machine import Machine
11
12
 
12
13
 
13
14
  class AIHub:
@@ -107,6 +108,7 @@ class AIHub:
107
108
  teamspace: Optional[Union[str, "Teamspace"]] = None,
108
109
  org: Optional[Union[str, "Organization"]] = None,
109
110
  user: Optional[Union[str, "User"]] = None,
111
+ machine: Optional[Union[str, "Machine"]] = None,
110
112
  ) -> Dict[str, Union[str, bool]]:
111
113
  """Deploy an API from the AI Hub.
112
114
 
@@ -128,6 +130,7 @@ class AIHub:
128
130
  teamspace: The team or group for deployment. Defaults to None.
129
131
  org: The organization for deployment. Don't pass user with this. Defaults to None.
130
132
  user: The user for deployment. Don't pass org with this. Defaults to None.
133
+ machine: The machine to run the deployment on. Defaults to the first option set in the AI Hub template.
131
134
 
132
135
  Returns:
133
136
  A dictionary containing the name of the deployed API,
@@ -153,6 +156,7 @@ class AIHub:
153
156
  project_id=teamspace_id,
154
157
  name=name,
155
158
  api_arguments=api_arguments,
159
+ machine=machine,
156
160
  )
157
161
 
158
162
  url = (
@@ -171,4 +175,22 @@ class AIHub:
171
175
  "deployment_url": url,
172
176
  "api_endpoint": deployment.status.urls[0],
173
177
  "interruptible": deployment.spec.spot,
178
+ "teamspace id": teamspace_id,
174
179
  }
180
+
181
+ def delete_deployment(self, deployment: Dict[str, Union[str, bool]]) -> None:
182
+ """Delete a deployment from the AI Hub.
183
+
184
+ Example:
185
+ from lightning_sdk import AIHub
186
+ hub = AIHub()
187
+ deployment = hub.run("temp_xxxx")
188
+ hub.delete_deployment(deployment)
189
+
190
+ Args:
191
+ deployment: The deployment dictionary returned by the run method.
192
+ """
193
+ if "teamspace id" not in deployment or "id" not in deployment:
194
+ raise ValueError("Deployment dictionary must contain 'teamspace id' and 'id' keys.")
195
+
196
+ self._api.delete_api(deployment["id"], deployment["teamspace id"])
@@ -1,8 +1,10 @@
1
1
  import traceback
2
- from typing import Dict, List, Optional, Tuple
2
+ from typing import Dict, List, Optional, Tuple, Union
3
3
 
4
4
  import backoff
5
5
 
6
+ from lightning_sdk.api.deployment_api import apply_change
7
+ from lightning_sdk.api.utils import _machine_to_compute_name
6
8
  from lightning_sdk.lightning_cloud.openapi.models import (
7
9
  CreateDeploymentRequestDefinesASpecForTheJobThatAllowsForAutoscalingJobs,
8
10
  V1Deployment,
@@ -16,6 +18,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_deployment_template_gallery
16
18
  V1DeploymentTemplateGalleryResponse,
17
19
  )
18
20
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
21
+ from lightning_sdk.machine import Machine
19
22
 
20
23
 
21
24
  class AIHubApi:
@@ -110,7 +113,13 @@ class AIHubApi:
110
113
  return job
111
114
 
112
115
  def run_api(
113
- self, template_id: str, project_id: str, cloud_account: str, name: Optional[str], api_arguments: Dict[str, str]
116
+ self,
117
+ template_id: str,
118
+ project_id: str,
119
+ cloud_account: str,
120
+ name: Optional[str],
121
+ api_arguments: Dict[str, str],
122
+ machine: Optional[Union[str, Machine]],
114
123
  ) -> V1Deployment:
115
124
  template = self._client.deployment_templates_service_get_deployment_template(template_id)
116
125
  name = name or template.name
@@ -123,6 +132,13 @@ class AIHubApi:
123
132
  template.spec_v2.autoscaling.enabled = True
124
133
 
125
134
  AIHubApi._set_parameters(template.spec_v2.job, template.parameter_spec.parameters, api_arguments)
135
+ if machine and isinstance(machine, Machine):
136
+ apply_change(template.spec_v2.job, "instance_name", _machine_to_compute_name(machine))
137
+ apply_change(template.spec_v2.job, "instance_type", _machine_to_compute_name(machine))
138
+ elif machine and isinstance(machine, str):
139
+ apply_change(template.spec_v2.job, "instance_name", machine)
140
+ apply_change(template.spec_v2.job, "instance_type", machine)
141
+
126
142
  return self._client.jobs_service_create_deployment(
127
143
  project_id=project_id,
128
144
  body=CreateDeploymentRequestDefinesASpecForTheJobThatAllowsForAutoscalingJobs(
@@ -134,3 +150,6 @@ class AIHubApi:
134
150
  spec=template.spec_v2.job,
135
151
  ),
136
152
  )
153
+
154
+ def delete_api(self, deployment_id: str, teamspace_id: str) -> None:
155
+ self._client.jobs_service_delete_deployment(project_id=teamspace_id, id=deployment_id)
@@ -1,7 +1,7 @@
1
1
  from time import sleep
2
2
  from typing import Any, List, Literal, Optional, Union
3
3
 
4
- from lightning_sdk.api.utils import _MACHINE_TO_COMPUTE_NAME
4
+ from lightning_sdk.api.utils import _machine_to_compute_name
5
5
  from lightning_sdk.lightning_cloud.openapi import (
6
6
  CreateDeploymentRequestDefinesASpecForTheJobThatAllowsForAutoscalingJobs,
7
7
  V1AutoscalingSpec,
@@ -270,7 +270,8 @@ class DeploymentApi:
270
270
 
271
271
  # Any updates to the Job Spec triggers a new release
272
272
  if machine:
273
- apply_change(deployment.spec, "instance_name", _MACHINE_TO_COMPUTE_NAME[machine])
273
+ apply_change(deployment.spec, "instance_name", _machine_to_compute_name(machine))
274
+ apply_change(deployment.spec, "instance_type", _machine_to_compute_name(machine))
274
275
 
275
276
  requires_release = False
276
277
  requires_release |= apply_change(deployment.spec, "image", environment)
@@ -554,7 +555,7 @@ def to_spec(
554
555
  env=to_env(env),
555
556
  image=environment,
556
557
  spot=spot,
557
- instance_name=_MACHINE_TO_COMPUTE_NAME[machine],
558
+ instance_name=_machine_to_compute_name(machine),
558
559
  readiness_probe=to_health_check(health_check),
559
560
  )
560
561
 
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Union
3
3
  from urllib.request import urlopen
4
4
 
5
5
  from lightning_sdk.api.utils import (
6
- _COMPUTE_NAME_TO_MACHINE,
7
6
  _create_app,
8
7
  _machine_to_compute_name,
9
8
  remove_datetime_prefix,
@@ -98,13 +97,11 @@ class JobApiV1:
98
97
  def get_machine_from_work(self, work: Externalv1Lightningwork) -> Machine:
99
98
  spec: V1LightningworkSpec = work.spec
100
99
  # prefer user-requested config if specified
101
- compute_config: V1UserRequestedComputeConfig = spec.user_requested_compute_config
102
- compute: str = compute_config.name
103
- if compute:
104
- return _COMPUTE_NAME_TO_MACHINE[compute]
100
+ user_requested_compute_config: V1UserRequestedComputeConfig = spec.user_requested_compute_config
101
+ if user_requested_compute_config.name:
102
+ return Machine(user_requested_compute_config.name, user_requested_compute_config.name)
105
103
  compute_config: V1ComputeConfig = spec.compute_config
106
- compute: str = compute_config.instance_type
107
- return _COMPUTE_NAME_TO_MACHINE[compute]
104
+ return Machine(compute_config.instance_type, compute_config.instance_type)
108
105
 
109
106
  def get_studio_name(self, job: Externalv1LightningappInstance) -> str:
110
107
  cs: V1CloudSpace = self._client.cloud_space_service_get_cloud_space(
@@ -343,9 +340,7 @@ class JobApiV2:
343
340
  instance_name = spec.instance_name
344
341
  instance_type = spec.instance_type
345
342
 
346
- return _COMPUTE_NAME_TO_MACHINE.get(
347
- instance_type, _COMPUTE_NAME_TO_MACHINE.get(instance_name, instance_type or instance_name)
348
- )
343
+ return Machine(instance_name, instance_type or instance_name)
349
344
 
350
345
  def get_total_cost(self, job: V1Job) -> float:
351
346
  return job.total_cost
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Union
4
4
 
5
5
  from lightning_sdk.api.job_api import JobApiV1
6
6
  from lightning_sdk.api.utils import (
7
- _COMPUTE_NAME_TO_MACHINE,
8
7
  _create_app,
9
8
  _machine_to_compute_name,
10
9
  resolve_path_mappings,
@@ -205,9 +204,7 @@ class MMTApiV2:
205
204
  instance_name = spec.instance_name
206
205
  instance_type = spec.instance_type
207
206
 
208
- return _COMPUTE_NAME_TO_MACHINE.get(
209
- instance_type, _COMPUTE_NAME_TO_MACHINE.get(instance_name, instance_type or instance_name)
210
- )
207
+ return Machine(instance_name, instance_type or instance_name)
211
208
 
212
209
  def get_total_cost(self, job: V1MultiMachineJob) -> float:
213
210
  return job.total_cost
@@ -12,8 +12,6 @@ import requests
12
12
  from tqdm import tqdm
13
13
 
14
14
  from lightning_sdk.api.utils import (
15
- _COMPUTE_NAME_TO_MACHINE,
16
- _MACHINE_TO_COMPUTE_NAME,
17
15
  _create_app,
18
16
  _DummyBody,
19
17
  _DummyResponse,
@@ -257,7 +255,7 @@ class StudioApi:
257
255
  response: V1CloudSpaceInstanceConfig = self._client.cloud_space_service_get_cloud_space_instance_config(
258
256
  project_id=teamspace_id, id=studio_id
259
257
  )
260
- return _COMPUTE_NAME_TO_MACHINE[response.compute_config.name]
258
+ return Machine(response.compute_config.name, response.compute_config.name)
261
259
 
262
260
  def get_interruptible(self, studio_id: str, teamspace_id: str) -> bool:
263
261
  """Get whether the Studio is running on a interruptible instance."""
@@ -582,7 +580,7 @@ class StudioApi:
582
580
  plugin_type="job",
583
581
  entrypoint=entrypoint,
584
582
  name=name,
585
- compute=_MACHINE_TO_COMPUTE_NAME[machine],
583
+ compute=_machine_to_compute_name(machine),
586
584
  interruptible=interruptible,
587
585
  )
588
586
 
@@ -600,7 +598,7 @@ class StudioApi:
600
598
  ) -> Externalv1LightningappInstance:
601
599
  """Creates a multi-machine job with given commands."""
602
600
  distributed_args = {
603
- "cloud_compute": _MACHINE_TO_COMPUTE_NAME[machine],
601
+ "cloud_compute": _machine_to_compute_name(machine),
604
602
  "num_instances": num_instances,
605
603
  "strategy": strategy,
606
604
  }
@@ -628,7 +626,7 @@ class StudioApi:
628
626
  ) -> Externalv1LightningappInstance:
629
627
  """Creates a multi-machine job with given commands."""
630
628
  data_prep_args = {
631
- "cloud_compute": _MACHINE_TO_COMPUTE_NAME[machine],
629
+ "cloud_compute": _machine_to_compute_name(machine),
632
630
  "num_instances": num_instances,
633
631
  }
634
632
  return self._create_app(
@@ -665,7 +663,7 @@ class StudioApi:
665
663
  teamspace_id=teamspace_id,
666
664
  cloud_account=cloud_account,
667
665
  plugin_type="inference_plugin",
668
- compute=_MACHINE_TO_COMPUTE_NAME[machine],
666
+ compute=_machine_to_compute_name(machine),
669
667
  entrypoint=entrypoint,
670
668
  name=name,
671
669
  min_replicas=min_replicas,
@@ -14,6 +14,7 @@ from lightning_sdk.lightning_cloud.openapi import (
14
14
  ProjectIdModelsBody,
15
15
  V1Assistant,
16
16
  V1CloudSpace,
17
+ V1ClusterAccelerator,
17
18
  V1Endpoint,
18
19
  V1Job,
19
20
  V1ModelVersionArchive,
@@ -295,3 +296,9 @@ class TeamspaceApi:
295
296
  ).lightningapps
296
297
  jobs = self._client.jobs_service_list_multi_machine_jobs(project_id=teamspace_id).multi_machine_jobs
297
298
  return apps, jobs
299
+
300
+ def list_machines(self, teamspace_id: str, cloud_account: str) -> List[V1ClusterAccelerator]:
301
+ response = self._client.cluster_service_list_project_cluster_accelerators(
302
+ project_id=teamspace_id, id=cloud_account
303
+ )
304
+ return response.accelerator
@@ -322,38 +322,12 @@ class _DummyResponse:
322
322
  self.data = data
323
323
 
324
324
 
325
- # TODO: This should really come from some kind of metadata service
326
- _MACHINE_TO_COMPUTE_NAME: Dict[Machine, str] = {
327
- Machine.CPU_SMALL: "m3.medium",
328
- Machine.CPU: "cpu-4",
329
- Machine.DATA_PREP: "data-large",
330
- Machine.DATA_PREP_MAX: "data-max",
331
- Machine.DATA_PREP_ULTRA: "data-ultra",
332
- Machine.T4: "g4dn.2xlarge",
333
- Machine.T4_X_4: "g4dn.12xlarge",
334
- Machine.L4: "g6.4xlarge",
335
- Machine.L4_X_4: "g6.12xlarge",
336
- Machine.L4_X_8: "g6.48xlarge",
337
- Machine.A10G: "g5.8xlarge",
338
- Machine.A10G_X_4: "g5.12xlarge",
339
- Machine.A10G_X_8: "g5.48xlarge",
340
- Machine.L40S: "g6e.4xlarge",
341
- Machine.L40S_X_4: "g6e.12xlarge",
342
- Machine.L40S_X_8: "g6e.48xlarge",
343
- Machine.A100_X_8: "p4d.24xlarge",
344
- Machine.H100_X_8: "p5.48xlarge",
345
- Machine.H200_X_8: "p5e.48xlarge",
346
- }
347
-
348
-
349
325
  def _machine_to_compute_name(machine: Union[Machine, str]) -> str:
350
326
  if isinstance(machine, Machine):
351
- return _MACHINE_TO_COMPUTE_NAME[machine]
327
+ return machine.instance_type
352
328
  return machine
353
329
 
354
330
 
355
- _COMPUTE_NAME_TO_MACHINE: Dict[str, Machine] = {v: k for k, v in _MACHINE_TO_COMPUTE_NAME.items()}
356
-
357
331
  _DEFAULT_CLOUD_URL = "https://lightning.ai"
358
332
  _DEFAULT_REGISTRY_URL = "litcr.io"
359
333
 
@@ -1,4 +1,6 @@
1
- from typing import List, Optional
1
+ from typing import Optional
2
+
3
+ import click
2
4
 
3
5
  from lightning_sdk.ai_hub import AIHub
4
6
  from lightning_sdk.cli.studios_menu import _StudiosMenu
@@ -7,10 +9,7 @@ from lightning_sdk.cli.studios_menu import _StudiosMenu
7
9
  class _AIHub(_StudiosMenu):
8
10
  """Interact with Lightning Studio - AI Hub."""
9
11
 
10
- def __init__(self) -> None:
11
- self._hub = AIHub()
12
-
13
- def api_info(self, api_id: str) -> dict:
12
+ def api_info(self, api_id: str) -> None:
14
13
  """Get full API template info such as input details.
15
14
 
16
15
  Example:
@@ -19,15 +18,15 @@ class _AIHub(_StudiosMenu):
19
18
  Args:
20
19
  api_id: The ID of the API for which information is requested.
21
20
  """
22
- return self._hub.api_info(api_id)
21
+ return api_info(api_id=api_id)
23
22
 
24
- def list_apis(self, search: Optional[str] = None) -> List[dict]:
23
+ def list_apis(self, search: Optional[str] = None) -> None:
25
24
  """List API templates available in the AI Hub.
26
25
 
27
26
  Args:
28
27
  search: Search for API templates by name.
29
28
  """
30
- return self._hub.list_apis(search=search)
29
+ return list_apis(search=search)
31
30
 
32
31
  def deploy(
33
32
  self,
@@ -36,7 +35,7 @@ class _AIHub(_StudiosMenu):
36
35
  name: Optional[str] = None,
37
36
  teamspace: Optional[str] = None,
38
37
  org: Optional[str] = None,
39
- ) -> dict:
38
+ ) -> None:
40
39
  """Deploy an API template from the AI Hub.
41
40
 
42
41
  Args:
@@ -46,4 +45,56 @@ class _AIHub(_StudiosMenu):
46
45
  teamspace: Teamspace to deploy the API to. Defaults to user's default teamspace.
47
46
  org: Organization to deploy the API to. Defaults to user's default organization.
48
47
  """
49
- return self._hub.run(api_id, cloud_account=cloud_account, name=name, teamspace=teamspace, org=org)
48
+ return deploy(api_id=api_id, cloud_account=cloud_account, name=name, teamspace=teamspace, org=org)
49
+
50
+
51
+ @click.group(name="aihub")
52
+ def aihub() -> None:
53
+ """Interact with Lightning Studio - AI Hub."""
54
+
55
+
56
+ # @aihub.command(name="api-info")
57
+ # @click.argument("api-id")
58
+ def api_info(api_id: str) -> None:
59
+ """Get full API template info such as input details.
60
+
61
+ Example:
62
+ lightning aihub api_info API-ID
63
+
64
+ API-ID: The ID of the API for which information is requested.
65
+ """
66
+ ai_hub = AIHub()
67
+ ai_hub.api_info(api_id)
68
+
69
+
70
+ # @aihub.command(name="list-apis")
71
+ # @click.option("--search", default=None, help="Search for API templates by name.")
72
+ def list_apis(search: Optional[str]) -> None:
73
+ """List API templates available in the AI Hub."""
74
+ ai_hub = AIHub()
75
+ ai_hub.list_apis(search=search)
76
+
77
+
78
+ # @aihub.command(name="deploy")
79
+ # @click.argument("api-id")
80
+ # @click.option(
81
+ # "--cloud-account",
82
+ # default=None,
83
+ # help="Cloud Account to deploy the API to. Defaults to user's default cloud account.",
84
+ # )
85
+ # @click.option("--name", default=None, help="Name of the deployed API. Defaults to the name of the API template.")
86
+ # @click.option(
87
+ # "--teamspace",
88
+ # default=None,
89
+ # help="Teamspace to deploy the API to. Defaults to user's default teamspace.",
90
+ # )
91
+ # @click.option(
92
+ # "--org",
93
+ # default=None,
94
+ # help="Organization to deploy the API to. Defaults to user's default organization.",
95
+ # )
96
+ def deploy(
97
+ api_id: str, cloud_account: Optional[str], name: Optional[str], teamspace: Optional[str], org: Optional[str]
98
+ ) -> None:
99
+ ai_hub = AIHub()
100
+ ai_hub.run(api_id, cloud_account=cloud_account, name=name, teamspace=teamspace, org=org)
@@ -0,0 +1,137 @@
1
+ import os
2
+ import platform
3
+ import uuid
4
+ from pathlib import Path
5
+ from typing import Optional, Union
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ from lightning_sdk.cli.generate import _generate_ssh_config
11
+ from lightning_sdk.cli.studios_menu import _StudiosMenu
12
+ from lightning_sdk.lightning_cloud.login import Auth
13
+
14
+
15
+ class _Configure(_StudiosMenu):
16
+ """Configure lightning products."""
17
+
18
+ def ssh(self, name: Optional[str] = None, teamspace: Optional[str] = None, overwrite: bool = False) -> None:
19
+ """Get SSH config entry for a studio.
20
+
21
+ Args:
22
+ name: The name of the studio to obtain SSH config.
23
+ If not specified, tries to infer from the environment (e.g. when run from within a Studio.)
24
+ teamspace: The teamspace the studio is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.
25
+ If not specified, tries to infer from the environment (e.g. when run from within a Studio.)
26
+ overwrite: Whether to overwrite the SSH key and config if they already exist.
27
+ """
28
+ ssh(name=name, teamspace=teamspace, overwrite=overwrite)
29
+
30
+
31
+ @click.group(name="configure")
32
+ def configure() -> None:
33
+ """Configure access to resources on the Lightning AI platform."""
34
+
35
+
36
+ # @configure.command(name="ssh")
37
+ # @click.option(
38
+ # "--name",
39
+ # default=None,
40
+ # help=(
41
+ # "The name of the studio to obtain SSH config. "
42
+ # "If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
43
+ # ),
44
+ # )
45
+ # @click.option(
46
+ # "--teamspace",
47
+ # default=None,
48
+ # help=(
49
+ # "The teamspace the studio is part of. "
50
+ # "Should be of format <OWNER>/<TEAMSPACE_NAME>. "
51
+ # "If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
52
+ # ),
53
+ # )
54
+ # @click.option(
55
+ # "--overwrite",
56
+ # is_flag=True,
57
+ # flag_value=True,
58
+ # help="Whether to overwrite the SSH key and config if they already exist.",
59
+ # )
60
+ def ssh(name: Optional[str] = None, teamspace: Optional[str] = None, overwrite: bool = False) -> None:
61
+ """Get SSH config entry for a studio."""
62
+ auth = Auth()
63
+ auth.authenticate()
64
+ console = Console()
65
+ ssh_dir = Path.home() / ".ssh"
66
+ ssh_dir.mkdir(parents=True, exist_ok=True)
67
+
68
+ key_path = ssh_dir / "lightning_rsa"
69
+ config_path = ssh_dir / "config"
70
+
71
+ # Check if the SSH key already exists
72
+ if key_path.exists() and (key_path.with_suffix(".pub")).exists() and not overwrite:
73
+ console.print(f"SSH key already exists at {key_path}")
74
+ else:
75
+ _download_ssh_keys(auth.api_key, ssh_home=ssh_dir, ssh_key_name="lightning_rsa", overwrite=overwrite)
76
+ console.print(f"SSH key generated and saved to {key_path}")
77
+
78
+ # Check if the SSH config already contains the required configuration
79
+ menu = _StudiosMenu()
80
+ studio = menu._get_studio(name=name, teamspace=teamspace)
81
+ config_content = _generate_ssh_config(key_path=str(key_path), user=f"s_{studio._studio.id}", host=studio.name)
82
+ if config_path.exists():
83
+ with config_path.open("r") as config_file:
84
+ # check if the host already exists in the config
85
+ if f"Host {studio.name}" in config_file.read():
86
+ console.print("SSH config already contains the required configuration.")
87
+ return
88
+
89
+ with config_path.open("a") as config_file:
90
+ config_file.write(os.linesep)
91
+ config_file.write(config_content)
92
+ config_file.write(os.linesep)
93
+ console.print(f"SSH config updated at {config_path}")
94
+
95
+
96
+ def _download_file(url: str, local_path: Path, overwrite: bool = True, chmod: Optional[int] = None) -> None:
97
+ """Download a file from a URL."""
98
+ import requests
99
+
100
+ if local_path.exists() and not overwrite:
101
+ raise FileExistsError(f"The file {local_path} already exists and overwrite is set to False.")
102
+
103
+ response = requests.get(url, stream=True)
104
+ response.raise_for_status()
105
+
106
+ with open(local_path, "wb") as file:
107
+ for chunk in response.iter_content(chunk_size=8192):
108
+ file.write(chunk)
109
+ if chmod is not None:
110
+ local_path.chmod(0o600)
111
+
112
+
113
+ def _download_ssh_keys(
114
+ api_key: str,
115
+ key_id: str = "",
116
+ ssh_home: Union[str, Path] = "",
117
+ ssh_key_name: str = "lightning_rsa",
118
+ overwrite: bool = False,
119
+ ) -> None:
120
+ if not ssh_home:
121
+ ssh_home = Path.home() / ".ssh"
122
+ elif isinstance(ssh_home, str):
123
+ ssh_home = Path(ssh_home)
124
+ if not key_id:
125
+ key_id = str(uuid.uuid4())
126
+
127
+ path_key = ssh_home / ssh_key_name
128
+ path_pub = ssh_home / f"{ssh_key_name}.pub"
129
+
130
+ # todo: consider hitting the API to get the key pair directly instead of using wget
131
+ _download_file(
132
+ f"https://lightning.ai/setup/ssh-gen?t={api_key}&id={key_id}&machineName={platform.node()}",
133
+ path_key,
134
+ overwrite=overwrite,
135
+ chmod=0o600,
136
+ )
137
+ _download_file(f"https://lightning.ai/setup/ssh-public?t={api_key}&id={key_id}", path_pub, overwrite=overwrite)
@@ -0,0 +1,47 @@
1
+ import subprocess
2
+ import sys
3
+ from typing import Optional
4
+
5
+ import click
6
+
7
+ from lightning_sdk.cli.configure import ssh as configure_ssh
8
+ from lightning_sdk.cli.studios_menu import _StudiosMenu
9
+
10
+
11
+ class _Connect:
12
+ """Connect to lightning products."""
13
+
14
+ def studio(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
15
+ """Connect to a studio via SSH.
16
+
17
+ Args:
18
+ name: The name of the studio to connect to.
19
+ teamspace: The teamspace the studio is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.
20
+ """
21
+ return studio(name=name, teamspace=teamspace)
22
+
23
+
24
+ @click.group(name="connect")
25
+ def connect() -> None:
26
+ """Connect to lightning products."""
27
+
28
+
29
+ # @connect.command(name="studio")
30
+ # @click.option("--name", default=None, help="The name of the studio to connect to.")
31
+ # @click.option(
32
+ # "--teamspace",
33
+ # default=None,
34
+ # help="The teamspace the studio is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.",
35
+ # )
36
+ def studio(name: Optional[str], teamspace: Optional[str]) -> None:
37
+ """Connect to a studio via SSH."""
38
+ configure_ssh(name=name, teamspace=teamspace, overwrite=False)
39
+
40
+ menu = _StudiosMenu()
41
+ studio = menu._get_studio(name=name, teamspace=teamspace)
42
+
43
+ try:
44
+ subprocess.run(["ssh", studio.name])
45
+ except Exception as ex:
46
+ print(f"Failed to establish SSH connection: {ex}")
47
+ sys.exit(1)