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.
- lightning_sdk/__init__.py +1 -1
- lightning_sdk/api/base_studio_api.py +73 -0
- lightning_sdk/api/license_api.py +48 -0
- lightning_sdk/api/llm_api.py +50 -8
- lightning_sdk/api/studio_api.py +47 -1
- lightning_sdk/base_studio.py +70 -0
- lightning_sdk/cli/delete.py +6 -8
- lightning_sdk/cli/download.py +25 -0
- lightning_sdk/cli/serve.py +82 -30
- lightning_sdk/cli/teamspace_menu.py +9 -1
- lightning_sdk/cli/upload.py +0 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +11 -0
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +9 -1
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +121 -0
- lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +178 -0
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +243 -2
- lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +525 -0
- lightning_sdk/lightning_cloud/openapi/configuration.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +10 -0
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/endpoints_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/model_id_versions_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/update.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_aws_direct_v1.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +3 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_config.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_template_config.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloudflare_v1.py +66 -66
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_upload.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_cloud_space_environment_template_request.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_api.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_state.py +4 -4
- lightning_sdk/lightning_cloud/openapi/models/v1_endpoint.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_ge_list_deployment_routing_telemetry_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_job_stats_response.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_get_project_balance_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_get_user_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_product_licenses_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_membership.py +17 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_modify_filesystem_volume_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_state.py +111 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_presigned_url.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +409 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_product_license_check_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_project_membership.py +17 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_r2_data_connection.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_secret_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_server_alert_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_trigger_filesystem_upgrade_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_user_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_upload_project_artifact_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_usage_report.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +295 -113
- lightning_sdk/lightning_cloud/rest_client.py +4 -0
- lightning_sdk/llm/llm.py +88 -40
- lightning_sdk/services/__init__.py +1 -1
- lightning_sdk/services/license.py +236 -0
- lightning_sdk/studio.py +30 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/METADATA +1 -1
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/RECORD +83 -68
- /lightning_sdk/services/{finetune/__init__.py → finetune_llm.py} +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.2.14.dist-info → lightning_sdk-0.2.15.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
10
|
-
from lightning_sdk.
|
|
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
|
-
|
|
18
|
-
org: Union[str, "Organization", None] = None,
|
|
18
|
+
teamspace: Optional[str] = None,
|
|
19
19
|
) -> None:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
55
|
+
self._org = _resolve_org(self._model_provider)
|
|
40
56
|
except ApiException:
|
|
41
|
-
self.
|
|
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) ->
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
104
|
-
self
|
|
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
|
-
|
|
153
|
+
stream: bool = False,
|
|
154
|
+
) -> Union[str, Generator[str, None, None]]:
|
|
113
155
|
if conversation and conversation not in self._conversations:
|
|
114
|
-
self.
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|