ee-client 2.1.0__tar.gz → 2.2.1__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 (31) hide show
  1. {ee_client-2.1.0 → ee_client-2.2.1}/PKG-INFO +1 -1
  2. {ee_client-2.1.0 → ee_client-2.2.1}/ee_client.egg-info/PKG-INFO +1 -1
  3. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/__init__.py +1 -1
  4. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/client.py +29 -89
  5. ee_client-2.2.1/eeclient/exceptions.py +100 -0
  6. ee_client-2.2.1/eeclient/sepal_credential_mixin.py +384 -0
  7. {ee_client-2.1.0 → ee_client-2.2.1}/pyproject.toml +2 -2
  8. ee_client-2.1.0/eeclient/exceptions.py +0 -26
  9. ee_client-2.1.0/eeclient/sepal_credential_mixin.py +0 -37
  10. {ee_client-2.1.0 → ee_client-2.2.1}/LICENSE +0 -0
  11. {ee_client-2.1.0 → ee_client-2.2.1}/README.rst +0 -0
  12. {ee_client-2.1.0 → ee_client-2.2.1}/ee_client.egg-info/SOURCES.txt +0 -0
  13. {ee_client-2.1.0 → ee_client-2.2.1}/ee_client.egg-info/dependency_links.txt +0 -0
  14. {ee_client-2.1.0 → ee_client-2.2.1}/ee_client.egg-info/requires.txt +0 -0
  15. {ee_client-2.1.0 → ee_client-2.2.1}/ee_client.egg-info/top_level.txt +0 -0
  16. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/data.py +0 -0
  17. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/export/__init__.py +0 -0
  18. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/export/image.py +0 -0
  19. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/export/table.py +0 -0
  20. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/helpers.py +0 -0
  21. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/interfaces/__init__.py +0 -0
  22. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/interfaces/export.py +0 -0
  23. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/interfaces/operations.py +0 -0
  24. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/interfaces/tasks.py +0 -0
  25. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/models.py +0 -0
  26. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/oauth_app.py +0 -0
  27. {ee_client-2.1.0 → ee_client-2.2.1}/eeclient/tasks.py +0 -0
  28. {ee_client-2.1.0 → ee_client-2.2.1}/setup.cfg +0 -0
  29. {ee_client-2.1.0 → ee_client-2.2.1}/tests/test_client.py +0 -0
  30. {ee_client-2.1.0 → ee_client-2.2.1}/tests/test_data.py +0 -0
  31. {ee_client-2.1.0 → ee_client-2.2.1}/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.1
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.1
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.1"
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,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
@@ -0,0 +1,384 @@
1
+ from eeclient.models import SepalHeaders, GoogleTokens
2
+ from eeclient.exceptions import (
3
+ CredentialsFileNotFoundError,
4
+ CredentialsFileUnknownError,
5
+ SepalCredentialsUnavailableError,
6
+ )
7
+ import os
8
+ import logging
9
+ import json
10
+ import time
11
+ import asyncio
12
+ import httpx
13
+ import requests
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ log = logging.getLogger("eeclient")
18
+
19
+
20
+ class SepalCredentialMixin:
21
+ def __init__(self, sepal_headers: Optional[SepalHeaders] = None):
22
+ log.debug("Initializing SepalCredentialMixin")
23
+
24
+ self.max_retries = 3
25
+ self._credentials = None
26
+ self.auth_mode = "sepal" if sepal_headers else "file"
27
+
28
+ if sepal_headers:
29
+ self._init_sepal_mode(sepal_headers)
30
+ else:
31
+ self._init_file_mode()
32
+
33
+ self.logger = logging.getLogger(f"eeclient.{self.user}")
34
+
35
+ # For backward compatibility (some code might expect this attribute)
36
+ self._service = None
37
+
38
+ def _init_sepal_mode(self, sepal_headers: SepalHeaders):
39
+ """Initialize SEPAL authentication mode"""
40
+ self.sepal_host = os.getenv("SEPAL_HOST")
41
+ if not self.sepal_host:
42
+ raise ValueError("SEPAL_HOST environment variable not set")
43
+
44
+ self.sepal_headers = SepalHeaders.model_validate(sepal_headers)
45
+ self.sepal_session_id = self.sepal_headers.cookies["SEPAL-SESSIONID"]
46
+ self.sepal_user_data = self.sepal_headers.sepal_user
47
+ self.user = self.sepal_user_data.username
48
+
49
+ self.sepal_api_download_url = (
50
+ f"https://{self.sepal_host}/api/user-files/download/"
51
+ "?path=%2F.config%2Fearthengine%2Fcredentials"
52
+ )
53
+ self.verify_ssl = not (
54
+ self.sepal_host == "host.docker.internal"
55
+ or self.sepal_host == "danielg.sepal.io"
56
+ )
57
+
58
+ self._google_tokens = self.sepal_user_data.google_tokens
59
+ if self._google_tokens:
60
+ self._credentials = self._google_tokens
61
+ self.access_token = self._google_tokens.access_token
62
+ self.project_id = self._google_tokens.project_id
63
+ self.expiry_date = self._google_tokens.access_token_expiry_date
64
+ else:
65
+ self.access_token = None
66
+ self.project_id = None
67
+ self.expiry_date = 0
68
+
69
+ def _init_file_mode(self):
70
+ """Initialize file-based authentication mode"""
71
+ home_path = Path.home()
72
+ credentials_file = (
73
+ ".config/earthengine/credentials"
74
+ if "sepal-user" in home_path.name
75
+ else ".config/earthengine/sepal_credentials"
76
+ )
77
+ self.credentials_path = home_path / credentials_file
78
+ self.user = "local_user"
79
+
80
+ # No SEPAL session info for file-based credentials
81
+ self.sepal_session_id = None
82
+ self.sepal_host = None
83
+ self.sepal_api_download_url = None
84
+ self.verify_ssl = True
85
+
86
+ # Load initial credentials
87
+ self._load_credentials_from_file()
88
+
89
+ def _load_credentials_from_file(self):
90
+ """Load credentials from file and update internal state"""
91
+ if not self.credentials_path.exists():
92
+ raise CredentialsFileNotFoundError(str(self.credentials_path))
93
+
94
+ try:
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)
100
+ self._credentials = GoogleTokens.model_validate(credentials_data)
101
+ self.access_token = self._credentials.access_token
102
+ self.project_id = self._credentials.project_id
103
+ self.expiry_date = self._credentials.access_token_expiry_date
104
+
105
+ if not self.access_token:
106
+ raise ValueError("No access token available in credentials file")
107
+
108
+ except json.JSONDecodeError as e:
109
+ raise ValueError(f"Invalid JSON in credentials file: {e}")
110
+ except Exception as e:
111
+ raise ValueError(f"Error loading credentials from file: {e}")
112
+
113
+ def is_expired(self) -> bool:
114
+ """Returns if a token is about to expire"""
115
+ return (self.expiry_date / 1000) - time.time() < 60
116
+
117
+ def needs_credentials_refresh(self) -> bool:
118
+ """Returns if credentials need to be refreshed (missing or expired)"""
119
+ return not self._credentials or self.is_expired()
120
+
121
+ async def set_credentials(self) -> None:
122
+ """
123
+ Refresh credentials based on the authentication mode.
124
+ """
125
+ if self.auth_mode == "sepal":
126
+ await self._refresh_credentials_from_sepal()
127
+ else:
128
+ await self._refresh_credentials_from_file()
129
+
130
+ async def _refresh_credentials_from_sepal(self) -> None:
131
+ """Refresh credentials via SEPAL API"""
132
+ if not self.sepal_session_id or not self.sepal_api_download_url:
133
+ raise ValueError("SEPAL session information not available")
134
+
135
+ self.logger.debug(
136
+ "Token is expired or about to expire; "
137
+ "attempting to refresh credentials from SEPAL."
138
+ )
139
+ attempt = 0
140
+ credentials_url = self.sepal_api_download_url
141
+ last_status = None
142
+
143
+ # Prepare cookies for authentication
144
+ sepal_cookies = httpx.Cookies()
145
+ sepal_cookies.set("SEPAL-SESSIONID", self.sepal_session_id)
146
+
147
+ while attempt < self.max_retries:
148
+ attempt += 1
149
+ try:
150
+ async with httpx.AsyncClient(
151
+ cookies=sepal_cookies,
152
+ verify=self.verify_ssl,
153
+ limits=httpx.Limits(
154
+ max_connections=100, max_keepalive_connections=50
155
+ ),
156
+ ) as client:
157
+ self.logger.debug(
158
+ f"Attempt {attempt} to refresh credentials from SEPAL."
159
+ )
160
+ response = await client.get(credentials_url)
161
+
162
+ last_status = response.status_code
163
+
164
+ if response.status_code == 200:
165
+ self._credentials = GoogleTokens.model_validate(response.json())
166
+ self.expiry_date = self._credentials.access_token_expiry_date
167
+ self.access_token = self._credentials.access_token
168
+ # Don't override project_id if enforce_project_id is set
169
+ if not hasattr(self, "enforce_project_id") or not getattr(
170
+ self, "enforce_project_id", False
171
+ ):
172
+ self.project_id = self._credentials.project_id
173
+ self.logger.debug(
174
+ f"Successfully refreshed credentials from SEPAL. "
175
+ f"Project: {self.project_id}"
176
+ )
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)
184
+ else:
185
+ self.logger.debug(
186
+ f"Attempt {attempt}/{self.max_retries} failed with "
187
+ f"status code: {response.status_code}."
188
+ )
189
+ except Exception as e:
190
+ self.logger.error(
191
+ f"Attempt {attempt}/{self.max_retries} "
192
+ f"encountered an exception: {e}"
193
+ )
194
+ await asyncio.sleep(2**attempt) # Exponential backoff
195
+
196
+ raise ValueError(
197
+ f"Failed to retrieve credentials from SEPAL after "
198
+ f"{self.max_retries} attempts, last status code: {last_status}"
199
+ )
200
+
201
+ async def _refresh_credentials_from_file(self) -> None:
202
+ """Refresh credentials by re-reading from file"""
203
+ self.logger.debug(
204
+ "Token is expired or about to expire; "
205
+ "attempting to refresh credentials from file."
206
+ )
207
+ attempt = 0
208
+
209
+ log.debug(f"Credentials path: {self.credentials_path}")
210
+
211
+ while attempt < self.max_retries:
212
+ attempt += 1
213
+ try:
214
+ self.logger.debug(
215
+ f"Attempt {attempt} to refresh credentials from file."
216
+ )
217
+
218
+ # Re-read credentials from file
219
+ self._load_credentials_from_file()
220
+
221
+ # Check if the new credentials are still expired
222
+ if not self.is_expired():
223
+ self.logger.debug(
224
+ f"Successfully refreshed credentials from file. "
225
+ f"Project: {self.project_id}"
226
+ )
227
+ return
228
+ else:
229
+ self.logger.debug(
230
+ f"Attempt {attempt}/{self.max_retries}: "
231
+ f"Credentials from file are still expired."
232
+ )
233
+
234
+ except CredentialsFileNotFoundError:
235
+ self.logger.error(
236
+ f"Credentials file not found: {self.credentials_path}"
237
+ )
238
+ raise
239
+ except Exception as e:
240
+ self.logger.error(
241
+ f"Attempt {attempt}/{self.max_retries} "
242
+ f"encountered an exception while reading file: {e}"
243
+ )
244
+ raise
245
+
246
+ # Wait before retrying (the file might be updated externally)
247
+ if attempt < self.max_retries:
248
+ wait_time = 2**attempt
249
+ self.logger.debug(f"Waiting {wait_time} seconds before retry...")
250
+ await asyncio.sleep(wait_time)
251
+
252
+ raise CredentialsFileUnknownError()
253
+
254
+ def set_credentials_sync(self) -> None:
255
+ """
256
+ Refresh credentials synchronously based on the authentication mode.
257
+ """
258
+ if self.auth_mode == "sepal":
259
+ self._refresh_credentials_from_sepal_sync()
260
+ else:
261
+ self._refresh_credentials_from_file_sync()
262
+
263
+ def _refresh_credentials_from_sepal_sync(self) -> None:
264
+ """Refresh credentials via SEPAL API synchronously"""
265
+ if not self.sepal_session_id or not self.sepal_api_download_url:
266
+ raise ValueError("SEPAL session information not available")
267
+
268
+ self.logger.debug(
269
+ "Token is expired or about to expire; "
270
+ "attempting to refresh credentials from SEPAL (sync)."
271
+ )
272
+ attempt = 0
273
+ credentials_url = self.sepal_api_download_url
274
+ last_status = None
275
+
276
+ # Prepare session with cookies for authentication
277
+ session = requests.Session()
278
+ session.cookies.set("SEPAL-SESSIONID", self.sepal_session_id)
279
+ session.verify = self.verify_ssl
280
+
281
+ while attempt < self.max_retries:
282
+ attempt += 1
283
+ try:
284
+ self.logger.debug(
285
+ f"Attempt {attempt} to refresh credentials from SEPAL (sync)."
286
+ )
287
+ response = session.get(credentials_url)
288
+ last_status = response.status_code
289
+
290
+ if response.status_code == 200:
291
+ self._credentials = GoogleTokens.model_validate(response.json())
292
+ self.expiry_date = self._credentials.access_token_expiry_date
293
+ self.access_token = self._credentials.access_token
294
+ # Don't override project_id if enforce_project_id is set
295
+ if not hasattr(self, "enforce_project_id") or not getattr(
296
+ self, "enforce_project_id", False
297
+ ):
298
+ self.project_id = self._credentials.project_id
299
+ self.logger.debug(
300
+ f"Successfully refreshed credentials from SEPAL (sync). "
301
+ f"Project: {self.project_id}"
302
+ )
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)
311
+ else:
312
+ self.logger.debug(
313
+ f"Attempt {attempt}/{self.max_retries} failed with "
314
+ f"status code: {response.status_code}."
315
+ )
316
+ except Exception as e:
317
+ self.logger.error(
318
+ f"Attempt {attempt}/{self.max_retries} "
319
+ f"encountered an exception: {e}"
320
+ )
321
+
322
+ # Wait before retrying (exponential backoff)
323
+ if attempt < self.max_retries:
324
+ wait_time = 2**attempt
325
+ self.logger.debug(f"Waiting {wait_time} seconds before retry...")
326
+ time.sleep(wait_time)
327
+
328
+ session.close()
329
+ raise ValueError(
330
+ f"Failed to retrieve credentials from SEPAL after "
331
+ f"{self.max_retries} attempts, last status code: {last_status}"
332
+ )
333
+
334
+ def _refresh_credentials_from_file_sync(self) -> None:
335
+ """Refresh credentials by re-reading from file synchronously"""
336
+ self.logger.debug(
337
+ "Token is expired or about to expire; "
338
+ "attempting to refresh credentials from file (sync)."
339
+ )
340
+ attempt = 0
341
+
342
+ while attempt < self.max_retries:
343
+ attempt += 1
344
+ try:
345
+ self.logger.debug(
346
+ f"Attempt {attempt} to refresh credentials from file (sync)."
347
+ )
348
+
349
+ # Re-read credentials from file
350
+ self._load_credentials_from_file()
351
+
352
+ # Check if the new credentials are still expired
353
+ if not self.is_expired():
354
+ self.logger.debug(
355
+ f"Successfully refreshed credentials from file (sync). "
356
+ f"Project: {self.project_id}"
357
+ )
358
+ return
359
+ else:
360
+ self.logger.debug(
361
+ f"Attempt {attempt}/{self.max_retries}: "
362
+ f"Credentials from file are still expired."
363
+ )
364
+
365
+ except CredentialsFileNotFoundError:
366
+ # Re-raise immediately - no point in retrying if file doesn't exist
367
+ raise
368
+ except Exception as e:
369
+ self.logger.error(
370
+ f"Attempt {attempt}/{self.max_retries} "
371
+ f"encountered an exception while reading file: {e}"
372
+ )
373
+
374
+ # Wait before retrying (the file might be updated externally)
375
+ if attempt < self.max_retries:
376
+ wait_time = 2**attempt
377
+ self.logger.debug(f"Waiting {wait_time} seconds before retry...")
378
+ time.sleep(wait_time)
379
+
380
+ raise ValueError(
381
+ f"Failed to retrieve valid credentials from file after "
382
+ f"{self.max_retries} attempts. File may not be automatically "
383
+ f"updated or credentials are permanently expired."
384
+ )
@@ -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.1"
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.1"
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
@@ -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