lightning-sdk 2025.10.14__py3-none-any.whl → 2025.10.22__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 (53) hide show
  1. lightning_sdk/__init__.py +6 -3
  2. lightning_sdk/api/base_studio_api.py +13 -9
  3. lightning_sdk/api/license_api.py +26 -59
  4. lightning_sdk/api/studio_api.py +7 -2
  5. lightning_sdk/base_studio.py +30 -17
  6. lightning_sdk/cli/base_studio/list.py +1 -3
  7. lightning_sdk/cli/entrypoint.py +8 -34
  8. lightning_sdk/cli/studio/connect.py +42 -92
  9. lightning_sdk/cli/studio/create.py +23 -1
  10. lightning_sdk/cli/studio/start.py +12 -2
  11. lightning_sdk/cli/utils/get_base_studio.py +24 -0
  12. lightning_sdk/cli/utils/handle_machine_and_gpus_args.py +71 -0
  13. lightning_sdk/cli/utils/logging.py +121 -0
  14. lightning_sdk/cli/utils/ssh_connection.py +1 -1
  15. lightning_sdk/constants.py +1 -0
  16. lightning_sdk/helpers.py +53 -34
  17. lightning_sdk/lightning_cloud/login.py +260 -10
  18. lightning_sdk/lightning_cloud/openapi/__init__.py +10 -3
  19. lightning_sdk/lightning_cloud/openapi/api/auth_service_api.py +97 -0
  20. lightning_sdk/lightning_cloud/openapi/api/product_license_service_api.py +108 -108
  21. lightning_sdk/lightning_cloud/openapi/models/__init__.py +10 -3
  22. lightning_sdk/lightning_cloud/openapi/models/create_machine_request_represents_the_request_to_create_a_machine.py +27 -1
  23. lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
  24. lightning_sdk/lightning_cloud/openapi/models/id_fork_body1.py +27 -1
  25. lightning_sdk/lightning_cloud/openapi/models/license_key_validate_body.py +123 -0
  26. lightning_sdk/lightning_cloud/openapi/models/v1_create_license_request.py +175 -0
  27. lightning_sdk/lightning_cloud/openapi/models/v1_delete_license_response.py +97 -0
  28. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -1
  29. lightning_sdk/lightning_cloud/openapi/models/v1_get_cloud_space_transfer_estimate_response.py +29 -3
  30. lightning_sdk/lightning_cloud/openapi/models/v1_incident.py +27 -1
  31. lightning_sdk/lightning_cloud/openapi/models/v1_incident_detail.py +149 -0
  32. lightning_sdk/lightning_cloud/openapi/models/v1_incident_event.py +27 -1
  33. lightning_sdk/lightning_cloud/openapi/models/v1_license.py +227 -0
  34. lightning_sdk/lightning_cloud/openapi/models/{v1_list_product_licenses_response.py → v1_list_license_response.py} +16 -16
  35. lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -1
  36. lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +53 -1
  37. lightning_sdk/lightning_cloud/openapi/models/v1_token_login_request.py +123 -0
  38. lightning_sdk/lightning_cloud/openapi/models/v1_token_login_response.py +123 -0
  39. lightning_sdk/lightning_cloud/openapi/models/v1_token_owner_type.py +104 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -79
  41. lightning_sdk/lightning_cloud/openapi/models/{v1_product_license_check_response.py → v1_validate_license_response.py} +21 -21
  42. lightning_sdk/lightning_cloud/rest_client.py +48 -45
  43. lightning_sdk/machine.py +2 -0
  44. lightning_sdk/studio.py +14 -2
  45. lightning_sdk/utils/license.py +13 -0
  46. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.22.dist-info}/METADATA +1 -1
  47. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.22.dist-info}/RECORD +51 -41
  48. lightning_sdk/lightning_cloud/openapi/models/v1_product_license.py +0 -435
  49. lightning_sdk/services/license.py +0 -363
  50. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.22.dist-info}/LICENSE +0 -0
  51. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.22.dist-info}/WHEEL +0 -0
  52. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.22.dist-info}/entry_points.txt +0 -0
  53. {lightning_sdk-2025.10.14.dist-info → lightning_sdk-2025.10.22.dist-info}/top_level.txt +0 -0
