ee-client 2.2.0__tar.gz → 2.3.0__tar.gz
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.
- {ee_client-2.2.0 → ee_client-2.3.0}/PKG-INFO +2 -2
- {ee_client-2.2.0 → ee_client-2.3.0}/ee_client.egg-info/PKG-INFO +2 -2
- {ee_client-2.2.0 → ee_client-2.3.0}/ee_client.egg-info/requires.txt +1 -1
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/__init__.py +1 -1
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/client.py +101 -60
- ee_client-2.3.0/eeclient/exceptions.py +100 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/sepal_credential_mixin.py +36 -7
- {ee_client-2.2.0 → ee_client-2.3.0}/pyproject.toml +3 -3
- ee_client-2.2.0/eeclient/exceptions.py +0 -26
- {ee_client-2.2.0 → ee_client-2.3.0}/LICENSE +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/README.rst +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/ee_client.egg-info/SOURCES.txt +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/ee_client.egg-info/dependency_links.txt +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/ee_client.egg-info/top_level.txt +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/data.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/export/__init__.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/export/image.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/export/table.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/helpers.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/interfaces/__init__.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/interfaces/export.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/interfaces/operations.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/interfaces/tasks.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/models.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/oauth_app.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/eeclient/tasks.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/setup.cfg +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/tests/test_client.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/tests/test_data.py +0 -0
- {ee_client-2.2.0 → ee_client-2.3.0}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ee-client
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: extends the capabilities of the earthengine-api by providing custom session management and client interactions
|
|
5
5
|
Author-email: Daniel Guerrero <dfgm2006@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dfguerrerom/ee-client
|
|
@@ -15,7 +15,7 @@ Requires-Python: >=3.9
|
|
|
15
15
|
Description-Content-Type: text/x-rst
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: earthengine-api
|
|
18
|
-
Requires-Dist: httpx
|
|
18
|
+
Requires-Dist: httpx[http2]
|
|
19
19
|
Requires-Dist: aiogoogle
|
|
20
20
|
Requires-Dist: tenacity
|
|
21
21
|
Requires-Dist: pydantic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ee-client
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: extends the capabilities of the earthengine-api by providing custom session management and client interactions
|
|
5
5
|
Author-email: Daniel Guerrero <dfgm2006@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dfguerrerom/ee-client
|
|
@@ -15,7 +15,7 @@ Requires-Python: >=3.9
|
|
|
15
15
|
Description-Content-Type: text/x-rst
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: earthengine-api
|
|
18
|
-
Requires-Dist: httpx
|
|
18
|
+
Requires-Dist: httpx[http2]
|
|
19
19
|
Requires-Dist: aiogoogle
|
|
20
20
|
Requires-Dist: tenacity
|
|
21
21
|
Requires-Dist: pydantic
|
|
@@ -33,6 +33,23 @@ SEPAL_API_DOWNLOAD_URL = None
|
|
|
33
33
|
VERIFY_SSL = True
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
class SimpleRateLimiter:
|
|
37
|
+
def __init__(self, qps: float | None):
|
|
38
|
+
self.qps = qps
|
|
39
|
+
self._lock = asyncio.Lock()
|
|
40
|
+
self._next = 0.0
|
|
41
|
+
|
|
42
|
+
async def acquire(self):
|
|
43
|
+
if not self.qps or self.qps <= 0:
|
|
44
|
+
return
|
|
45
|
+
async with self._lock:
|
|
46
|
+
now = asyncio.get_running_loop().time()
|
|
47
|
+
wait = max(0.0, self._next - now)
|
|
48
|
+
if wait:
|
|
49
|
+
await asyncio.sleep(wait)
|
|
50
|
+
self._next = max(now, self._next) + 1.0 / self.qps
|
|
51
|
+
|
|
52
|
+
|
|
36
53
|
class EESession(SepalCredentialMixin):
|
|
37
54
|
def __init__(
|
|
38
55
|
self,
|
|
@@ -52,12 +69,18 @@ class EESession(SepalCredentialMixin):
|
|
|
52
69
|
Raises:
|
|
53
70
|
ValueError: If SEPAL_HOST environment variable is not set
|
|
54
71
|
"""
|
|
72
|
+
self._inflight = asyncio.BoundedSemaphore(30)
|
|
73
|
+
self._rate = SimpleRateLimiter(60)
|
|
74
|
+
|
|
75
|
+
self._auth_refresh_lock = asyncio.Lock()
|
|
76
|
+
|
|
77
|
+
self._client: httpx.AsyncClient | None = None
|
|
78
|
+
self._client_lock = asyncio.Lock()
|
|
79
|
+
|
|
55
80
|
self.enforce_project_id = enforce_project_id
|
|
56
81
|
super().__init__(sepal_headers)
|
|
57
82
|
|
|
58
|
-
# Create user-specific logger
|
|
59
83
|
self.logger = logging.getLogger(f"eeclient.{self.user}")
|
|
60
|
-
|
|
61
84
|
self.logger.debug(
|
|
62
85
|
"EESession initialized"
|
|
63
86
|
if self._credentials
|
|
@@ -127,29 +150,37 @@ class EESession(SepalCredentialMixin):
|
|
|
127
150
|
|
|
128
151
|
return GEEHeaders.model_validate(data)
|
|
129
152
|
|
|
130
|
-
async def get_headers(self)
|
|
131
|
-
|
|
153
|
+
async def get_headers(self):
|
|
154
|
+
# Only one task refreshes the token; others wait briefly.
|
|
132
155
|
if self.needs_credentials_refresh():
|
|
133
|
-
|
|
156
|
+
async with self._auth_refresh_lock:
|
|
157
|
+
if self.needs_credentials_refresh(): # double-check after lock
|
|
158
|
+
await self.set_credentials()
|
|
134
159
|
return self.get_current_headers()
|
|
135
160
|
|
|
161
|
+
async def _ensure_client(self) -> httpx.AsyncClient:
|
|
162
|
+
if self._client is None:
|
|
163
|
+
async with self._client_lock:
|
|
164
|
+
if self._client is None:
|
|
165
|
+
self._client = httpx.AsyncClient(
|
|
166
|
+
http2=True,
|
|
167
|
+
timeout=httpx.Timeout(connect=60, read=360, write=60, pool=60),
|
|
168
|
+
limits=httpx.Limits(
|
|
169
|
+
max_connections=40, max_keepalive_connections=20
|
|
170
|
+
),
|
|
171
|
+
verify=getattr(self, "verify_ssl", True),
|
|
172
|
+
)
|
|
173
|
+
return self._client
|
|
174
|
+
|
|
136
175
|
@asynccontextmanager
|
|
137
176
|
async def get_client(self):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
limits = httpx.Limits(max_connections=100, max_keepalive_connections=50)
|
|
146
|
-
async_client = httpx.AsyncClient(
|
|
147
|
-
headers=headers, timeout=timeout, limits=limits
|
|
148
|
-
)
|
|
149
|
-
try:
|
|
150
|
-
yield async_client
|
|
151
|
-
finally:
|
|
152
|
-
await async_client.aclose()
|
|
177
|
+
client = await self._ensure_client()
|
|
178
|
+
yield client
|
|
179
|
+
|
|
180
|
+
async def aclose(self):
|
|
181
|
+
if self._client is not None:
|
|
182
|
+
await self._client.aclose()
|
|
183
|
+
self._client = None
|
|
153
184
|
|
|
154
185
|
async def rest_call(
|
|
155
186
|
self,
|
|
@@ -168,51 +199,61 @@ class EESession(SepalCredentialMixin):
|
|
|
168
199
|
|
|
169
200
|
while attempt < max_attempts:
|
|
170
201
|
try:
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
headers = (await self.get_headers()).model_dump(by_alias=True)
|
|
203
|
+
url_with_project = self.set_url_project(url)
|
|
204
|
+
|
|
205
|
+
async with self._inflight:
|
|
206
|
+
await self._rate.acquire()
|
|
207
|
+
async with self.get_client() as client:
|
|
173
208
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
if "assets" not in url_with_project:
|
|
210
|
+
# Do not log assets requests
|
|
211
|
+
self.logger.debug(
|
|
212
|
+
f"Making async {method} request to {url_with_project}"
|
|
213
|
+
)
|
|
214
|
+
response = await client.request(
|
|
215
|
+
method,
|
|
216
|
+
url_with_project,
|
|
217
|
+
json=data,
|
|
218
|
+
params=params,
|
|
219
|
+
headers=headers,
|
|
178
220
|
)
|
|
179
|
-
response = await client.request(
|
|
180
|
-
method, url_with_project, json=data, params=params
|
|
181
|
-
)
|
|
182
221
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
if response.status_code >= 400:
|
|
223
|
+
if "application/json" in response.headers.get(
|
|
224
|
+
"Content-Type", ""
|
|
225
|
+
):
|
|
226
|
+
error_data = response.json().get("error", {})
|
|
227
|
+
self.logger.error(
|
|
228
|
+
f"Request failed with error: {error_data}"
|
|
229
|
+
)
|
|
230
|
+
raise EERestException(error_data)
|
|
231
|
+
else:
|
|
232
|
+
error_data = {
|
|
233
|
+
"code": response.status_code,
|
|
234
|
+
"message": response.reason_phrase
|
|
235
|
+
or "Unknown HTTP error",
|
|
236
|
+
"status": response.status_code,
|
|
237
|
+
}
|
|
238
|
+
self.logger.error(
|
|
239
|
+
f"Request failed with HTTP error: {error_data}"
|
|
240
|
+
)
|
|
241
|
+
raise EERestException(error_data)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
return response.json()
|
|
245
|
+
except Exception as e:
|
|
246
|
+
self.logger.error(f"Error parsing JSON response: {str(e)}")
|
|
247
|
+
self.logger.debug(
|
|
248
|
+
f"Response content: {response.text[:500]}..."
|
|
190
249
|
)
|
|
191
|
-
raise EERestException(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"status": response.status_code,
|
|
198
|
-
}
|
|
199
|
-
self.logger.error(
|
|
200
|
-
f"Request failed with HTTP error: {error_data}"
|
|
250
|
+
raise EERestException(
|
|
251
|
+
{
|
|
252
|
+
"code": 500,
|
|
253
|
+
"message": f"Invalid JSON response: {str(e)}",
|
|
254
|
+
"status": response.status_code,
|
|
255
|
+
}
|
|
201
256
|
)
|
|
202
|
-
raise EERestException(error_data)
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
return response.json()
|
|
206
|
-
except Exception as e:
|
|
207
|
-
self.logger.error(f"Error parsing JSON response: {str(e)}")
|
|
208
|
-
self.logger.debug(f"Response content: {response.text[:500]}...")
|
|
209
|
-
raise EERestException(
|
|
210
|
-
{
|
|
211
|
-
"code": 500,
|
|
212
|
-
"message": f"Invalid JSON response: {str(e)}",
|
|
213
|
-
"status": response.status_code,
|
|
214
|
-
}
|
|
215
|
-
)
|
|
216
257
|
|
|
217
258
|
except EERestException as e:
|
|
218
259
|
last_error = e
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from ee.ee_exception import EEException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EERestException(EEException):
|
|
5
|
+
def __init__(self, error):
|
|
6
|
+
self.message = error.get("message", "EE responded with an error")
|
|
7
|
+
super().__init__(self.message)
|
|
8
|
+
self.code = error.get("code", -1)
|
|
9
|
+
self.status = error.get("status", "UNDEFINED")
|
|
10
|
+
self.details = error.get("details")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EEClientError(Exception):
|
|
14
|
+
"""Custom exception class for EEClient errors."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, error):
|
|
17
|
+
if isinstance(error, str):
|
|
18
|
+
self.message = error
|
|
19
|
+
self.code = -1
|
|
20
|
+
self.status = "UNDEFINED"
|
|
21
|
+
self.details = None
|
|
22
|
+
else:
|
|
23
|
+
self.message = error.get("message", "EEClient responded with an error")
|
|
24
|
+
self.code = error.get("code", -1)
|
|
25
|
+
self.status = error.get("status", "UNDEFINED")
|
|
26
|
+
self.details = error.get("details")
|
|
27
|
+
super().__init__(self.message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CredentialsFileNotFoundError(EEClientError):
|
|
31
|
+
"""Raised when local credentials file is not found."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, file_path: str):
|
|
34
|
+
self.file_path = file_path
|
|
35
|
+
error = {
|
|
36
|
+
"code": 404,
|
|
37
|
+
"message": (
|
|
38
|
+
"There was an error when trying to authenticate with Google "
|
|
39
|
+
"Earth Engine. Make sure your GEE account is properly "
|
|
40
|
+
"connected to SEPAL. For more information, please visit "
|
|
41
|
+
"<a href='https://docs.sepal.io/en/latest/setup/gee.html' "
|
|
42
|
+
"target='_blank'> the documentation </a>."
|
|
43
|
+
),
|
|
44
|
+
"status": "CREDENTIALS_NOT_FOUND",
|
|
45
|
+
"details": f"Credentials file not found at {file_path}",
|
|
46
|
+
}
|
|
47
|
+
super().__init__(error)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CredentialsFileUnknownError(EEClientError):
|
|
51
|
+
"""Raised when unknown error occurred while accessing credentials file."""
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
error = {
|
|
55
|
+
"code": 500,
|
|
56
|
+
"message": (
|
|
57
|
+
"There was an error when trying to authenticate with Google "
|
|
58
|
+
"Earth Engine. Please re-authenticate your GEE account with "
|
|
59
|
+
"SEPAL. For more information, visit "
|
|
60
|
+
"<a href='https://docs.sepal.io/en/latest/setup/gee.html' "
|
|
61
|
+
"target='_blank'> the documentation </a>."
|
|
62
|
+
),
|
|
63
|
+
"status": "CREDENTIALS_ERROR",
|
|
64
|
+
"details": "Unknown error occurred while accessing credentials file",
|
|
65
|
+
}
|
|
66
|
+
super().__init__(error)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SepalCredentialsUnavailableError(EEClientError):
|
|
70
|
+
"""Raised when SEPAL API cannot provide credentials (e.g., 500 error)."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, status_code: int | None = None):
|
|
73
|
+
self.status_code = status_code
|
|
74
|
+
error = {
|
|
75
|
+
"code": status_code or 500,
|
|
76
|
+
"message": (
|
|
77
|
+
"There was an error when trying to authenticate with Google "
|
|
78
|
+
"Earth Engine. Make sure your GEE account is properly "
|
|
79
|
+
"connected to SEPAL. Please visit "
|
|
80
|
+
"<a href='https://docs.sepal.io/en/latest/setup/gee.html' "
|
|
81
|
+
"target='_blank'> the documentation </a> for more information."
|
|
82
|
+
),
|
|
83
|
+
"status": "SEPAL_CREDENTIALS_UNAVAILABLE",
|
|
84
|
+
"details": (
|
|
85
|
+
f"SEPAL API returned error {status_code}"
|
|
86
|
+
if status_code
|
|
87
|
+
else "SEPAL API is unable to provide credentials"
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
super().__init__(error)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# {'code': 401, 'message': 'Request had invalid authentication credentials.
|
|
94
|
+
# Expected OAuth 2 access token, login cookie or other valid authentication
|
|
95
|
+
# credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.',
|
|
96
|
+
# 'status': 'UNAUTHENTICATED'}
|
|
97
|
+
# Exception in _run_task: Request had invalid authentication credentials.
|
|
98
|
+
# Expected OAuth 2 access token, login cookie or other valid authentication
|
|
99
|
+
# credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
|
|
100
|
+
# when that error happens, I need to re-set the credentials
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
from eeclient.models import SepalHeaders, GoogleTokens
|
|
2
|
+
from eeclient.exceptions import (
|
|
3
|
+
CredentialsFileNotFoundError,
|
|
4
|
+
CredentialsFileUnknownError,
|
|
5
|
+
SepalCredentialsUnavailableError,
|
|
6
|
+
)
|
|
2
7
|
import os
|
|
3
8
|
import logging
|
|
4
9
|
import json
|
|
@@ -84,10 +89,14 @@ class SepalCredentialMixin:
|
|
|
84
89
|
def _load_credentials_from_file(self):
|
|
85
90
|
"""Load credentials from file and update internal state"""
|
|
86
91
|
if not self.credentials_path.exists():
|
|
87
|
-
raise
|
|
92
|
+
raise CredentialsFileNotFoundError(str(self.credentials_path))
|
|
88
93
|
|
|
89
94
|
try:
|
|
90
|
-
|
|
95
|
+
file_content = self.credentials_path.read_text().strip()
|
|
96
|
+
if not file_content:
|
|
97
|
+
raise CredentialsFileNotFoundError(str(self.credentials_path))
|
|
98
|
+
|
|
99
|
+
credentials_data = json.loads(file_content)
|
|
91
100
|
self._credentials = GoogleTokens.model_validate(credentials_data)
|
|
92
101
|
self.access_token = self._credentials.access_token
|
|
93
102
|
self.project_id = self._credentials.project_id
|
|
@@ -166,6 +175,12 @@ class SepalCredentialMixin:
|
|
|
166
175
|
f"Project: {self.project_id}"
|
|
167
176
|
)
|
|
168
177
|
return
|
|
178
|
+
elif response.status_code == 500:
|
|
179
|
+
self.logger.error(
|
|
180
|
+
"SEPAL API returned 500 error - "
|
|
181
|
+
"credentials not available on server"
|
|
182
|
+
)
|
|
183
|
+
raise SepalCredentialsUnavailableError(500)
|
|
169
184
|
else:
|
|
170
185
|
self.logger.debug(
|
|
171
186
|
f"Attempt {attempt}/{self.max_retries} failed with "
|
|
@@ -191,6 +206,8 @@ class SepalCredentialMixin:
|
|
|
191
206
|
)
|
|
192
207
|
attempt = 0
|
|
193
208
|
|
|
209
|
+
log.debug(f"Credentials path: {self.credentials_path}")
|
|
210
|
+
|
|
194
211
|
while attempt < self.max_retries:
|
|
195
212
|
attempt += 1
|
|
196
213
|
try:
|
|
@@ -214,11 +231,17 @@ class SepalCredentialMixin:
|
|
|
214
231
|
f"Credentials from file are still expired."
|
|
215
232
|
)
|
|
216
233
|
|
|
234
|
+
except CredentialsFileNotFoundError:
|
|
235
|
+
self.logger.error(
|
|
236
|
+
f"Credentials file not found: {self.credentials_path}"
|
|
237
|
+
)
|
|
238
|
+
raise
|
|
217
239
|
except Exception as e:
|
|
218
240
|
self.logger.error(
|
|
219
241
|
f"Attempt {attempt}/{self.max_retries} "
|
|
220
242
|
f"encountered an exception while reading file: {e}"
|
|
221
243
|
)
|
|
244
|
+
raise
|
|
222
245
|
|
|
223
246
|
# Wait before retrying (the file might be updated externally)
|
|
224
247
|
if attempt < self.max_retries:
|
|
@@ -226,11 +249,7 @@ class SepalCredentialMixin:
|
|
|
226
249
|
self.logger.debug(f"Waiting {wait_time} seconds before retry...")
|
|
227
250
|
await asyncio.sleep(wait_time)
|
|
228
251
|
|
|
229
|
-
raise
|
|
230
|
-
f"Failed to retrieve valid credentials from file after "
|
|
231
|
-
f"{self.max_retries} attempts. File may not be automatically "
|
|
232
|
-
f"updated or credentials are permanently expired."
|
|
233
|
-
)
|
|
252
|
+
raise CredentialsFileUnknownError()
|
|
234
253
|
|
|
235
254
|
def set_credentials_sync(self) -> None:
|
|
236
255
|
"""
|
|
@@ -282,6 +301,13 @@ class SepalCredentialMixin:
|
|
|
282
301
|
f"Project: {self.project_id}"
|
|
283
302
|
)
|
|
284
303
|
return
|
|
304
|
+
elif response.status_code == 500:
|
|
305
|
+
self.logger.error(
|
|
306
|
+
"SEPAL API returned 500 error - "
|
|
307
|
+
"credentials not available on server"
|
|
308
|
+
)
|
|
309
|
+
session.close()
|
|
310
|
+
raise SepalCredentialsUnavailableError(500)
|
|
285
311
|
else:
|
|
286
312
|
self.logger.debug(
|
|
287
313
|
f"Attempt {attempt}/{self.max_retries} failed with "
|
|
@@ -336,6 +362,9 @@ class SepalCredentialMixin:
|
|
|
336
362
|
f"Credentials from file are still expired."
|
|
337
363
|
)
|
|
338
364
|
|
|
365
|
+
except CredentialsFileNotFoundError:
|
|
366
|
+
# Re-raise immediately - no point in retrying if file doesn't exist
|
|
367
|
+
raise
|
|
339
368
|
except Exception as e:
|
|
340
369
|
self.logger.error(
|
|
341
370
|
f"Attempt {attempt}/{self.max_retries} "
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ee-client"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "extends the capabilities of the earthengine-api by providing custom session management and client interactions"
|
|
9
9
|
readme = { file = "README.rst", content-type = "text/x-rst" }
|
|
10
10
|
authors = [
|
|
@@ -12,7 +12,7 @@ authors = [
|
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
14
|
"earthengine-api",
|
|
15
|
-
"httpx",
|
|
15
|
+
"httpx[http2]",
|
|
16
16
|
"aiogoogle",
|
|
17
17
|
"tenacity",
|
|
18
18
|
"pydantic",
|
|
@@ -66,7 +66,7 @@ branch = true
|
|
|
66
66
|
[tool.commitizen]
|
|
67
67
|
tag_format = "v$major.$minor.$patch$prerelease"
|
|
68
68
|
update_changelog_on_bump = false
|
|
69
|
-
version = "2.
|
|
69
|
+
version = "2.3.0"
|
|
70
70
|
version_files = [
|
|
71
71
|
"pyproject.toml:version",
|
|
72
72
|
"eeclient/__init__.py:__version__",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from ee.ee_exception import EEException
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class EERestException(EEException):
|
|
5
|
-
def __init__(self, error):
|
|
6
|
-
self.message = error.get("message", "EE responded with an error")
|
|
7
|
-
super().__init__(self.message)
|
|
8
|
-
self.code = error.get("code", -1)
|
|
9
|
-
self.status = error.get("status", "UNDEFINED")
|
|
10
|
-
self.details = error.get("details")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class EEClientError(Exception):
|
|
14
|
-
"""Custom exception class for EEClient errors."""
|
|
15
|
-
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# {'code': 401, 'message': 'Request had invalid authentication credentials.
|
|
20
|
-
# Expected OAuth 2 access token, login cookie or other valid authentication
|
|
21
|
-
# credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.',
|
|
22
|
-
# 'status': 'UNAUTHENTICATED'}
|
|
23
|
-
# Exception in _run_task: Request had invalid authentication credentials.
|
|
24
|
-
# Expected OAuth 2 access token, login cookie or other valid authentication
|
|
25
|
-
# credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
|
|
26
|
-
# when that error happens, I need to re-set the credentials
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|