lightning-sdk 0.1.57__py3-none-any.whl → 0.1.58__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/lit_container_api.py +82 -20
- lightning_sdk/cli/upload.py +13 -3
- {lightning_sdk-0.1.57.dist-info → lightning_sdk-0.1.58.dist-info}/METADATA +1 -1
- {lightning_sdk-0.1.57.dist-info → lightning_sdk-0.1.58.dist-info}/RECORD +9 -9
- {lightning_sdk-0.1.57.dist-info → lightning_sdk-0.1.58.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.1.57.dist-info → lightning_sdk-0.1.58.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.1.57.dist-info → lightning_sdk-0.1.58.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.1.57.dist-info → lightning_sdk-0.1.58.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Callable, Dict, Generator, Iterator, List
|
|
2
3
|
|
|
3
4
|
import docker
|
|
4
5
|
import requests
|
|
@@ -12,11 +13,11 @@ from lightning_sdk.teamspace import Teamspace
|
|
|
12
13
|
|
|
13
14
|
class LCRAuthFailedError(Exception):
|
|
14
15
|
def __init__(self) -> None:
|
|
15
|
-
super().__init__(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
super().__init__("Failed to authenticate with Lightning Container Registry. Please try again.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DockerPushError(Exception):
|
|
20
|
+
pass
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def retry_on_lcr_auth_failure(func: Callable) -> Callable:
|
|
@@ -24,7 +25,7 @@ def retry_on_lcr_auth_failure(func: Callable) -> Callable:
|
|
|
24
25
|
try:
|
|
25
26
|
return func(self, *args, **kwargs)
|
|
26
27
|
except LCRAuthFailedError:
|
|
27
|
-
self.authenticate()
|
|
28
|
+
self.authenticate(reauth=True)
|
|
28
29
|
return func(self, *args, **kwargs)
|
|
29
30
|
|
|
30
31
|
return wrapper
|
|
@@ -37,15 +38,35 @@ class LitContainerApi:
|
|
|
37
38
|
try:
|
|
38
39
|
self._docker_client = docker.from_env()
|
|
39
40
|
self._docker_client.ping()
|
|
41
|
+
self._docker_auth_config = {}
|
|
40
42
|
except docker.errors.DockerException as e:
|
|
41
43
|
raise RuntimeError(f"Failed to connect to Docker daemon: {e!s}. Is Docker running?") from None
|
|
42
44
|
|
|
43
|
-
def authenticate(self) -> bool:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
def authenticate(self, reauth: bool = False) -> bool:
|
|
46
|
+
try:
|
|
47
|
+
authed_user = self._client.auth_service_get_user()
|
|
48
|
+
username = authed_user.username
|
|
49
|
+
api_key = authed_user.api_key
|
|
50
|
+
registry = _get_registry_url()
|
|
51
|
+
resp = self._docker_client.login(username, password=api_key, registry=registry, reauth=reauth)
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
resp.get("username", None) == username
|
|
55
|
+
and resp.get("password", None) == api_key
|
|
56
|
+
and resp.get("serveraddress", None) == registry
|
|
57
|
+
):
|
|
58
|
+
self._docker_auth_config = {"username": username, "password": api_key}
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
# This is a new 200 response auth attempt from the client.
|
|
62
|
+
if "Status" in resp and resp["Status"] == "Login Succeeded":
|
|
63
|
+
self._docker_auth_config = {"username": username, "password": api_key}
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
return False
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"Authentication error: {e} resp: {resp}")
|
|
69
|
+
return False
|
|
49
70
|
|
|
50
71
|
def list_containers(self, project_id: str) -> List:
|
|
51
72
|
project = self._client.lit_registry_service_get_lit_project_registry(project_id)
|
|
@@ -61,7 +82,15 @@ class LitContainerApi:
|
|
|
61
82
|
try:
|
|
62
83
|
self._docker_client.images.get(container)
|
|
63
84
|
except docker.errors.ImageNotFound:
|
|
64
|
-
|
|
85
|
+
try:
|
|
86
|
+
self._docker_client.images.pull(container, tag)
|
|
87
|
+
self._docker_client.images.get(container)
|
|
88
|
+
except docker.errors.APIError as e:
|
|
89
|
+
raise ValueError(f"Could not pull container {container}") from e
|
|
90
|
+
except docker.errors.ImageNotFound as e:
|
|
91
|
+
raise ValueError(f"Container {container} does not exist") from e
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ValueError(f"Unable to upload {container}") from e
|
|
65
94
|
|
|
66
95
|
registry_url = _get_registry_url()
|
|
67
96
|
container_basename = container.split("/")[-1]
|
|
@@ -69,22 +98,55 @@ class LitContainerApi:
|
|
|
69
98
|
tagged = self._docker_client.api.tag(container, repository, tag)
|
|
70
99
|
if not tagged:
|
|
71
100
|
raise ValueError(f"Could not tag container {container} with {repository}:{tag}")
|
|
72
|
-
|
|
73
|
-
for line in lines:
|
|
74
|
-
if "errorDetail" in line and ("authorization failed" in line["error"] or "unauth" in line["error"]):
|
|
75
|
-
raise LCRAuthFailedError()
|
|
76
|
-
yield line
|
|
101
|
+
yield from self._push_with_retry(repository)
|
|
77
102
|
yield {
|
|
78
103
|
"finish": True,
|
|
79
104
|
"url": f"{LIGHTNING_CLOUD_URL}/{teamspace.owner.name}/{teamspace.name}/containers/{container_basename}",
|
|
80
105
|
}
|
|
81
106
|
|
|
107
|
+
def _push_with_retry(self, repository: str, max_retries: int = 3) -> Iterator[Dict[str, Any]]:
|
|
108
|
+
def is_auth_error(error_msg: str) -> bool:
|
|
109
|
+
auth_errors = ["unauthorized", "authentication required", "unauth"]
|
|
110
|
+
return any(err in error_msg.lower() for err in auth_errors)
|
|
111
|
+
|
|
112
|
+
def is_timeout_error(error_msg: str) -> bool:
|
|
113
|
+
timeout_errors = ["proxyconnect tcp", "i/o timeout"]
|
|
114
|
+
return any(err in error_msg.lower() for err in timeout_errors)
|
|
115
|
+
|
|
116
|
+
for attempt in range(max_retries):
|
|
117
|
+
try:
|
|
118
|
+
if attempt > 0:
|
|
119
|
+
# This is important, if we don't set reauth here then we just keep using the
|
|
120
|
+
# same authentication context that we know just failed.
|
|
121
|
+
self.authenticate(reauth=True)
|
|
122
|
+
time.sleep(2)
|
|
123
|
+
|
|
124
|
+
lines = self._docker_client.api.push(repository, stream=True, decode=True, auth_config=self._docker_auth_config)
|
|
125
|
+
for line in lines:
|
|
126
|
+
if isinstance(line, dict) and "error" in line:
|
|
127
|
+
error = line["error"]
|
|
128
|
+
if is_auth_error(error) or is_timeout_error(error):
|
|
129
|
+
if attempt < max_retries - 1:
|
|
130
|
+
break
|
|
131
|
+
raise DockerPushError(f"Max retries reached: {error}")
|
|
132
|
+
raise DockerPushError(f"Push error: {error}")
|
|
133
|
+
yield line
|
|
134
|
+
else:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
except docker.errors.APIError as e:
|
|
138
|
+
if (is_auth_error(str(e)) or is_timeout_error(str(e))) and attempt < max_retries - 1:
|
|
139
|
+
continue
|
|
140
|
+
raise DockerPushError(f"Push failed: {e}") from e
|
|
141
|
+
|
|
142
|
+
raise DockerPushError("Max push retries reached")
|
|
143
|
+
|
|
82
144
|
@retry_on_lcr_auth_failure
|
|
83
145
|
def download_container(self, container: str, teamspace: Teamspace, tag: str) -> Generator[str, None, None]:
|
|
84
146
|
registry_url = _get_registry_url()
|
|
85
147
|
repository = f"{registry_url}/lit-container/{teamspace.owner.name}/{teamspace.name}/{container}"
|
|
86
148
|
try:
|
|
87
|
-
self._docker_client.images.pull(repository, tag=tag)
|
|
149
|
+
self._docker_client.images.pull(repository, tag=tag, auth_config=self._docker_auth_config)
|
|
88
150
|
except requests.exceptions.HTTPError as e:
|
|
89
151
|
if "unauthorized" in e.response.text:
|
|
90
152
|
raise LCRAuthFailedError() from e
|
lightning_sdk/cli/upload.py
CHANGED
|
@@ -154,7 +154,9 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
|
|
|
154
154
|
console.print(f"See your file at {studio_url}")
|
|
155
155
|
|
|
156
156
|
def container(self, container: str, tag: str = "latest", teamspace: Optional[str] = None) -> None:
|
|
157
|
-
|
|
157
|
+
"""Upload a container to Lightning AI's container registry."""
|
|
158
|
+
menu = _TeamspacesMenu()
|
|
159
|
+
teamspace = menu._resolve_teamspace(teamspace)
|
|
158
160
|
api = LitContainerApi()
|
|
159
161
|
console = Console()
|
|
160
162
|
with Progress(
|
|
@@ -166,11 +168,19 @@ class _Uploads(_StudiosMenu, _TeamspacesMenu):
|
|
|
166
168
|
) as progress:
|
|
167
169
|
push_task = progress.add_task("Pushing Docker image", total=None)
|
|
168
170
|
try:
|
|
171
|
+
console.print("Authenticating with Lightning Container Registry...")
|
|
172
|
+
try:
|
|
173
|
+
api.authenticate()
|
|
174
|
+
console.print("Authenticated with Lightning Container Registry", style="green")
|
|
175
|
+
except Exception:
|
|
176
|
+
# let the push with retry take control of auth moving forward
|
|
177
|
+
pass
|
|
178
|
+
|
|
169
179
|
lines = api.upload_container(container, teamspace, tag)
|
|
170
180
|
self._print_docker_push(lines, console, progress, push_task)
|
|
171
181
|
except LCRAuthFailedError:
|
|
172
|
-
console.print("
|
|
173
|
-
if not api.authenticate():
|
|
182
|
+
console.print("Re-authenticating with Lightning Container Registry...")
|
|
183
|
+
if not api.authenticate(reauth=True):
|
|
174
184
|
raise StudioCliError("Failed to authenticate with Lightning Container Registry") from None
|
|
175
185
|
console.print("Authenticated with Lightning Container Registry", style="green")
|
|
176
186
|
lines = api.upload_container(container, teamspace, tag)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
|
|
2
|
-
lightning_sdk/__init__.py,sha256=
|
|
2
|
+
lightning_sdk/__init__.py,sha256=ruIP3Znv7pOMFZGza2nk5WQAKSi4E64V4RBeB28YTFc,1039
|
|
3
3
|
lightning_sdk/agents.py,sha256=ly6Ma1j0ZgGPFyvPvMN28JWiB9dATIstFa5XM8pMi6I,1577
|
|
4
4
|
lightning_sdk/ai_hub.py,sha256=dfw5CS93ehGmPlZCbbE2Pu3v0ANI6ZY05-ULbcHFqs4,7189
|
|
5
5
|
lightning_sdk/constants.py,sha256=ztl1PTUBULnqTf3DyKUSJaV_O20hNtUYT6XvAYIrmIk,749
|
|
@@ -19,7 +19,7 @@ lightning_sdk/api/agents_api.py,sha256=G47TbFo9kYqnBMqdw2RW-lfS1VAUBSXDmzs6fpIEM
|
|
|
19
19
|
lightning_sdk/api/ai_hub_api.py,sha256=Yr9VvxueIqPUUeMqExbjbWD5CE_yeWjq0dXQQv47erg,6779
|
|
20
20
|
lightning_sdk/api/deployment_api.py,sha256=SwpTh8elvCeEE-Ilf5pJc4nM_xUXBUNWV5ejH4Y5glI,21903
|
|
21
21
|
lightning_sdk/api/job_api.py,sha256=pHJMs209qMfML0uO2jV-6v5QfA8bb-whMxQfmiInb5U,12404
|
|
22
|
-
lightning_sdk/api/lit_container_api.py,sha256=
|
|
22
|
+
lightning_sdk/api/lit_container_api.py,sha256=jaE4uXD9Y5ThhXhDw2qcmwQ8umtvsbrtYqRsnqIIPvg,7018
|
|
23
23
|
lightning_sdk/api/mmt_api.py,sha256=UXUJaHG4R-8jDWkYh6hFYr0uJSiAYiXFiWTJn4_eVHQ,7515
|
|
24
24
|
lightning_sdk/api/org_api.py,sha256=Ze3z_ATVrukobujV5YdC42DKj45Vuwl7X52q_Vr-o3U,803
|
|
25
25
|
lightning_sdk/api/studio_api.py,sha256=N0XbvNvFha-et94bTlQLLjj6OPTXvrpIWgnSMUXi4UI,26327
|
|
@@ -48,7 +48,7 @@ lightning_sdk/cli/stop.py,sha256=KzykI4-mVfla0yMRvSuBAxoo06hq3gG342sVuSFpBA4,264
|
|
|
48
48
|
lightning_sdk/cli/studios_menu.py,sha256=TA9rO6_fFHGMz0Nt4rJ6iV80X5pZE4xShrSiyXoU-oQ,4129
|
|
49
49
|
lightning_sdk/cli/switch.py,sha256=K78yxn9voyVCZR46lV29ZD6A539EPRmvg6XYgb2K6mc,1876
|
|
50
50
|
lightning_sdk/cli/teamspace_menu.py,sha256=C3g3spTKgtMwoK7pnooy0MBPz4AKhFjcObkvZyZ4v04,3797
|
|
51
|
-
lightning_sdk/cli/upload.py,sha256=
|
|
51
|
+
lightning_sdk/cli/upload.py,sha256=qcEdBYFFHc_LfSNir6wl1_GZG3PkijFxYatapwv-VyM,12094
|
|
52
52
|
lightning_sdk/deployment/__init__.py,sha256=BLu7_cVLp97TYxe6qe-J1zKUSZXAVcvCjgcA7plV2k4,497
|
|
53
53
|
lightning_sdk/deployment/deployment.py,sha256=Fn8iJVimaWB0Ngz53oJ03kBUzVe5_-3oHAbvEqFTf0s,15681
|
|
54
54
|
lightning_sdk/job/__init__.py,sha256=1MxjQ6rHkyUHCypSW9RuXuVMVH11WiqhIXcU2LCFMwE,64
|
|
@@ -896,9 +896,9 @@ lightning_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
896
896
|
lightning_sdk/utils/dynamic.py,sha256=glUTO1JC9APtQ6Gr9SO02a3zr56-sPAXM5C3NrTpgyQ,1959
|
|
897
897
|
lightning_sdk/utils/enum.py,sha256=h2JRzqoBcSlUdanFHmkj_j5DleBHAu1esQYUsdNI-hU,4106
|
|
898
898
|
lightning_sdk/utils/resolve.py,sha256=idhRk4zg0jBOlySwKwjhsGgRorpsNVHOKOADOxMUYY0,6062
|
|
899
|
-
lightning_sdk-0.1.
|
|
900
|
-
lightning_sdk-0.1.
|
|
901
|
-
lightning_sdk-0.1.
|
|
902
|
-
lightning_sdk-0.1.
|
|
903
|
-
lightning_sdk-0.1.
|
|
904
|
-
lightning_sdk-0.1.
|
|
899
|
+
lightning_sdk-0.1.58.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
|
|
900
|
+
lightning_sdk-0.1.58.dist-info/METADATA,sha256=YJJ6KEn8kLe7WF-r72Dhg5loqInqGLlJALO231wf1lo,4054
|
|
901
|
+
lightning_sdk-0.1.58.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
902
|
+
lightning_sdk-0.1.58.dist-info/entry_points.txt,sha256=msB9PJWIJ784dX-OP8by51d4IbKYH3Fj1vCuA9oXjHY,68
|
|
903
|
+
lightning_sdk-0.1.58.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
|
|
904
|
+
lightning_sdk-0.1.58.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|