ee-client 2.2.1__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.1 → ee_client-2.3.0}/PKG-INFO +2 -2
- {ee_client-2.2.1 → ee_client-2.3.0}/ee_client.egg-info/PKG-INFO +2 -2
- {ee_client-2.2.1 → ee_client-2.3.0}/ee_client.egg-info/requires.txt +1 -1
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/__init__.py +1 -1
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/client.py +101 -60
- {ee_client-2.2.1 → ee_client-2.3.0}/pyproject.toml +3 -3
- {ee_client-2.2.1 → ee_client-2.3.0}/LICENSE +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/README.rst +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/ee_client.egg-info/SOURCES.txt +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/ee_client.egg-info/dependency_links.txt +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/ee_client.egg-info/top_level.txt +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/data.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/exceptions.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/export/__init__.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/export/image.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/export/table.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/helpers.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/interfaces/__init__.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/interfaces/export.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/interfaces/operations.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/interfaces/tasks.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/models.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/oauth_app.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/sepal_credential_mixin.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/eeclient/tasks.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/setup.cfg +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/tests/test_client.py +0 -0
- {ee_client-2.2.1 → ee_client-2.3.0}/tests/test_data.py +0 -0
- {ee_client-2.2.1 → 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
|
|
@@ -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__",
|
|
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
|
|
File without changes
|
|
File without changes
|