ee-client 2.1.0__tar.gz → 2.2.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.
Files changed (30) hide show
  1. {ee_client-2.1.0 → ee_client-2.2.0}/PKG-INFO +1 -1
  2. {ee_client-2.1.0 → ee_client-2.2.0}/ee_client.egg-info/PKG-INFO +1 -1
  3. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/__init__.py +1 -1
  4. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/client.py +29 -89
  5. ee_client-2.2.0/eeclient/sepal_credential_mixin.py +355 -0
  6. {ee_client-2.1.0 → ee_client-2.2.0}/pyproject.toml +2 -2
  7. ee_client-2.1.0/eeclient/sepal_credential_mixin.py +0 -37
  8. {ee_client-2.1.0 → ee_client-2.2.0}/LICENSE +0 -0
  9. {ee_client-2.1.0 → ee_client-2.2.0}/README.rst +0 -0
  10. {ee_client-2.1.0 → ee_client-2.2.0}/ee_client.egg-info/SOURCES.txt +0 -0
  11. {ee_client-2.1.0 → ee_client-2.2.0}/ee_client.egg-info/dependency_links.txt +0 -0
  12. {ee_client-2.1.0 → ee_client-2.2.0}/ee_client.egg-info/requires.txt +0 -0
  13. {ee_client-2.1.0 → ee_client-2.2.0}/ee_client.egg-info/top_level.txt +0 -0
  14. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/data.py +0 -0
  15. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/exceptions.py +0 -0
  16. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/export/__init__.py +0 -0
  17. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/export/image.py +0 -0
  18. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/export/table.py +0 -0
  19. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/helpers.py +0 -0
  20. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/interfaces/__init__.py +0 -0
  21. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/interfaces/export.py +0 -0
  22. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/interfaces/operations.py +0 -0
  23. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/interfaces/tasks.py +0 -0
  24. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/models.py +0 -0
  25. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/oauth_app.py +0 -0
  26. {ee_client-2.1.0 → ee_client-2.2.0}/eeclient/tasks.py +0 -0
  27. {ee_client-2.1.0 → ee_client-2.2.0}/setup.cfg +0 -0
  28. {ee_client-2.1.0 → ee_client-2.2.0}/tests/test_client.py +0 -0
  29. {ee_client-2.1.0 → ee_client-2.2.0}/tests/test_data.py +0 -0
  30. {ee_client-2.1.0 → ee_client-2.2.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.1.0
3
+ Version: 2.2.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ee-client
3
- Version: 2.1.0
3
+ Version: 2.2.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
@@ -1,6 +1,6 @@
1
1
  __title__ = "eeclient"
2
2
  __summary__ = "A client for Google Earth Engine"
3
- __version__ = "2.1.0"
3
+ __version__ = "2.2.0"
4
4
 
5
5
  __author__ = "Daniel Guerrero"
6
6
  __email__ = "dfgm2006@gmail.com"
@@ -1,14 +1,13 @@
1
1
  from typing import Any, Dict, Literal, Optional
2
2
 
3
3
  import os
4
- import time
5
4
  import asyncio
6
5
  import httpx
7
6
  import logging
8
7
  from contextlib import asynccontextmanager
9
8
 
10
9
  from eeclient.exceptions import EEClientError, EERestException
11
- from eeclient.models import GEEHeaders, GoogleTokens, SepalHeaders
10
+ from eeclient.models import GEEHeaders, SepalHeaders
12
11
  from eeclient.sepal_credential_mixin import SepalCredentialMixin
13
12
 
14
13
  import eeclient.export as _export_module
@@ -35,7 +34,11 @@ VERIFY_SSL = True
35
34
 
36
35
 
37
36
  class EESession(SepalCredentialMixin):
38
- def __init__(self, sepal_headers: SepalHeaders, enforce_project_id: bool = True):
37
+ def __init__(
38
+ self,
39
+ sepal_headers: Optional[SepalHeaders] = None,
40
+ enforce_project_id: bool = True,
41
+ ):
39
42
  """Session that handles two scenarios to set the headers for Earth Engine API
40
43
 
41
44
  It can be initialized with the headers sent by SEPAL or with the
@@ -49,25 +52,20 @@ class EESession(SepalCredentialMixin):
49
52
  Raises:
50
53
  ValueError: If SEPAL_HOST environment variable is not set
51
54
  """
52
- super().__init__(sepal_headers)
53
-
54
- self.max_retries = 3
55
- self._credentials = None
56
55
  self.enforce_project_id = enforce_project_id
56
+ super().__init__(sepal_headers)
57
57
 
58
58
  # Create user-specific logger
59
59
  self.logger = logging.getLogger(f"eeclient.{self.user}")
60
60
 
61
- # Initialize credentials from the initial tokens
62
- if self._google_tokens:
63
- self._credentials = self._google_tokens
64
- else:
65
- self._credentials = None
66
- self.project_id = None
67
- self.logger.debug(
68
- "No credentials found in initial headers. "
61
+ self.logger.debug(
62
+ "EESession initialized"
63
+ if self._credentials
64
+ else (
65
+ "EESession created without credentials. "
69
66
  "Call initialize() or use create() to fetch credentials."
70
67
  )
68
+ )
71
69
 
72
70
  async def initialize(self) -> "EESession":
73
71
  """Asynchronously initialize the session by fetching credentials if needed.
@@ -83,11 +81,16 @@ class EESession(SepalCredentialMixin):
83
81
  return self
84
82
 
85
83
  @classmethod
86
- async def create(cls, sepal_headers: SepalHeaders, enforce_project_id: bool = True):
84
+ async def create(
85
+ cls,
86
+ sepal_headers: Optional[SepalHeaders] = None,
87
+ enforce_project_id: bool = True,
88
+ ):
87
89
  """Asynchronously create an EESession instance.
88
90
 
89
91
  Args:
90
- sepal_headers (SepalHeaders): The headers sent by SEPAL
92
+ sepal_headers (Optional[SepalHeaders]): The headers sent by SEPAL.
93
+ If None, will use file-based authentication.
91
94
  enforce_project_id (bool, optional): If set, it cannot be changed.
92
95
  Defaults to True.
93
96
 
@@ -102,14 +105,6 @@ class EESession(SepalCredentialMixin):
102
105
  await self.set_credentials()
103
106
  return f"projects/{self.project_id}/assets/"
104
107
 
105
- def is_expired(self) -> bool:
106
- """Returns if a token is about to expire"""
107
- return (self.expiry_date / 1000) - time.time() < 60
108
-
109
- def needs_credentials_refresh(self) -> bool:
110
- """Returns if credentials need to be refreshed (missing or expired)"""
111
- return not self._credentials or self.is_expired()
112
-
113
108
  def get_current_headers(self) -> GEEHeaders:
114
109
  """Get current headers without refreshing credentials"""
115
110
  if not self._credentials:
@@ -117,10 +112,17 @@ class EESession(SepalCredentialMixin):
117
112
 
118
113
  self.logger.debug(f"Getting headers with project id: {self.project_id}")
119
114
 
115
+ # Get username based on authentication mode
116
+ username = (
117
+ self.sepal_user_data.username
118
+ if hasattr(self, "sepal_user_data") and self.sepal_user_data
119
+ else self.user
120
+ )
121
+
120
122
  data = {
121
123
  "x-goog-user-project": self.project_id,
122
124
  "Authorization": f"Bearer {self._credentials.access_token}",
123
- "Username": self.sepal_user_data.username,
125
+ "Username": username,
124
126
  }
125
127
 
126
128
  return GEEHeaders.model_validate(data)
@@ -149,68 +151,6 @@ class EESession(SepalCredentialMixin):
149
151
  finally:
150
152
  await async_client.aclose()
151
153
 
152
- async def set_credentials(self) -> None:
153
- """
154
- Refresh credentials asynchronously.
155
- Uses its own HTTP client (thus bypassing get_headers) to avoid recursion.
156
- """
157
- self.logger.debug(
158
- "Token is expired or about to expire; attempting to refresh credentials."
159
- )
160
- attempt = 0
161
- credentials_url = self.sepal_api_download_url
162
-
163
- # Prepare cookies for authentication.
164
- sepal_cookies = httpx.Cookies()
165
- sepal_cookies.set("SEPAL-SESSIONID", self.sepal_session_id)
166
-
167
- last_status = None
168
-
169
- while attempt < self.max_retries:
170
- attempt += 1
171
- try:
172
- async with httpx.AsyncClient(
173
- cookies=sepal_cookies,
174
- verify=self.verify_ssl,
175
- limits=httpx.Limits(
176
- max_connections=100, max_keepalive_connections=50
177
- ),
178
- ) as client:
179
- self.logger.debug(f"Attempt {attempt} to refresh credentials.")
180
- response = await client.get(credentials_url)
181
-
182
- last_status = response.status_code
183
-
184
- if response.status_code == 200:
185
- self._credentials = GoogleTokens.model_validate(response.json())
186
- self.expiry_date = self._credentials.access_token_expiry_date
187
- self.project_id = (
188
- self._credentials.project_id
189
- if self.enforce_project_id
190
- else self.project_id
191
- )
192
- self.logger.debug(
193
- f"Successfully refreshed credentials "
194
- f"!{self._credentials}==================. {self.project_id}"
195
- )
196
- return
197
- else:
198
- self.logger.debug(
199
- f"Attempt {attempt}/{self.max_retries} failed with "
200
- f"status code: {response.status_code}."
201
- )
202
- except Exception as e:
203
- self.logger.error(
204
- f"Attempt {attempt}/{self.max_retries} "
205
- f"encountered an exception: {e}"
206
- )
207
- await asyncio.sleep(2**attempt) # Exponential backoff
208
-
209
- raise ValueError(
210
- f"Failed to retrieve credentials after {self.max_retries} attempts, "
211
- f"last status code: {last_status}"
212
- )
213
-
214
154
  async def rest_call(
215
155
  self,
216
156
  method: Literal["GET", "POST", "DELETE"],
@@ -343,7 +283,7 @@ class EESession(SepalCredentialMixin):
343
283
  last_error = e
344
284
  error_type = type(e).__name__
345
285
  self.logger.error(
346
- f"Unexpected error in rest_call ({error_type}): " f"{str(e)}"
286
+ f"Unexpected error in rest_call ({error_type}): {str(e)}"
347
287
  )
348
288
 
349
289
  import traceback
@@ -0,0 +1,355 @@
1
+ from eeclient.models import SepalHeaders, GoogleTokens
2
+ import os
3
+ import logging
4
+ import json
5
+ import time
6
+ import asyncio
7
+ import httpx
8
+ import requests
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ log = logging.getLogger("eeclient")
13
+
14
+
15
+ class SepalCredentialMixin:
16
+ def __init__(self, sepal_headers: Optional[SepalHeaders] = None):
17
+ log.debug("Initializing SepalCredentialMixin")
18
+
19
+ self.max_retries = 3
20
+ self._credentials = None
21
+ self.auth_mode = "sepal" if sepal_headers else "file"
22
+
23
+ if sepal_headers:
24
+ self._init_sepal_mode(sepal_headers)
25
+ else:
26
+ self._init_file_mode()
27
+
28
+ self.logger = logging.getLogger(f"eeclient.{self.user}")
29
+
30
+ # For backward compatibility (some code might expect this attribute)
31
+ self._service = None
32
+
33
+ def _init_sepal_mode(self, sepal_headers: SepalHeaders):
34
+ """Initialize SEPAL authentication mode"""
35
+ self.sepal_host = os.getenv("SEPAL_HOST")
36
+ if not self.sepal_host:
37
+ raise ValueError("SEPAL_HOST environment variable not set")
38
+
39
+ self.sepal_headers = SepalHeaders.model_validate(sepal_headers)
40
+ self.sepal_session_id = self.sepal_headers.cookies["SEPAL-SESSIONID"]
41
+ self.sepal_user_data = self.sepal_headers.sepal_user
42
+ self.user = self.sepal_user_data.username
43
+
44
+ self.sepal_api_download_url = (
45
+ f"https://{self.sepal_host}/api/user-files/download/"
46
+ "?path=%2F.config%2Fearthengine%2Fcredentials"
47
+ )
48
+ self.verify_ssl = not (
49
+ self.sepal_host == "host.docker.internal"
50
+ or self.sepal_host == "danielg.sepal.io"
51
+ )
52
+
53
+ self._google_tokens = self.sepal_user_data.google_tokens
54
+ if self._google_tokens:
55
+ self._credentials = self._google_tokens
56
+ self.access_token = self._google_tokens.access_token
57
+ self.project_id = self._google_tokens.project_id
58
+ self.expiry_date = self._google_tokens.access_token_expiry_date
59
+ else:
60
+ self.access_token = None
61
+ self.project_id = None
62
+ self.expiry_date = 0
63
+
64
+ def _init_file_mode(self):
65
+ """Initialize file-based authentication mode"""
66
+ home_path = Path.home()
67
+ credentials_file = (
68
+ ".config/earthengine/credentials"
69
+ if "sepal-user" in home_path.name
70
+ else ".config/earthengine/sepal_credentials"
71
+ )
72
+ self.credentials_path = home_path / credentials_file
73
+ self.user = "local_user"
74
+
75
+ # No SEPAL session info for file-based credentials
76
+ self.sepal_session_id = None
77
+ self.sepal_host = None
78
+ self.sepal_api_download_url = None
79
+ self.verify_ssl = True
80
+
81
+ # Load initial credentials
82
+ self._load_credentials_from_file()
83
+
84
+ def _load_credentials_from_file(self):
85
+ """Load credentials from file and update internal state"""
86
+ if not self.credentials_path.exists():
87
+ raise ValueError(f"Credentials file not found at {self.credentials_path}")
88
+
89
+ try:
90
+ credentials_data = json.loads(self.credentials_path.read_text())
91
+ self._credentials = GoogleTokens.model_validate(credentials_data)
92
+ self.access_token = self._credentials.access_token
93
+ self.project_id = self._credentials.project_id
94
+ self.expiry_date = self._credentials.access_token_expiry_date
95
+
96
+ if not self.access_token:
97
+ raise ValueError("No access token available in credentials file")
98
+
99
+ except json.JSONDecodeError as e:
100
+ raise ValueError(f"Invalid JSON in credentials file: {e}")
101
+ except Exception as e:
102
+ raise ValueError(f"Error loading credentials from file: {e}")
103
+
104
+ def is_expired(self) -> bool:
105
+ """Returns if a token is about to expire"""
106
+ return (self.expiry_date / 1000) - time.time() < 60
107
+
108
+ def needs_credentials_refresh(self) -> bool:
109
+ """Returns if credentials need to be refreshed (missing or expired)"""
110
+ return not self._credentials or self.is_expired()
111
+
112
+ async def set_credentials(self) -> None:
113
+ """
114
+ Refresh credentials based on the authentication mode.
115
+ """
116
+ if self.auth_mode == "sepal":
117
+ await self._refresh_credentials_from_sepal()
118
+ else:
119
+ await self._refresh_credentials_from_file()
120
+
121
+ async def _refresh_credentials_from_sepal(self) -> None:
122
+ """Refresh credentials via SEPAL API"""
123
+ if not self.sepal_session_id or not self.sepal_api_download_url:
124
+ raise ValueError("SEPAL session information not available")
125
+
126
+ self.logger.debug(
127
+ "Token is expired or about to expire; "
128
+ "attempting to refresh credentials from SEPAL."
129
+ )
130
+ attempt = 0
131
+ credentials_url = self.sepal_api_download_url
132
+ last_status = None
133
+
134
+ # Prepare cookies for authentication
135
+ sepal_cookies = httpx.Cookies()
136
+ sepal_cookies.set("SEPAL-SESSIONID", self.sepal_session_id)
137
+
138
+ while attempt < self.max_retries:
139
+ attempt += 1
140
+ try:
141
+ async with httpx.AsyncClient(
142
+ cookies=sepal_cookies,
143
+ verify=self.verify_ssl,
144
+ limits=httpx.Limits(
145
+ max_connections=100, max_keepalive_connections=50
146
+ ),
147
+ ) as client:
148
+ self.logger.debug(
149
+ f"Attempt {attempt} to refresh credentials from SEPAL."
150
+ )
151
+ response = await client.get(credentials_url)
152
+
153
+ last_status = response.status_code
154
+
155
+ if response.status_code == 200:
156
+ self._credentials = GoogleTokens.model_validate(response.json())
157
+ self.expiry_date = self._credentials.access_token_expiry_date
158
+ self.access_token = self._credentials.access_token
159
+ # Don't override project_id if enforce_project_id is set
160
+ if not hasattr(self, "enforce_project_id") or not getattr(
161
+ self, "enforce_project_id", False
162
+ ):
163
+ self.project_id = self._credentials.project_id
164
+ self.logger.debug(
165
+ f"Successfully refreshed credentials from SEPAL. "
166
+ f"Project: {self.project_id}"
167
+ )
168
+ return
169
+ else:
170
+ self.logger.debug(
171
+ f"Attempt {attempt}/{self.max_retries} failed with "
172
+ f"status code: {response.status_code}."
173
+ )
174
+ except Exception as e:
175
+ self.logger.error(
176
+ f"Attempt {attempt}/{self.max_retries} "
177
+ f"encountered an exception: {e}"
178
+ )
179
+ await asyncio.sleep(2**attempt) # Exponential backoff
180
+
181
+ raise ValueError(
182
+ f"Failed to retrieve credentials from SEPAL after "
183
+ f"{self.max_retries} attempts, last status code: {last_status}"
184
+ )
185
+
186
+ async def _refresh_credentials_from_file(self) -> None:
187
+ """Refresh credentials by re-reading from file"""
188
+ self.logger.debug(
189
+ "Token is expired or about to expire; "
190
+ "attempting to refresh credentials from file."
191
+ )
192
+ attempt = 0
193
+
194
+ while attempt < self.max_retries:
195
+ attempt += 1
196
+ try:
197
+ self.logger.debug(
198
+ f"Attempt {attempt} to refresh credentials from file."
199
+ )
200
+
201
+ # Re-read credentials from file
202
+ self._load_credentials_from_file()
203
+
204
+ # Check if the new credentials are still expired
205
+ if not self.is_expired():
206
+ self.logger.debug(
207
+ f"Successfully refreshed credentials from file. "
208
+ f"Project: {self.project_id}"
209
+ )
210
+ return
211
+ else:
212
+ self.logger.debug(
213
+ f"Attempt {attempt}/{self.max_retries}: "
214
+ f"Credentials from file are still expired."
215
+ )
216
+
217
+ except Exception as e:
218
+ self.logger.error(
219
+ f"Attempt {attempt}/{self.max_retries} "
220
+ f"encountered an exception while reading file: {e}"
221
+ )
222
+
223
+ # Wait before retrying (the file might be updated externally)
224
+ if attempt < self.max_retries:
225
+ wait_time = 2**attempt
226
+ self.logger.debug(f"Waiting {wait_time} seconds before retry...")
227
+ await asyncio.sleep(wait_time)
228
+
229
+ raise ValueError(
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
+ )
234
+
235
+ def set_credentials_sync(self) -> None:
236
+ """
237
+ Refresh credentials synchronously based on the authentication mode.
238
+ """
239
+ if self.auth_mode == "sepal":
240
+ self._refresh_credentials_from_sepal_sync()
241
+ else:
242
+ self._refresh_credentials_from_file_sync()
243
+
244
+ def _refresh_credentials_from_sepal_sync(self) -> None:
245
+ """Refresh credentials via SEPAL API synchronously"""
246
+ if not self.sepal_session_id or not self.sepal_api_download_url:
247
+ raise ValueError("SEPAL session information not available")
248
+
249
+ self.logger.debug(
250
+ "Token is expired or about to expire; "
251
+ "attempting to refresh credentials from SEPAL (sync)."
252
+ )
253
+ attempt = 0
254
+ credentials_url = self.sepal_api_download_url
255
+ last_status = None
256
+
257
+ # Prepare session with cookies for authentication
258
+ session = requests.Session()
259
+ session.cookies.set("SEPAL-SESSIONID", self.sepal_session_id)
260
+ session.verify = self.verify_ssl
261
+
262
+ while attempt < self.max_retries:
263
+ attempt += 1
264
+ try:
265
+ self.logger.debug(
266
+ f"Attempt {attempt} to refresh credentials from SEPAL (sync)."
267
+ )
268
+ response = session.get(credentials_url)
269
+ last_status = response.status_code
270
+
271
+ if response.status_code == 200:
272
+ self._credentials = GoogleTokens.model_validate(response.json())
273
+ self.expiry_date = self._credentials.access_token_expiry_date
274
+ self.access_token = self._credentials.access_token
275
+ # Don't override project_id if enforce_project_id is set
276
+ if not hasattr(self, "enforce_project_id") or not getattr(
277
+ self, "enforce_project_id", False
278
+ ):
279
+ self.project_id = self._credentials.project_id
280
+ self.logger.debug(
281
+ f"Successfully refreshed credentials from SEPAL (sync). "
282
+ f"Project: {self.project_id}"
283
+ )
284
+ return
285
+ else:
286
+ self.logger.debug(
287
+ f"Attempt {attempt}/{self.max_retries} failed with "
288
+ f"status code: {response.status_code}."
289
+ )
290
+ except Exception as e:
291
+ self.logger.error(
292
+ f"Attempt {attempt}/{self.max_retries} "
293
+ f"encountered an exception: {e}"
294
+ )
295
+
296
+ # Wait before retrying (exponential backoff)
297
+ if attempt < self.max_retries:
298
+ wait_time = 2**attempt
299
+ self.logger.debug(f"Waiting {wait_time} seconds before retry...")
300
+ time.sleep(wait_time)
301
+
302
+ session.close()
303
+ raise ValueError(
304
+ f"Failed to retrieve credentials from SEPAL after "
305
+ f"{self.max_retries} attempts, last status code: {last_status}"
306
+ )
307
+
308
+ def _refresh_credentials_from_file_sync(self) -> None:
309
+ """Refresh credentials by re-reading from file synchronously"""
310
+ self.logger.debug(
311
+ "Token is expired or about to expire; "
312
+ "attempting to refresh credentials from file (sync)."
313
+ )
314
+ attempt = 0
315
+
316
+ while attempt < self.max_retries:
317
+ attempt += 1
318
+ try:
319
+ self.logger.debug(
320
+ f"Attempt {attempt} to refresh credentials from file (sync)."
321
+ )
322
+
323
+ # Re-read credentials from file
324
+ self._load_credentials_from_file()
325
+
326
+ # Check if the new credentials are still expired
327
+ if not self.is_expired():
328
+ self.logger.debug(
329
+ f"Successfully refreshed credentials from file (sync). "
330
+ f"Project: {self.project_id}"
331
+ )
332
+ return
333
+ else:
334
+ self.logger.debug(
335
+ f"Attempt {attempt}/{self.max_retries}: "
336
+ f"Credentials from file are still expired."
337
+ )
338
+
339
+ except Exception as e:
340
+ self.logger.error(
341
+ f"Attempt {attempt}/{self.max_retries} "
342
+ f"encountered an exception while reading file: {e}"
343
+ )
344
+
345
+ # Wait before retrying (the file might be updated externally)
346
+ if attempt < self.max_retries:
347
+ wait_time = 2**attempt
348
+ self.logger.debug(f"Waiting {wait_time} seconds before retry...")
349
+ time.sleep(wait_time)
350
+
351
+ raise ValueError(
352
+ f"Failed to retrieve valid credentials from file after "
353
+ f"{self.max_retries} attempts. File may not be automatically "
354
+ f"updated or credentials are permanently expired."
355
+ )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ee-client"
7
- version = "2.1.0"
7
+ version = "2.2.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 = [
@@ -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.1.0"
69
+ version = "2.2.0"
70
70
  version_files = [
71
71
  "pyproject.toml:version",
72
72
  "eeclient/__init__.py:__version__",
@@ -1,37 +0,0 @@
1
- from eeclient.models import SepalHeaders
2
- import os
3
- import logging
4
-
5
- log = logging.getLogger("eeclient")
6
-
7
-
8
- class SepalCredentialMixin:
9
- def __init__(self, sepal_headers: SepalHeaders):
10
- log.debug("Initializing GDriveInterface with SEPAL headers")
11
- self.sepal_host = os.getenv("SEPAL_HOST")
12
- if not self.sepal_host:
13
- raise ValueError("SEPAL_HOST environment variable not set")
14
-
15
- self.sepal_headers = SepalHeaders.model_validate(sepal_headers)
16
- self.sepal_session_id = self.sepal_headers.cookies["SEPAL-SESSIONID"]
17
- self.sepal_user_data = self.sepal_headers.sepal_user
18
- self.user = self.sepal_user_data.username
19
-
20
- self.sepal_api_download_url = (
21
- f"https://{self.sepal_host}/api/user-files/download/"
22
- "?path=%2F.config%2Fearthengine%2Fcredentials"
23
- )
24
- self.verify_ssl = not (
25
- self.sepal_host == "host.docker.internal"
26
- or self.sepal_host == "danielg.sepal.io"
27
- )
28
-
29
- self._google_tokens = self.sepal_user_data.google_tokens
30
- if self._google_tokens:
31
- self.access_token = self._google_tokens.access_token
32
- self.project_id = self._google_tokens.project_id
33
- self.expiry_date = self._google_tokens.access_token_expiry_date
34
- else:
35
- self.access_token = None
36
- self.project_id = None
37
- self.expiry_date = 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes