Skip to content

Commit 2d0174c

Browse files
authored
refactor: update gee auth process (#915)
2 parents 2d13970 + 1299db1 commit 2d0174c

File tree

12 files changed

+231
-49
lines changed

12 files changed

+231
-49
lines changed

.github/workflows/unit.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ on:
99
env:
1010
PLANET_API_CREDENTIALS: ${{ secrets.PLANET_API_CREDENTIALS }}
1111
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
12-
EARTHENGINE_TOKEN: ${{ secrets.EARTHENGINE_TOKEN }}
12+
EARTHENGINE_TOKEN: ${{ secrets.EARTHENGINE_SERVICE_ACCOUNT }}
13+
EARTHENGINE_SERVICE_ACCOUNT: ${{ secrets.EARTHENGINE_SERVICE_ACCOUNT }}
14+
EARTHENGINE_PROJECT: ${{ secrets.EARTHENGINE_PROJECT }}
1315

1416
jobs:
1517
lint:

docs/source/tutorials/decorator.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Let's import the required modules. All the decorators are stored in the utils mo
7878

7979
.. code:: python
8080
81+
import ee
8182
from time import sleep
8283
import ipyvuetify as v
8384
import sepal_ui.sepalwidgets as sw
@@ -142,7 +143,7 @@ It's time to use the decorators in the class methods. For this example, we will
142143
def request_items(self):
143144
"""Connect to gee and request the root assets id's"""
144145
145-
folder = ee.data.getAssetRoots()[0]["id"]
146+
folder = f"projects/{ee.data._cloud_api_user_project}/assets"
146147
return [
147148
asset["id"]
148149
for asset

sepal_ui/aoi/aoi_model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ def __init__(
154154
self.gee = gee
155155
if gee:
156156
su.init_ee()
157-
self.folder = str(folder) or ee.data.getAssetRoots()[0]["id"]
157+
self.folder = (
158+
str(folder) or f"projects/{ee.data._cloud_api_user_project}/assets/"
159+
)
158160

159161
# set default values
160162
self.set_default(vector, admin, asset)

sepal_ui/message/en/locale.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"custom": "Custom",
4343
"no_access": "It seems like you do not have access to the input asset or it does not exist.",
4444
"wrong_type": "The type of the selected asset ({}) does not match authorized asset type ({}).",
45-
"placeholder": "users/custom_user/custom_asset"
45+
"placeholder": "projects/{project}/assets/asset_name",
46+
"no_assets": "No user assets found in: '{}'"
4647
},
4748
"load_table": {
4849
"too_small": "The provided file have less than 3 columns. Please provide a complete point file with at least ID, lattitude and longitude columns."

sepal_ui/scripts/decorator.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
...
99
"""
1010

11+
import json
1112
import os
1213
import warnings
1314
from functools import wraps
@@ -17,7 +18,6 @@
1718
from warnings import warn
1819

1920
import ee
20-
import httplib2
2121
import ipyvuetify as v
2222
from deprecated.sphinx import versionadded
2323

@@ -34,28 +34,52 @@
3434

3535

3636
def init_ee() -> None:
37-
"""Initialize earth engine according to the environment.
37+
r"""Initialize earth engine according using a token.
3838
39-
It will use the creddential file if the EARTHENGINE_TOKEN env variable exist.
40-
Otherwise it use the simple Initialize command (asking the user to register if necessary).
39+
THe environment used to run the tests need to have a EARTHENGINE_TOKEN variable.
40+
The content of this variable must be the copy of a personal credential file that you can find on your local computer if you already run the earth engine command line tool. See the usage question for a github action example.
41+
42+
- Windows: ``C:\Users\USERNAME\\.config\\earthengine\\credentials``
43+
- Linux: ``/home/USERNAME/.config/earthengine/credentials``
44+
- MacOS: ``/Users/USERNAME/.config/earthengine/credentials``
45+
46+
Note:
47+
As all init method of pytest-gee, this method will fallback to a regular ``ee.Initialize()`` if the environment variable is not found e.g. on your local computer.
4148
"""
42-
# only do the initialization if the credential are missing
4349
if not ee.data._credentials:
44-
# if the credentials token is asved in the environment use it
45-
if "EARTHENGINE_TOKEN" in os.environ:
50+
credential_folder_path = Path.home() / ".config" / "earthengine"
51+
credential_file_path = credential_folder_path / "credentials"
52+
53+
if "EARTHENGINE_TOKEN" in os.environ and not credential_file_path.exists():
54+
4655
# write the token to the appropriate folder
4756
ee_token = os.environ["EARTHENGINE_TOKEN"]
48-
credential_folder_path = Path.home() / ".config" / "earthengine"
4957
credential_folder_path.mkdir(parents=True, exist_ok=True)
50-
credential_file_path = credential_folder_path / "credentials"
5158
credential_file_path.write_text(ee_token)
5259

60+
# Extract the project name from credentials
61+
_credentials = json.loads(credential_file_path.read_text())
62+
project_id = _credentials.get("project_id", _credentials.get("project", None))
63+
64+
if not project_id:
65+
raise NameError(
66+
"The project name cannot be detected. "
67+
"Please set it using `earthengine set_project project_name`."
68+
)
69+
70+
# Check if we are using a google service account
71+
if _credentials.get("type") == "service_account":
72+
ee_user = _credentials.get("client_email")
73+
credentials = ee.ServiceAccountCredentials(
74+
ee_user, str(credential_file_path)
75+
)
76+
ee.Initialize(credentials=credentials)
77+
ee.data._cloud_api_user_project = project_id
78+
return
79+
5380
# if the user is in local development the authentication should
5481
# already be available
55-
ee.Initialize(http_transport=httplib2.Http())
56-
assert len(ee.data.getAssetRoots()) > 0, ms.utils.ee.no_asset_root
57-
58-
return
82+
ee.Initialize(project=project_id)
5983

6084

6185
################################################################################

sepal_ui/scripts/gee.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def get_assets(folder: Union[str, Path] = "") -> List[dict]:
9595
"""
9696
# set the folder and init the list
9797
asset_list = []
98-
folder = str(folder) or ee.data.getAssetRoots()[0]["id"]
98+
folder = str(folder) or f"projects/{ee.data._cloud_api_user_project}/assets/"
9999

100100
def _recursive_get(folder, asset_list):
101101

@@ -122,7 +122,7 @@ def is_asset(asset_name: str, folder: Union[str, Path] = "") -> bool:
122122
true if already in folder
123123
"""
124124
# get the folder
125-
folder = str(folder) or ee.data.getAssetRoots()[0]["id"]
125+
folder = str(folder) or f"projects/{ee.data._cloud_api_user_project}/assets/"
126126

127127
# get all the assets
128128
asset_list = get_assets(folder)

sepal_ui/scripts/utils.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""All the helper function of sepal-ui."""
22

33
import configparser
4+
import json
45
import math
56
import os
67
import random
@@ -12,7 +13,6 @@
1213
from urllib.parse import urlparse
1314

1415
import ee
15-
import httplib2
1616
import ipyvuetify as v
1717
import requests
1818
import tomli
@@ -127,28 +127,52 @@ def get_file_size(filename: Pathlike) -> str:
127127

128128

129129
def init_ee() -> None:
130-
"""Initialize earth engine according to the environment.
130+
r"""Initialize earth engine according using a token.
131131
132-
It will use the creddential file if the EARTHENGINE_TOKEN env variable exist.
133-
Otherwise it use the simple Initialize command (asking the user to register if necessary).
132+
THe environment used to run the tests need to have a EARTHENGINE_TOKEN variable.
133+
The content of this variable must be the copy of a personal credential file that you can find on your local computer if you already run the earth engine command line tool. See the usage question for a github action example.
134+
135+
- Windows: ``C:\Users\USERNAME\\.config\\earthengine\\credentials``
136+
- Linux: ``/home/USERNAME/.config/earthengine/credentials``
137+
- MacOS: ``/Users/USERNAME/.config/earthengine/credentials``
138+
139+
Note:
140+
As all init method of pytest-gee, this method will fallback to a regular ``ee.Initialize()`` if the environment variable is not found e.g. on your local computer.
134141
"""
135-
# only do the initialization if the credential are missing
136142
if not ee.data._credentials:
137-
# if the credentials token is asved in the environment use it
138-
if "EARTHENGINE_TOKEN" in os.environ:
143+
credential_folder_path = Path.home() / ".config" / "earthengine"
144+
credential_file_path = credential_folder_path / "credentials"
145+
146+
if "EARTHENGINE_TOKEN" in os.environ and not credential_file_path.exists():
147+
139148
# write the token to the appropriate folder
140149
ee_token = os.environ["EARTHENGINE_TOKEN"]
141-
credential_folder_path = Path.home() / ".config" / "earthengine"
142150
credential_folder_path.mkdir(parents=True, exist_ok=True)
143-
credential_file_path = credential_folder_path / "credentials"
144151
credential_file_path.write_text(ee_token)
145152

153+
# Extract the project name from credentials
154+
_credentials = json.loads(credential_file_path.read_text())
155+
project_id = _credentials.get("project_id", _credentials.get("project", None))
156+
157+
if not project_id:
158+
raise NameError(
159+
"The project name cannot be detected. "
160+
"Please set it using `earthengine set_project project_name`."
161+
)
162+
163+
# Check if we are using a google service account
164+
if _credentials.get("type") == "service_account":
165+
ee_user = _credentials.get("client_email")
166+
credentials = ee.ServiceAccountCredentials(
167+
ee_user, str(credential_file_path)
168+
)
169+
ee.Initialize(credentials=credentials)
170+
ee.data._cloud_api_user_project = project_id
171+
return
172+
146173
# if the user is in local development the authentication should
147174
# already be available
148-
ee.Initialize(http_transport=httplib2.Http())
149-
assert len(ee.data.getAssetRoots()) > 0, ms.utils.ee.no_asset_root
150-
151-
return
175+
ee.Initialize(project=project_id)
152176

153177

154178
def normalize_str(msg: str, folder: bool = True) -> str:

sepal_ui/sepalwidgets/inputs.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,9 @@ def __init__(
690690
self.asset_info = None
691691

692692
# if folder is not set use the root one
693-
self.folder = str(folder) or ee.data.getAssetRoots()[0]["id"]
693+
self.folder = (
694+
str(folder) or f"projects/{ee.data._cloud_api_user_project}/assets/"
695+
)
694696
self.types = types
695697

696698
# load the default assets
@@ -699,6 +701,8 @@ def __init__(
699701
# Validate the input as soon as the object is instantiated
700702
self.observe(self._validate, "v_model")
701703

704+
self.observe(self._fill_no_data, "items")
705+
702706
# set the default parameters
703707
kwargs.setdefault("v_model", None)
704708
kwargs.setdefault("clearable", True)
@@ -714,10 +718,26 @@ def __init__(
714718
# load the assets in the combobox
715719
self._get_items()
716720

721+
self._fill_no_data({})
722+
717723
# add js behaviours
718724
self.on_event("click:prepend", self._get_items)
719725
self.observe(self._get_items, "default_asset")
720726

727+
def _fill_no_data(self, _: dict) -> None:
728+
"""Fill the items with a no data message if the items are empty."""
729+
# Done in this way because v_slots are not working
730+
if not self.items:
731+
self.v_model = None
732+
self.items = [
733+
{
734+
"text": ms.widgets.asset_select.no_assets.format(self.folder),
735+
"disabled": True,
736+
}
737+
]
738+
739+
return
740+
721741
@sd.switch("loading")
722742
def _validate(self, change: dict) -> None:
723743
"""Validate the selected asset. Throw an error message if is not accessible or not in the type list."""

tests/conftest.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121

2222
try:
2323
su.init_ee()
24-
except Exception:
25-
pass # try to init earthengine. use ee.data._credentials to skip
24+
except Exception as e:
25+
raise e
26+
# pass # try to init earthengine. use ee.data._credentials to skip
2627

2728
# -- a component to fake the display in Ipython --------------------------------
2829

@@ -123,7 +124,7 @@ def gee_dir(_hash: str) -> Optional[Path]:
123124
pytest.skip("Eathengine is not connected")
124125

125126
# create a test folder with a hash name
126-
root = ee.data.getAssetRoots()[0]["id"]
127+
root = f"projects/{ee.data._cloud_api_user_project}/assets/"
127128
gee_dir = Path(root) / f"sepal-ui-{_hash}"
128129
ee.data.createAsset({"type": "FOLDER"}, str(gee_dir))
129130

@@ -197,17 +198,15 @@ def fake_asset(gee_dir: Path) -> Path:
197198

198199
@pytest.fixture(scope="session")
199200
def gee_user_dir(gee_dir: Path) -> Path:
200-
"""Return the path to the gee_dir assets without the project elements.
201+
"""Return the path to the gee_dir assets.
201202
202203
Args:
203204
gee_dir: the path to the session defined GEE directory
204205
205206
Returns:
206207
the path to gee_dir
207208
"""
208-
legacy_project = Path("projects/earthengine-legacy/assets")
209-
210-
return gee_dir.relative_to(legacy_project)
209+
return gee_dir
211210

212211

213212
@pytest.fixture(scope="session")

tests/test_scripts/test_decorator.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Test the custom decorators."""
22

3+
import json
4+
import os
35
import warnings
6+
from pathlib import Path
47

58
import ee
69
import ipyvuetify as v
@@ -11,11 +14,58 @@
1114
from sepal_ui.scripts.warning import SepalWarning
1215

1316

14-
@pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set")
1517
def test_init_ee() -> None:
16-
"""Check that ee can be initialized from sepal_ui."""
17-
# check that no error is raised
18-
sd.init_ee()
18+
"""Test the init_ee_from_token function."""
19+
credentials_filepath = Path(ee.oauth.get_credentials_path())
20+
existing = False
21+
22+
try:
23+
# Reset credentials to force the initialization
24+
# It can be initiated from different imports
25+
ee.data._credentials = None
26+
27+
# Get the credentials path
28+
29+
# Remove the credentials file if it exists
30+
if credentials_filepath.exists():
31+
existing = True
32+
credentials_filepath.rename(credentials_filepath.with_suffix(".json.bak"))
33+
34+
# Act: Earthengine token should be created
35+
sd.init_ee()
36+
37+
assert credentials_filepath.exists()
38+
39+
# read the back up and remove the "project_id" key
40+
credentials = json.loads(
41+
credentials_filepath.with_suffix(".json.bak").read_text()
42+
)
43+
44+
## 2. Assert when there's no a project associated
45+
# remove the project_id key if it exists
46+
ee.data._credentials = None
47+
credentials.pop("project_id", None)
48+
credentials.pop("project", None)
49+
if "EARTHENGINE_PROJECT" in os.environ:
50+
del os.environ["EARTHENGINE_PROJECT"]
51+
52+
# write the new credentials
53+
credentials_filepath.write_text(json.dumps(credentials))
54+
55+
with pytest.raises(NameError) as e:
56+
sd.init_ee()
57+
58+
# Access the exception message via `e.value`
59+
error_message = str(e.value)
60+
assert "The project name cannot be detected" in error_message
61+
62+
finally:
63+
# restore the file
64+
if existing:
65+
credentials_filepath.with_suffix(".json.bak").rename(credentials_filepath)
66+
67+
# check that no error is raised
68+
sd.init_ee()
1969

2070
return
2171

0 commit comments

Comments
 (0)