@@ -1,363 +0,0 @@
1
- import importlib
2
- import json
3
- import os
4
- import socket
5
- import threading
6
- from contextlib import suppress
7
- from functools import partial
8
- from importlib import metadata
9
- from pathlib import Path
10
- from typing import Optional, Tuple
11
-
12
- from lightning_sdk.api.license_api import LICENSE_SIGNING_URL, LicenseApi, generate_url_user_settings
13
- from lightning_sdk.lightning_cloud.login import Auth
14
-
15
- MESSAGE_NOT_AUTHENTICATED = (
16
- "┌─────────────────────────────────────────────────────────────────────────┐\n"
17
- "│ ⚠️ No authenticated user found or license API is not available. │\n"
18
- "│ │\n"
19
- "│ Please make sure you are logged in and have a valid license. │\n"
20
- "│ If you're not logged in, you can use the following command: │\n"
21
- "│ │\n"
22
- "│ lightning login │\n"
23
- "└─────────────────────────────────────────────────────────────────────────┘"
24
- )
25
- MESSAGE_AUTH_NO_LICENSE = (
26
- "┌──────────────────────────────────────────────────────────────────────────────────────────────┐\n"
27
- "│ ⚠️ No valid license found for the authenticated user for product '{self.product_name}'. │\n"
28
- "│ │\n"
29
- "│ Please ensure you have an approved license for this product. │\n"
30
- "│ If you believe this is an error, please contact support. │\n"
31
- "│ │\n"
32
- "│ You can review or update your license settings here: │\n"
33
- f"│ {LICENSE_SIGNING_URL:<89}│\n"
34
- "└──────────────────────────────────────────────────────────────────────────────────────────────┘"
35
- )
36
- MESSAGE_GUIDE_SIGN_LICENSE = (
37
- "┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n"
38
- "│ ⚠️ {reason} │\n"
39
- "│ Details: license key ({license_strats}...{license_ends}) for package {package_name:<56} │\n"
40
- "│ Please make sure you have signed the license agreement and set the license key. │\n"
41
- "│ │\n"
42
- "│ Sign the license agreement here (if you dont have Lightning account, you will asked to create one): │\n"
43
- "│ {link}\n"
44
- "│ │\n"
45
- "│ Once you have the license key, you may need to reinstall this package to activate it. Use the commands: │\n"
46
- "│ │\n"
47
- "│ export LIGHTNING_LICENSE_KEY=<your_license_key> │\n"
48
- "│ pip install --force-reinstall --no-deps {package_name:<63} │\n"
49
- "│ │\n"
50
- "│ For more information, please refer to the documentation. │\n"
51
- "└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘"
52
- )
53
-
54
-
55
- def generate_message_guide_sign_license(package_name: str, reason: str, license_key: str = "") -> str:
56
- """Generate a default message for signing the license agreement."""
57
- if not license_key:
58
- license_key = "." * 64
59
- return MESSAGE_GUIDE_SIGN_LICENSE.format(
60
- link=generate_url_user_settings(name=package_name),
61
- package_name=package_name,
62
- reason=reason.ljust(102, " "),
63
- license_strats=license_key[:5],
64
- license_ends=license_key[-5:],
65
- )
66
-
67
-
68
- MESSAGE_WITH_KEY_OFFLINE = (
69
- "┌──────────────────────────────────────────────────────────────────────────────────────┐\n"
70
- "│ ⚠️ License key ({license_strats}...{license_ends}) is set, but the system is offline. │\n"
71
- "│ │\n"
72
- "│ Please ensure your license key is valid and that the system is connected online. │\n"
73
- "└──────────────────────────────────────────────────────────────────────────────────────┘"
74
- )
75
-
76
-
77
- def generate_message_with_key_offline(license_key: str) -> str:
78
- """Generate a message for when the license key is set but the system is offline."""
79
- return MESSAGE_WITH_KEY_OFFLINE.format(license_strats=license_key[:5], license_ends=license_key[-5:])
80
-
81
-
82
- class LightningLicense:
83
- """This class is used to manage the license for the Lightning SDK."""
84
-
85
- _is_valid: Optional[bool] = None
86
- _license_api: Optional[LicenseApi] = None
87
- _stream_messages: Optional[callable] = None
88
-
89
- def __init__(
90
- self,
91
- name: str,
92
- license_key: Optional[str] = None,
93
- product_version: Optional[str] = None,
94
- product_type: str = "package",
95
- stream_messages: callable = print,
96
- ) -> None:
97
- self._product_name = name
98
- self._license_key = license_key
99
- self._product_version = product_version
100
- self.product_type = product_type
101
- self._is_valid = None
102
- self._license_api = None
103
- self._stream_messages = stream_messages
104
-
105
- @property
106
- def license_api(self) -> LicenseApi:
107
- """Get the LicenseApi instance."""
108
- if not self._license_api:
109
- with suppress(ValueError):
110
- self._license_api = LicenseApi()
111
- return self._license_api
112
-
113
- def validate_license(self) -> bool:
114
- """Validate the license key."""
115
- if not self.is_online():
116
- raise ConnectionError("No internet connection.")
117
-
118
- return self.license_api.valid_license(
119
- license_key=self.license_key,
120
- product_name=self.product_name,
121
- product_version=self.product_version,
122
- product_type=self.product_type,
123
- )
124
-
125
- def _auth_user_id(self) -> Tuple[Optional[str], Optional[str]]:
126
- """Get the authenticated user ID."""
127
- try:
128
- auth = Auth()
129
- except ValueError:
130
- return None, "No user credentials found. Please run `lightning login` to authenticate."
131
- return auth.user_id, None
132
-
133
- def _check_user_license(self, user_id: Optional[str] = None) -> bool:
134
- """Check if the authenticated user has a valid license for this product."""
135
- if not user_id:
136
- user_id, msg = self._auth_user_id()
137
- if msg:
138
- self._stream_messages(msg)
139
- if not user_id:
140
- return False
141
- if not self.license_api:
142
- self._stream_messages(MESSAGE_NOT_AUTHENTICATED)
143
- return False
144
-
145
- licenses = self.license_api.list_user_licenses(user_id=user_id)
146
- for license_info in licenses:
147
- if (
148
- license_info.product_name == self.product_name
149
- and license_info.product_type == self.product_type
150
- and license_info.is_valid
151
- ):
152
- return True
153
- self._stream_messages(MESSAGE_AUTH_NO_LICENSE)
154
- return False
155
-
156
- @staticmethod
157
- def is_online(timeout: float = 2.0) -> bool:
158
- """Check if the system is online by attempting to connect to a public DNS server (Google's).
159
-
160
- This is a simple way to check for internet connectivity.
161
-
162
- Args:
163
- timeout: The timeout for the connection attempt.
164
- """
165
- try:
166
- socket.create_connection(("8.8.8.8", 53), timeout=timeout)
167
- return True
168
- except OSError:
169
- return False
170
-
171
- @property
172
- def is_valid(self) -> Optional[bool]:
173
- """Check if the license key is valid.
174
-
175
- license validation within package:
176
- - user online with valid key -> everything as now
177
- - user online with invalid key -> warning using wrong key + instructions
178
- - user online with no key -> warning for missing license approval + instructions
179
- - user offline with a key -> small warning that key could not be verified
180
- - user offline with no key -> warning for missing license approval + instructions
181
- """
182
- if isinstance(self._is_valid, bool):
183
- # if the license key is already validated, return the cached value
184
- return self._is_valid
185
- is_online = self.is_online()
186
- if is_online:
187
- if self.license_key:
188
- self._is_valid = self.validate_license()
189
- else:
190
- # try to check if the session has logged-in user and if the user has a valid license
191
- self._stream_messages(
192
- "Missing required license key for license validation."
193
- f" Attempting to check if the authenticated user has a valid license for {self.product_name}."
194
- )
195
- user_id, auth_msg = self._auth_user_id()
196
- if not user_id:
197
- self._is_valid = False
198
- if auth_msg:
199
- self._stream_messages(auth_msg)
200
- else:
201
- self._is_valid = self._check_user_license(user_id=user_id)
202
- elif self.license_key:
203
- self._stream_messages(generate_message_with_key_offline(self.license_key))
204
- else:
205
- self._stream_messages(
206
- generate_message_guide_sign_license(
207
- package_name=self.product_name,
208
- reason="License key is not set neither cannot be found in the package root or user home.",
209
- )
210
- )
211
-
212
- return self._is_valid
213
-
214
- @property
215
- def has_required_details(self) -> bool:
216
- """Check if the license key and product name are set."""
217
- return bool(self.license_key and self.product_name and self.product_type)
218
-
219
- @staticmethod
220
- def _find_package_license_key(package_name: str) -> Optional[str]:
221
- """Find the license key in the package root as .license_key or in user home as .lightning/licenses.json.
222
-
223
- Args:
224
- package_name: The name of the package. If not provided, it will be determined from the current module.
225
- """
226
- if not package_name:
227
- return None
228
- pkg_spec = importlib.util.find_spec(package_name)
229
- if pkg_spec is None:
230
- return None
231
- pkg_locations = pkg_spec.submodule_search_locations
232
- if not pkg_locations:
233
- return None
234
- try:
235
- license_file = os.path.join(pkg_locations[0], ".license_key")
236
- with open(license_file) as fp:
237
- return fp.read().strip()
238
- except FileNotFoundError:
239
- return None
240
-
241
- @staticmethod
242
- def _find_user_license_key(package_name: str) -> Optional[str]:
243
- """Find the license key in the user home as .lightning/licenses.json.
244
-
245
- Args:
246
- package_name: The name of the package.
247
- """
248
- home = str(Path.home())
249
- package_name = package_name.lower()
250
- license_file = os.path.join(home, ".lightning", "licenses.json")
251
- try:
252
- with open(license_file) as fp:
253
- licenses = json.load(fp)
254
- # Check for the license key in the licenses.json file
255
- for name in (package_name, package_name.replace("-", "_"), package_name.replace("_", "-")):
256
- if name in licenses:
257
- return licenses[name]
258
- return None
259
- except (FileNotFoundError, json.JSONDecodeError):
260
- return None
261
-
262
- @staticmethod
263
- def _determine_package_version(package_name: str) -> Optional[str]:
264
- """Determine the product version based on the instantiation of the class.
265
-
266
- Args:
267
- package_name: The name of the package. If not provided, it will be determined from the current module.
268
- """
269
- try:
270
- return metadata.version(package_name)
271
- except metadata.PackageNotFoundError:
272
- return None
273
-
274
- @property
275
- def license_key(self) -> Optional[str]:
276
- """Get the license key."""
277
- name = self.product_name.replace("-", "_")
278
- if not self._license_key:
279
- # If the license key is not set, first try to find it env variables
280
- self._license_key = os.environ.get(f"LIGHTNING_{name.upper()}_LICENSE_KEY", None)
281
- if not self._license_key:
282
- # If the license key is not set, second try to find it in the package root
283
- self._license_key = self._find_package_license_key(name)
284
- # If not found, try to find it in the user home
285
- if not self._license_key:
286
- # If not found, try to find it in the user home
287
- self._license_key = self._find_user_license_key(self.product_name)
288
- return self._license_key
289
-
290
- @property
291
- def product_name(self) -> str:
292
- """Get the product name."""
293
- return self._product_name
294
-
295
- @property
296
- def product_version(self) -> Optional[str]:
297
- """Get the product version."""
298
- if not self._product_version and self.product_type == "package":
299
- self._product_version = self._determine_package_version(self.product_name.replace("-", "_"))
300
- return self._product_version
301
-
302
-
303
- def check_license(
304
- name: str,
305
- license_key: Optional[str] = None,
306
- product_version: Optional[str] = None,
307
- product_type: str = "package",
308
- stream_messages: callable = print,
309
- ) -> None:
310
- """Run the license check and stream outputs.
311
-
312
- Args:
313
- name: The name of the product.
314
- license_key: The license key to check.
315
- product_version: The version of the product.
316
- product_type: The type of the product.
317
- stream_messages: A callable to stream messages.
318
- """
319
- lit_license = LightningLicense(
320
- name=name,
321
- license_key=license_key,
322
- product_version=product_version,
323
- product_type=product_type,
324
- stream_messages=stream_messages,
325
- )
326
- if lit_license.is_valid is False:
327
- stream_messages(
328
- generate_message_guide_sign_license(
329
- package_name=lit_license.product_name,
330
- reason="License key is not valid or not set.",
331
- license_key=lit_license.license_key,
332
- )
333
- )
334
-
335
-
336
- def check_license_in_background(
337
- name: str,
338
- license_key: Optional[str] = None,
339
- product_version: Optional[str] = None,
340
- product_type: str = "package",
341
- stream_messages: callable = print,
342
- ) -> threading.Thread:
343
- """Run the license check in a background thread and stream outputs.
344
-
345
- Args:
346
- name: The name of the product.
347
- license_key: The license key to check.
348
- product_version: The version of the product.
349
- product_type: The type of the product.
350
- stream_messages: A callable to stream messages.
351
- """
352
- check_license_local = partial(
353
- check_license,
354
- name=name,
355
- license_key=license_key,
356
- product_version=product_version,
357
- product_type=product_type,
358
- stream_messages=stream_messages,
359
- )
360
-
361
- thread = threading.Thread(target=check_license_local, daemon=True)
362
- thread.start()
363
- return thread