lightning-sdk 0.2.14__py3-none-any.whl → 0.2.15__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 (83) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/base_studio_api.py +73 -0
  3. lightning_sdk/api/license_api.py +48 -0
  4. lightning_sdk/api/llm_api.py +50 -8
  5. lightning_sdk/api/studio_api.py +47 -1
  6. lightning_sdk/base_studio.py +70 -0
  7. lightning_sdk/cli/delete.py +6 -8
  8. lightning_sdk/cli/download.py +25 -0
  9. lightning_sdk/cli/serve.py +82 -30
  10. lightning_sdk/cli/teamspace_menu.py +9 -1
  11. lightning_sdk/cli/upload.py +0 -1
  12. lightning_sdk/lightning_cloud/openapi/__init__.py +11 -0
  13. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  14. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +9 -1
  15. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +121 -0
  16. lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +178 -0
  17. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +243 -2
  18. lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +525 -0
  19. lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
  20. lightning_sdk/lightning_cloud/openapi/models/__init__.py +10 -0
  21. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +53 -1
  22. lightning_sdk/lightning_cloud/openapi/models/endpoints_id_body.py +27 -1
  23. lightning_sdk/lightning_cloud/openapi/models/model_id_versions_body.py +27 -1
  24. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +79 -1
  25. lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +6 -6
  26. lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
  27. lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +79 -1
  28. lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
  29. lightning_sdk/lightning_cloud/openapi/models/update.py +79 -1
  30. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +55 -3
  31. lightning_sdk/lightning_cloud/openapi/models/v1_aws_direct_v1.py +53 -1
  32. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +3 -0
  33. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_config.py +123 -0
  35. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +79 -1
  36. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +104 -0
  37. lightning_sdk/lightning_cloud/openapi/models/v1_cloudflare_v1.py +66 -66
  38. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  39. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_upload.py +149 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload.py +55 -3
  41. lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +79 -1
  43. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_api.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +4 -4
  45. lightning_sdk/lightning_cloud/openapi/models/v1_endpoint.py +27 -1
  46. lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
  47. lightning_sdk/lightning_cloud/openapi/models/v1_ge_list_deployment_routing_telemetry_response.py +97 -0
  48. lightning_sdk/lightning_cloud/openapi/models/v1_get_job_stats_response.py +53 -1
  49. lightning_sdk/lightning_cloud/openapi/models/v1_get_project_balance_response.py +1 -27
  50. lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
  51. lightning_sdk/lightning_cloud/openapi/models/v1_job_type.py +1 -0
  52. lightning_sdk/lightning_cloud/openapi/models/v1_list_product_licenses_response.py +123 -0
  53. lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +27 -1
  54. lightning_sdk/lightning_cloud/openapi/models/v1_membership.py +17 -17
  55. lightning_sdk/lightning_cloud/openapi/models/v1_modify_filesystem_volume_response.py +97 -0
  56. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +79 -1
  57. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +6 -6
  58. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_state.py +111 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_presigned_url.py +53 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +409 -0
  61. lightning_sdk/lightning_cloud/openapi/models/v1_product_license_check_response.py +123 -0
  62. lightning_sdk/lightning_cloud/openapi/models/v1_project_membership.py +17 -17
  63. lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +79 -1
  64. lightning_sdk/lightning_cloud/openapi/models/v1_r2_data_connection.py +53 -1
  65. lightning_sdk/lightning_cloud/openapi/models/v1_secret_type.py +1 -0
  66. lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_type.py +1 -0
  67. lightning_sdk/lightning_cloud/openapi/models/v1_trigger_filesystem_upgrade_response.py +123 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
  69. lightning_sdk/lightning_cloud/openapi/models/v1_upload_project_artifact_response.py +27 -1
  70. lightning_sdk/lightning_cloud/openapi/models/v1_usage_report.py +79 -1
  71. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +295 -113
  72. lightning_sdk/lightning_cloud/rest_client.py +4 -0
  73. lightning_sdk/llm/llm.py +88 -40
  74. lightning_sdk/services/__init__.py +1 -1
  75. lightning_sdk/services/license.py +236 -0
  76. lightning_sdk/studio.py +30 -0
  77. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/METADATA +1 -1
  78. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/RECORD +83 -68
  79. /lightning_sdk/services/{finetune/__init__.py → finetune_llm.py} +0 -0
  80. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/LICENSE +0 -0
  81. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/WHEEL +0 -0
  82. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/entry_points.txt +0 -0
  83. {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/top_level.txt +0 -0
@@ -35,6 +35,8 @@ from lightning_sdk.lightning_cloud.openapi import (
35
35
  LitRegistryServiceApi,
36
36
  PipelinesServiceApi,
37
37
  SchedulesServiceApi,
38
+ ProductLicenseServiceApi,
39
+ CloudSpaceEnvironmentTemplateServiceApi
38
40
  )
39
41
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
40
42
  from lightning_sdk.lightning_cloud.source_code.logs_socket_api import LightningLogsSocketAPI
@@ -97,6 +99,8 @@ class GridRestClient(
97
99
  LitRegistryServiceApi,
98
100
  PipelinesServiceApi,
99
101
  SchedulesServiceApi,
102
+ ProductLicenseServiceApi,
103
+ CloudSpaceEnvironmentTemplateServiceApi
100
104
  ):
101
105
 
102
106
  def __init__(self, api_client: Optional[ApiClient] = None):
lightning_sdk/llm/llm.py CHANGED
@@ -1,44 +1,63 @@
1
- from typing import Dict, List, Optional, Set, Tuple, Union
1
+ from typing import Dict, Generator, List, Optional, Set, Tuple, Union
2
2
 
3
- from lightning_sdk.api import UserApi
4
3
  from lightning_sdk.api.llm_api import LLMApi
5
- from lightning_sdk.lightning_cloud.login import Auth
4
+ from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
6
5
  from lightning_sdk.lightning_cloud.openapi import V1Assistant
6
+ from lightning_sdk.lightning_cloud.openapi.models.v1_conversation_response_chunk import V1ConversationResponseChunk
7
7
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
8
8
  from lightning_sdk.organization import Organization
9
- from lightning_sdk.user import User
10
- from lightning_sdk.utils.resolve import _resolve_org, _resolve_user
9
+ from lightning_sdk.owner import Owner
10
+ from lightning_sdk.teamspace import Teamspace
11
+ from lightning_sdk.utils.resolve import _get_authed_user, _resolve_org
11
12
 
12
13
 
13
14
  class LLM:
14
15
  def __init__(
15
16
  self,
16
17
  name: str,
17
- user: Union[str, "User", None] = None,
18
- org: Union[str, "Organization", None] = None,
18
+ teamspace: Optional[str] = None,
19
19
  ) -> None:
20
- self._auth = Auth()
21
- self._user = None
22
-
23
- try:
24
- self._auth.authenticate()
25
- self._user = User(name=UserApi()._get_user_by_id(self._auth.user_id).username)
26
- except ConnectionError as e:
27
- raise e
28
-
29
- self._name = name
30
- try:
31
- self._user = _resolve_user(self._user or user)
32
- except ValueError:
33
- self._user = None
34
-
35
- self._name = name
36
- self._org, self._model_name = self._parse_model_name(name)
20
+ """Initializes the LLM instance with teamspace information, which is required for billing purposes.
21
+
22
+ Teamspace information is resolved through the following methods:
23
+ 1. `.lightning/credentials.json` - Attempts to retrieve the teamspace from the local credentials file.
24
+ 2. Environment Variables - Checks for `LIGHTNING_*` environment variables.
25
+ 3. User Authentication - Redirects the user to the login page if teamspace information is not found.
26
+
27
+ Args:
28
+ name (str): The name of the model or resource.
29
+ teamspace (Optional[str]): The specified teamspace for billing. If not provided, it will be resolved
30
+ through the above methods.
31
+
32
+ Raises:
33
+ ValueError: If teamspace information cannot be resolved.
34
+ """
35
+ menu = _TeamspacesMenu()
36
+ user = _get_authed_user()
37
+ possible_teamspaces = menu._get_possible_teamspaces(user)
38
+ if teamspace is None:
39
+ if len(possible_teamspaces) == 1:
40
+ teamspace_name = next(iter(possible_teamspaces.values()))["name"]
41
+ self._teamspace = Teamspace(name=teamspace_name, org=None, user=user)
42
+ else:
43
+ self._teamspace = menu._resolve_teamspace(teamspace)
44
+ else:
45
+ self._teamspace = Teamspace(**menu._get_teamspace_from_name(teamspace, possible_teamspaces))
46
+
47
+ if self._teamspace is None:
48
+ raise ValueError("Teamspace is required for billing but could not be resolved. ")
49
+
50
+ self._user = user
51
+
52
+ self._model_provider, self._model_name = self._parse_model_name(name)
37
53
  try:
38
54
  # check if it is a org model
39
- self._org = _resolve_org(self._org or org)
55
+ self._org = _resolve_org(self._model_provider)
40
56
  except ApiException:
41
- self._org = None
57
+ if isinstance(self._teamspace.owner, Organization):
58
+ self._org = self._teamspace.owner
59
+ else:
60
+ self._org = None
42
61
 
43
62
  self._llm_api = LLMApi()
44
63
  self._public_models = self._build_model_lookup(self._get_public_models())
@@ -47,6 +66,18 @@ class LLM:
47
66
  self._model = self._get_model()
48
67
  self._conversations = {}
49
68
 
69
+ @property
70
+ def name(self) -> str:
71
+ return self._model_name
72
+
73
+ @property
74
+ def provider(self) -> str:
75
+ return self._model_provider
76
+
77
+ @property
78
+ def owner(self) -> Optional[Owner]:
79
+ return self._teamspace.owner
80
+
50
81
  def _parse_model_name(self, name: str) -> Tuple[str, str]:
51
82
  parts = name.split("/")
52
83
  if len(parts) == 1:
@@ -95,13 +126,23 @@ class LLM:
95
126
  available_models_str = "\n".join(available_models)
96
127
  raise ValueError(f"Model '{self._model_name}' not found. \nAvailable models: \n{available_models_str}")
97
128
 
98
- def _get_conversations(self) -> Dict[str, str]:
99
- # TODO: after updating backend, this will fetch conversations from backend
100
- # conversations = self._llm_api.list_conversations(assistant_id=self._model.id)
101
- return self._conversations
129
+ def _get_conversations(self) -> None:
130
+ conversations = self._llm_api.list_conversations(assistant_id=self._model.id)
131
+ for conversation in conversations:
132
+ if conversation.name and conversation.name not in self._conversations:
133
+ self._conversations[conversation.name] = conversation.id
102
134
 
103
- def _fetch_conversations(self) -> None:
104
- self._conversations = self._get_conversations()
135
+ def _stream_chat_response(
136
+ self, result: Generator[V1ConversationResponseChunk, None, None], conversation: Optional[str] = None
137
+ ) -> Generator[str, None, None]:
138
+ first_line = next(result, None)
139
+ if first_line:
140
+ if conversation and first_line.conversation_id:
141
+ self._conversations[conversation] = first_line.conversation_id
142
+ yield first_line.choices[0].delta.content
143
+
144
+ for line in result:
145
+ yield line.choices[0].delta.content
105
146
 
106
147
  def chat(
107
148
  self,
@@ -109,9 +150,10 @@ class LLM:
109
150
  system_prompt: Optional[str] = None,
110
151
  max_completion_tokens: Optional[int] = 500,
111
152
  conversation: Optional[str] = None,
112
- ) -> str:
153
+ stream: bool = False,
154
+ ) -> Union[str, Generator[str, None, None]]:
113
155
  if conversation and conversation not in self._conversations:
