earthscope-sdk 1.0.0b0__py3-none-any.whl → 1.0.0b1__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.
- earthscope_sdk/__init__.py +1 -1
- earthscope_sdk/auth/auth_flow.py +19 -4
- earthscope_sdk/client/user/_base.py +2 -2
- earthscope_sdk/common/service.py +5 -0
- earthscope_sdk/config/models.py +85 -2
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.0.0b1.dist-info}/METADATA +5 -3
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.0.0b1.dist-info}/RECORD +10 -10
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.0.0b1.dist-info}/WHEEL +1 -1
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.0.0b1.dist-info/licenses}/LICENSE +0 -0
- {earthscope_sdk-1.0.0b0.dist-info → earthscope_sdk-1.0.0b1.dist-info}/top_level.txt +0 -0
earthscope_sdk/__init__.py
CHANGED
earthscope_sdk/auth/auth_flow.py
CHANGED
@@ -157,12 +157,14 @@ class AuthFlow(httpx.Auth):
|
|
157
157
|
NoRefreshTokenError: no refresh token is present
|
158
158
|
InvalidRefreshTokenError: the token refresh failed
|
159
159
|
"""
|
160
|
+
from httpx import HTTPStatusError, ReadTimeout
|
161
|
+
|
160
162
|
refresh_token = self.refresh_token
|
161
163
|
scope = scope or self._settings.scope
|
162
164
|
|
163
|
-
|
165
|
+
request = self._ctx.httpx_client.build_request(
|
166
|
+
"POST",
|
164
167
|
f"{self._settings.domain}oauth/token",
|
165
|
-
auth=None, # override client default
|
166
168
|
headers={"content-type": "application/x-www-form-urlencoded"},
|
167
169
|
data={
|
168
170
|
"grant_type": "refresh_token",
|
@@ -171,9 +173,22 @@ class AuthFlow(httpx.Auth):
|
|
171
173
|
"scopes": scope,
|
172
174
|
},
|
173
175
|
)
|
174
|
-
|
175
|
-
|
176
|
+
|
177
|
+
try:
|
178
|
+
async for attempt in self._settings.retry.retry_context(ReadTimeout):
|
179
|
+
with attempt:
|
180
|
+
r = await self._ctx.httpx_client.send(request, auth=None)
|
181
|
+
r.raise_for_status()
|
182
|
+
except HTTPStatusError as e:
|
183
|
+
logger.error(
|
184
|
+
f"error during token refresh ({attempt.num} attempts): {e.response.content}"
|
185
|
+
)
|
176
186
|
raise InvalidRefreshTokenError("refresh token exchange failed")
|
187
|
+
except Exception as e:
|
188
|
+
logger.error(
|
189
|
+
f"error during token refresh ({attempt.num} attempts)", exc_info=e
|
190
|
+
)
|
191
|
+
raise InvalidRefreshTokenError("refresh token exchange failed") from e
|
177
192
|
|
178
193
|
# add previous refresh token to new tokens if omitted from resp
|
179
194
|
# (i.e. we have a non-rotating refresh token)
|
@@ -18,7 +18,7 @@ class UserBaseService(SdkService):
|
|
18
18
|
url=f"{self.resources.api_url}beta/user/credentials/aws/{role}",
|
19
19
|
)
|
20
20
|
|
21
|
-
resp = await self.
|
21
|
+
resp = await self._send_with_retries(req)
|
22
22
|
|
23
23
|
return AwsTemporaryCredentials.model_validate_json(resp.content)
|
24
24
|
|
@@ -34,6 +34,6 @@ class UserBaseService(SdkService):
|
|
34
34
|
url=f"{self.resources.api_url}beta/user/profile",
|
35
35
|
)
|
36
36
|
|
37
|
-
resp = await self.
|
37
|
+
resp = await self._send_with_retries(req)
|
38
38
|
|
39
39
|
return UserProfile.model_validate_json(resp.content)
|
earthscope_sdk/common/service.py
CHANGED
earthscope_sdk/config/models.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
import base64
|
2
2
|
import binascii
|
3
3
|
import datetime as dt
|
4
|
+
import functools
|
4
5
|
from contextlib import suppress
|
5
6
|
from enum import Enum
|
6
7
|
from functools import cached_property
|
7
|
-
from typing import Annotated, Any, Optional, Union
|
8
|
+
from typing import Annotated, Any, Optional, Type, Union
|
8
9
|
|
10
|
+
from annotated_types import Ge, Gt
|
9
11
|
from pydantic import (
|
10
12
|
AliasChoices,
|
11
13
|
BaseModel,
|
@@ -86,7 +88,7 @@ class Tokens(BaseModel):
|
|
86
88
|
return None
|
87
89
|
|
88
90
|
with suppress(IndexError, binascii.Error, ValidationError):
|
89
|
-
payload_b64 = self.access_token.get_secret_value().split(".")[1]
|
91
|
+
payload_b64 = self.access_token.get_secret_value().split(".", 2)[1]
|
90
92
|
payload = base64.b64decode(payload_b64 + "==") # extra padding
|
91
93
|
return AccessTokenBody.model_validate_json(payload)
|
92
94
|
|
@@ -121,6 +123,76 @@ class Tokens(BaseModel):
|
|
121
123
|
raise ValueError("At least one of access token and refresh token is required.")
|
122
124
|
|
123
125
|
|
126
|
+
class RetrySettings(BaseModel):
|
127
|
+
"""
|
128
|
+
Retry configuration for the [Stamina library](https://stamina.hynek.me/en/stable/index.html)
|
129
|
+
"""
|
130
|
+
|
131
|
+
# same defaults as AWS SDK "standard" mode:
|
132
|
+
# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#standard-retry-mode
|
133
|
+
|
134
|
+
attempts: Annotated[int, Ge(0)] = 3
|
135
|
+
timeout: Timedelta = dt.timedelta(seconds=20)
|
136
|
+
|
137
|
+
wait_initial: Timedelta = dt.timedelta(milliseconds=100)
|
138
|
+
wait_max: Timedelta = dt.timedelta(seconds=5)
|
139
|
+
wait_jitter: Timedelta = dt.timedelta(seconds=1)
|
140
|
+
wait_exp_base: Annotated[float, Gt(0)] = 2
|
141
|
+
|
142
|
+
async def retry_context(self, *retry_exc: Type[Exception]):
|
143
|
+
"""
|
144
|
+
Obtain a [Stamina](https://stamina.hynek.me/en/stable/index.html) retry iterator.
|
145
|
+
"""
|
146
|
+
from stamina import retry_context
|
147
|
+
|
148
|
+
retry_on = functools.partial(self.is_retriable, retry_exc=retry_exc)
|
149
|
+
|
150
|
+
ctx = retry_context(
|
151
|
+
on=retry_on,
|
152
|
+
attempts=self.attempts,
|
153
|
+
timeout=self.timeout,
|
154
|
+
wait_initial=self.wait_initial,
|
155
|
+
wait_jitter=self.wait_jitter,
|
156
|
+
wait_max=self.wait_max,
|
157
|
+
wait_exp_base=self.wait_exp_base,
|
158
|
+
)
|
159
|
+
async for attempt in ctx:
|
160
|
+
yield attempt
|
161
|
+
|
162
|
+
def is_retriable(
|
163
|
+
self,
|
164
|
+
exc: Exception,
|
165
|
+
*args,
|
166
|
+
retry_exc: tuple[Type[Exception]] = (),
|
167
|
+
**kwargs,
|
168
|
+
) -> bool:
|
169
|
+
"""
|
170
|
+
Check if the given exception can be retried
|
171
|
+
"""
|
172
|
+
if retry_exc and isinstance(exc, retry_exc):
|
173
|
+
return True
|
174
|
+
|
175
|
+
return False
|
176
|
+
|
177
|
+
|
178
|
+
class HttpRetrySettings(RetrySettings):
|
179
|
+
status_codes: set[int] = {429, 500, 502, 503, 504}
|
180
|
+
|
181
|
+
def is_retriable(
|
182
|
+
self,
|
183
|
+
exc: Exception,
|
184
|
+
*args,
|
185
|
+
**kwargs,
|
186
|
+
) -> bool:
|
187
|
+
from httpx import HTTPStatusError
|
188
|
+
|
189
|
+
if isinstance(exc, HTTPStatusError):
|
190
|
+
if exc.response.status_code in self.status_codes:
|
191
|
+
return True
|
192
|
+
|
193
|
+
return super().is_retriable(exc, *args, **kwargs)
|
194
|
+
|
195
|
+
|
124
196
|
class AuthFlowSettings(Tokens):
|
125
197
|
"""
|
126
198
|
Auth flow configuration
|
@@ -135,6 +207,14 @@ class AuthFlowSettings(Tokens):
|
|
135
207
|
scope: str = "offline_access"
|
136
208
|
client_secret: Optional[SecretStr] = None
|
137
209
|
|
210
|
+
# Auth exchange retries
|
211
|
+
retry: HttpRetrySettings = HttpRetrySettings(
|
212
|
+
attempts=5,
|
213
|
+
timeout=dt.timedelta(seconds=30),
|
214
|
+
wait_initial=dt.timedelta(seconds=1),
|
215
|
+
wait_jitter=dt.timedelta(seconds=3),
|
216
|
+
)
|
217
|
+
|
138
218
|
@cached_property
|
139
219
|
def auth_flow_type(self) -> AuthFlowType:
|
140
220
|
if self.client_secret is not None:
|
@@ -157,6 +237,9 @@ class HttpSettings(BaseModel):
|
|
157
237
|
timeout_connect: Timedelta = dt.timedelta(seconds=5)
|
158
238
|
timeout_read: Timedelta = dt.timedelta(seconds=5)
|
159
239
|
|
240
|
+
# automatically retry requests
|
241
|
+
retry: HttpRetrySettings = HttpRetrySettings()
|
242
|
+
|
160
243
|
# Other
|
161
244
|
user_agent: str = f"earthscope-sdk py/{__version__}"
|
162
245
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: earthscope-sdk
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.0b1
|
4
4
|
Summary: An SDK for EarthScope API
|
5
5
|
Author-email: EarthScope <data-help@earthscope.org>
|
6
6
|
License: Apache License
|
@@ -214,7 +214,8 @@ Requires-Python: >=3.9
|
|
214
214
|
Description-Content-Type: text/markdown
|
215
215
|
License-File: LICENSE
|
216
216
|
Requires-Dist: httpx>=0.27.0
|
217
|
-
Requires-Dist: pydantic-settings[toml]>=2.
|
217
|
+
Requires-Dist: pydantic-settings[toml]>=2.8.0
|
218
|
+
Requires-Dist: stamina>=24.3.0
|
218
219
|
Provides-Extra: dev
|
219
220
|
Requires-Dist: bumpver; extra == "dev"
|
220
221
|
Requires-Dist: build; extra == "dev"
|
@@ -224,6 +225,7 @@ Requires-Dist: pip-tools; extra == "dev"
|
|
224
225
|
Requires-Dist: pytest-httpx; extra == "dev"
|
225
226
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
226
227
|
Requires-Dist: ruff; extra == "dev"
|
228
|
+
Dynamic: license-file
|
227
229
|
|
228
230
|
# EarthScope SDK
|
229
231
|
|
@@ -1,28 +1,28 @@
|
|
1
|
-
earthscope_sdk/__init__.py,sha256=
|
1
|
+
earthscope_sdk/__init__.py,sha256=GUJAs2cToEo9f8MaCUP_VnRsyqZWNhsN-pId5vzPk4U,156
|
2
2
|
earthscope_sdk/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
earthscope_sdk/auth/auth_flow.py,sha256=
|
3
|
+
earthscope_sdk/auth/auth_flow.py,sha256=6jBQxLJ2gAxXGgtZYXDH1yg5L_WUmgu7fRvW4JoCi4Q,10198
|
4
4
|
earthscope_sdk/auth/client_credentials_flow.py,sha256=1GyDSIR1OgYP4u0xZoTov1u_YhY1AzHFpOcBCzY1h6E,2769
|
5
5
|
earthscope_sdk/auth/device_code_flow.py,sha256=dC5Ffj3HzBguRxSHCZYvTe1MD3C-iKf2AlanGuRKNvI,7922
|
6
6
|
earthscope_sdk/auth/error.py,sha256=eC33Bw1HaBEJE7-eI2krtE__5PxStc3EyiYO12v0kVw,693
|
7
7
|
earthscope_sdk/client/__init__.py,sha256=JotTr5oTiiOsUc0RTg82EVCUSg_-u80Qu_R0-crCXkY,139
|
8
8
|
earthscope_sdk/client/_client.py,sha256=ai7WdsTOYglA6bLkT-Wntvxlke6nSaGHwqrtg5PEy80,833
|
9
9
|
earthscope_sdk/client/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
earthscope_sdk/client/user/_base.py,sha256=
|
10
|
+
earthscope_sdk/client/user/_base.py,sha256=8dn4pfQMwVDpF0E6dl6P6HJuNVvozUzfgUGefnPXMnw,1076
|
11
11
|
earthscope_sdk/client/user/_service.py,sha256=wRktOZF5GXajXXxij3Nkule6wvuWOV0vn4QsA1IXVHc,3063
|
12
12
|
earthscope_sdk/client/user/models.py,sha256=drZAMwOYC1NVCzBZQhNL-pPTB28SURKfoZF8HdjlIj8,1214
|
13
13
|
earthscope_sdk/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
14
|
earthscope_sdk/common/_sync_runner.py,sha256=h_A2pSEjZCLj7ov50M6cWHVoX6eXVmGzz5nX0MwLWDY,4131
|
15
15
|
earthscope_sdk/common/client.py,sha256=g5ZTNhFm33H68J9pWD5fDu760Yd5cBdfQmsbU3t8D_4,2156
|
16
16
|
earthscope_sdk/common/context.py,sha256=vrCB_Ez-98Ir7c0GrCe-g7DuRCgc9vPaoRWFYf5q8Ko,5138
|
17
|
-
earthscope_sdk/common/service.py,sha256=
|
17
|
+
earthscope_sdk/common/service.py,sha256=SCUZVJA3jFaEPeFrOf0v9osf2UpqldhlFmirOYWJjxM,1506
|
18
18
|
earthscope_sdk/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
earthscope_sdk/config/_compat.py,sha256=P3F5y_Kf5zp9m9uOhl1Bp3ke6expxq4Sm9AeVaBbAHk,4610
|
20
20
|
earthscope_sdk/config/_util.py,sha256=RZ6zvKrvjUkO7i69s7AVoIDhamRg4x71CAZLnucr9QM,1249
|
21
21
|
earthscope_sdk/config/error.py,sha256=jh25q-b317lAvp32WwQw0zdYoV-MxZtg-n5FgZOMymI,95
|
22
|
-
earthscope_sdk/config/models.py,sha256=
|
22
|
+
earthscope_sdk/config/models.py,sha256=1334Rxzw4qDLSdQg9btxFQySBOCb8TEW6J95M-lyKEc,8198
|
23
23
|
earthscope_sdk/config/settings.py,sha256=I2DwEvfmETcaYbSvUybs0EIih0yiJO9D46WnWzKPqbo,8812
|
24
|
-
earthscope_sdk-1.0.
|
25
|
-
earthscope_sdk-1.0.
|
26
|
-
earthscope_sdk-1.0.
|
27
|
-
earthscope_sdk-1.0.
|
28
|
-
earthscope_sdk-1.0.
|
24
|
+
earthscope_sdk-1.0.0b1.dist-info/licenses/LICENSE,sha256=E_MrVXxRaMQNpvZhsDuz_J9N_ux7dlL_WpYSsE391HU,11349
|
25
|
+
earthscope_sdk-1.0.0b1.dist-info/METADATA,sha256=BHeAUzZ882lmEExnoVKIywGSYvIbyNdnIh8BzRJs2Ng,17988
|
26
|
+
earthscope_sdk-1.0.0b1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
27
|
+
earthscope_sdk-1.0.0b1.dist-info/top_level.txt,sha256=zTtIT9yN3JPJF7TqmTzqQcAvZZe4pAm907DLoGa5T_E,15
|
28
|
+
earthscope_sdk-1.0.0b1.dist-info/RECORD,,
|
File without changes
|
File without changes
|