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 CHANGED
@@ -29,6 +29,6 @@ __all__ = [
29
29
  "AIHub",
30
30
  ]
31
31
 
32
- __version__ = "0.1.57"
32
+ __version__ = "0.1.58"
33
33
  _check_version_and_prompt_upgrade(__version__)
34
34
  _set_tqdm_envvars_noninteractive()
@@ -1,4 +1,5 @@
1
- from typing import Any, Callable, Generator, List
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
- "Failed to authenticate with Lightning Container Registry. Please login manually "
17
- "using the following command:\n "
18
- "echo $LIGHTNING_API_KEY | docker login litcr.io --username=LIGHTNING_USERNAME --password-stdin"
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
- authed_user = self._client.auth_service_get_user()
45
- username = authed_user.username
46
- api_key = authed_user.api_key
47
- resp = self._docker_client.login(username, password=api_key, registry=_get_registry_url())
48
- return resp["Status"] == "Login Succeeded"
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
- raise ValueError(f"Container {container} does not exist") from None
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
- lines = self._docker_client.api.push(repository, stream=True, decode=True)
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
@@ -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
- teamspace = self._resolve_teamspace(teamspace)
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("Authenticating with Lightning Container Registry...")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 0.1.57
3
+ Version: 0.1.58
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License
@@ -1,5 +1,5 @@
1
1
  docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
2
- lightning_sdk/__init__.py,sha256=OA3km-3Y0trAWAJMgGucXNd2lF73ByuAOu0VJ59EFak,1039
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=QJ2-6jnKztjbg-Em6KW82z-_hz8XHmzEToVvic3WcLo,4231
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=fy7Bz1TnAzxhjuYKrJQoClSA_hzXf02b0d1FxTvsv9o,11589
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.57.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
900
- lightning_sdk-0.1.57.dist-info/METADATA,sha256=zK1E4W0HFXVjevGaq4MI9eqcvAYTOcToZAOFatRGOlE,4054
901
- lightning_sdk-0.1.57.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
902
- lightning_sdk-0.1.57.dist-info/entry_points.txt,sha256=msB9PJWIJ784dX-OP8by51d4IbKYH3Fj1vCuA9oXjHY,68
903
- lightning_sdk-0.1.57.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
904
- lightning_sdk-0.1.57.dist-info/RECORD,,
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,,