114
- self._fetch_conversations()
156
+ self._get_conversations()
115
157
 
116
158
  conversation_id = self._conversations.get(conversation) if conversation else None
117
159
  output = self._llm_api.start_conversation(
@@ -120,22 +162,26 @@ class LLM:
120
162
  max_completion_tokens=max_completion_tokens,
121
163
  assistant_id=self._model.id,
122
164
  conversation_id=conversation_id,
165
+ billing_project_id=self._teamspace.id,
166
+ name=conversation,
167
+ stream=stream,
123
168
  )
124
- if conversation and not conversation_id:
125
- self._conversations[conversation] = output.conversation_id
126
- return output.choices[0].delta.content
169
+ if not stream:
170
+ if conversation and not conversation_id:
171
+ self._conversations[conversation] = output.conversation_id
172
+ return output.choices[0].delta.content
173
+ return self._stream_chat_response(output, conversation=conversation)
127
174
 
128
175
  def list_conversations(self) -> List[Dict]:
129
- self._fetch_conversations()
176
+ self._get_conversations()
130
177
  return list(self._conversations.keys())
131
178
 
132
179
  def _get_conversation_messages(self, conversation_id: str) -> Optional[str]:
133
180
  return self._llm_api.get_conversation(assistant_id=self._model.id, conversation_id=conversation_id)
134
181
 
135
182
  def get_history(self, conversation: str) -> Optional[List[Dict]]:
136
- # TODO: after updating backend, this will fetch conversation from backend
137
183
  if conversation not in self._conversations:
138
- self._fetch_conversations()
184
+ self._get_conversations()
139
185
 
140
186
  if conversation not in self._conversations:
141
187
  raise ValueError(
@@ -152,6 +198,8 @@ class LLM:
152
198
  return history
153
199
 
154
200
  def reset_conversation(self, conversation: str) -> None:
201
+ if conversation not in self._conversations:
202
+ self._get_conversations()
155
203
  if conversation in self._conversations:
156
204
  self._llm_api.reset_conversation(
157
205
  assistant_id=self._model.id,
@@ -1,5 +1,5 @@
1
1
  from lightning_sdk.services.file_endpoint import Client
2
- from lightning_sdk.services.finetune import LLMFinetune
2
+ from lightning_sdk.services.finetune_llm import LLMFinetune
3
3
  from lightning_sdk.services.utilities import download_file
4
4
 
5
5
  __all__ = ["LLMFinetune", "Client", "download_file"]
@@ -0,0 +1,236 @@
1
+ import importlib
2
+ import json
3
+ import os
4
+ import socket
5
+ import threading
6
+ from functools import partial
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from lightning_sdk.api.license_api import LicenseApi
11
+
12
+
13
+ class LightningLicense:
14
+ """This class is used to manage the license for the Lightning SDK."""
15
+
16
+ _is_valid: Optional[bool] = None
17
+ _license_api: Optional[LicenseApi] = None
18
+ _stream_messages: Optional[callable] = None
19
+
20
+ def __init__(
21
+ self,
22
+ name: str,
23
+ license_key: Optional[str] = None,
24
+ product_version: Optional[str] = None,
25
+ product_type: str = "package",
26
+ stream_messages: callable = print,
27
+ ) -> None:
28
+ self._product_name = name
29
+ self._license_key = license_key
30
+ self._product_version = product_version
31
+ self.product_type = product_type
32
+ self._is_valid = None
33
+ self._license_api = None
34
+ self._stream_messages = stream_messages
35
+
36
+ def validate_license(self) -> bool:
37
+ """Validate the license key."""
38
+ if not self.is_online():
39
+ raise ConnectionError("No internet connection.")
40
+
41
+ self._license_api = LicenseApi()
42
+ return self._license_api.valid_license(
43
+ license_key=self.license_key,
44
+ product_name=self.product_name,
45
+ product_version=self.product_version,
46
+ product_type=self.product_type,
47
+ )
48
+
49
+ @staticmethod
50
+ def is_online(timeout: float = 2.0) -> bool:
51
+ """Check if the system is online by attempting to connect to a public DNS server (Google's).
52
+
53
+ This is a simple way to check for internet connectivity.
54
+
55
+ Args:
56
+ timeout: The timeout for the connection attempt.
57
+ """
58
+ try:
59
+ socket.create_connection(("8.8.8.8", 53), timeout=timeout)
60
+ return True
61
+ except OSError:
62
+ return False
63
+
64
+ @property
65
+ def is_valid(self) -> Optional[bool]:
66
+ """Check if the license key is valid.
67
+
68
+ license validation within package:
69
+ - user online with valid key -> everything as now
70
+ - user online with invalid key -> warning using wrong key + instructions
71
+ - user online with no key -> warning for missing license approval + instructions
72
+ - user offline with a key -> small warning that key could not be verified
73
+ - user offline with no key -> warning for missing license approval + instructions
74
+ """
75
+ if isinstance(self._is_valid, bool):
76
+ # if the license key is already validated, return the cached value
77
+ return self._is_valid
78
+ if not self.product_version:
79
+ self._stream_messages("Product version is not set correctly, consider leave it empty for auto-determine.")
80
+ if not self.license_key:
81
+ self._stream_messages(
82
+ "License key is not set neither cannot be found in the package root or user home."
83
+ " Please make sure you have signed the license agreement and set the license key."
84
+ " For more information, please refer to the documentation.",
85
+ )
86
+ is_online = self.is_online()
87
+ if self.license_key and is_online:
88
+ self._is_valid = self.validate_license()
89
+ elif not is_online:
90
+ self._stream_messages(
91
+ "License key is set but the system is offline. "
92
+ "Please make sure you have a valid license key and the system is online."
93
+ )
94
+ return self._is_valid
95
+
96
+ @property
97
+ def has_required_details(self) -> bool:
98
+ """Check if the license key and product name are set."""
99
+ return bool(self.license_key and self.product_name and self.product_type)
100
+
101
+ @staticmethod
102
+ def _find_package_license_key(package_name: str) -> Optional[str]:
103
+ """Find the license key in the package root as .license_key or in user home as .lightning/licenses.json.
104
+
105
+ Args:
106
+ package_name: The name of the package. If not provided, it will be determined from the current module.
107
+ """
108
+ if not package_name:
109
+ return None
110
+ try:
111
+ pkg_locations = importlib.util.find_spec(package_name).submodule_search_locations
112
+ if not pkg_locations:
113
+ return None
114
+ license_file = os.path.join(pkg_locations[0], ".license_key")
115
+ with open(license_file) as fp:
116
+ return fp.read().strip()
117
+ except (FileNotFoundError, ModuleNotFoundError):
118
+ return None
119
+
120
+ @staticmethod
121
+ def _find_user_license_key(package_name: str) -> Optional[str]:
122
+ """Find the license key in the user home as .lightning/licenses.json.
123
+
124
+ Args:
125
+ package_name: The name of the package.
126
+ """
127
+ home = str(Path.home())
128
+ package_name = package_name.lower()
129
+ license_file = os.path.join(home, ".lightning", "licenses.json")
130
+ try:
131
+ with open(license_file) as fp:
132
+ licenses = json.load(fp)
133
+ # Check for the license key in the licenses.json file
134
+ for name in (package_name, package_name.replace("-", "_"), package_name.replace("_", "-")):
135
+ if name in licenses:
136
+ return licenses[name]
137
+ return None
138
+ except (FileNotFoundError, json.JSONDecodeError):
139
+ return None
140
+
141
+ @staticmethod
142
+ def _determine_package_version(package_name: str) -> Optional[str]:
143
+ """Determine the product version based on the instantiation of the class.
144
+
145
+ Args:
146
+ package_name: The name of the package. If not provided, it will be determined from the current module.
147
+ """
148
+ try:
149
+ pkg = importlib.import_module(package_name)
150
+ return getattr(pkg, "__version__", None)
151
+ except ImportError:
152
+ return None
153
+
154
+ @property
155
+ def license_key(self) -> Optional[str]:
156
+ """Get the license key."""
157
+ if not self._license_key:
158
+ # If the license key is not set, fist try to find it in the package root
159
+ self._license_key = self._find_package_license_key(self.product_name.replace("-", "_"))
160
+ # If not found, try to find it in the user home
161
+ if not self._license_key:
162
+ self._license_key = self._find_user_license_key(self.product_name)
163
+ return self._license_key
164
+
165
+ @property
166
+ def product_name(self) -> str:
167
+ """Get the product name."""
168
+ return self._product_name
169
+
170
+ @property
171
+ def product_version(self) -> Optional[str]:
172
+ """Get the product version."""
173
+ if not self._product_version and self.product_type == "package":
174
+ self._product_version = self._determine_package_version(self.product_name.replace("-", "_"))
175
+ return self._product_version
176
+
177
+
178
+ def check_license(
179
+ name: str,
180
+ license_key: Optional[str] = None,
181
+ product_version: Optional[str] = None,
182
+ product_type: str = "package",
183
+ stream_messages: callable = print,
184
+ ) -> None:
185
+ """Run the license check and stream outputs.
186
+
187
+ Args:
188
+ name: The name of the product.
189
+ license_key: The license key to check.
190
+ product_version: The version of the product.
191
+ product_type: The type of the product.
192
+ stream_messages: A callable to stream messages.
193
+ """
194
+ lit_license = LightningLicense(
195
+ name=name,
196
+ license_key=license_key,
197
+ product_version=product_version,
198
+ product_type=product_type,
199
+ stream_messages=stream_messages,
200
+ )
201
+ if lit_license.is_valid is False:
202
+ stream_messages(
203
+ "License key is not valid.\n"
204
+ f" Key: {lit_license.license_key}\n"
205
+ " Please make sure you have a valid license key."
206
+ )
207
+
208
+
209
+ def check_license_in_background(
210
+ name: str,
211
+ license_key: Optional[str] = None,
212
+ product_version: Optional[str] = None,
213
+ product_type: str = "package",
214
+ stream_messages: callable = print,
215
+ ) -> threading.Thread:
216
+ """Run the license check in a background thread and stream outputs.
217
+
218
+ Args:
219
+ name: The name of the product.
220
+ license_key: The license key to check.
221
+ product_version: The version of the product.
222
+ product_type: The type of the product.
223
+ stream_messages: A callable to stream messages.
224
+ """
225
+ check_license_local = partial(
226
+ check_license,
227
+ name=name,
228
+ license_key=license_key,
229
+ product_version=product_version,
230
+ product_type=product_type,
231
+ stream_messages=stream_messages,
232
+ )
233
+
234
+ thread = threading.Thread(target=check_license_local, daemon=True)
235
+ thread.start()
236
+ return thread
lightning_sdk/studio.py CHANGED
@@ -221,6 +221,36 @@ class Studio:
221
221
  self._studio.id, self._teamspace.id, machine, interruptible=interruptible
222
222
  )
223
223
 
224
+ def run_and_detach(self, *commands: str, timeout: float = 10, check_interval: float = 1) -> str:
225
+ """Runs given commands on the Studio and returns immediately.
226
+
227
+ The command will continue to run in the background.
228
+
229
+ Args:
230
+ timeout: wait for this many seconds for the command to finish.
231
+ check_interval: check the status of the command every this many seconds.
232
+ """
233
+ if check_interval > timeout:
234
+ raise ValueError("check_interval must be less than timeout")
235
+
236
+ if _LIGHTNING_DEBUG:
237
+ print(f"Running {commands=}")
238
+ status = self.status
239
+ if status != Status.Running:
240
+ raise RuntimeError(f"Cannot run a command in a studio that is not running. Studio {self.name} is {status}.")
241
+
242
+ iter_output = self._studio_api.run_studio_commands_and_yield(
243
+ self._studio.id, self._teamspace.id, *commands, timeout=timeout, check_interval=check_interval
244
+ )
245
+
246
+ output = ""
247
+ code = None
248
+ for line, exit_code in iter_output:
249
+ print(line)
250
+ output += line
251
+ code = exit_code
252
+ return output, code
253
+
224
254
  def run_with_exit_code(self, *commands: str) -> Tuple[str, int]:
225
255
  """Runs given commands on the Studio while returning output and exit code.
226
256
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License