Skip to content

Commit

Permalink
feat: Planet enhance (#896)
Browse files Browse the repository at this point in the history
  • Loading branch information
12rambau authored Nov 24, 2023
2 parents 2e831c2 + 5292607 commit 6b1c031
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 42 deletions.
5 changes: 4 additions & 1 deletion sepal_ui/message/en/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,17 @@
"exception": {
"empty": "Please fill the required field(s).",
"invalid": "Invalid email or password",
"nosubs": "Your credentials do not have any valid planet subscription."
"nosubs": "Your credentials do not have any valid planet subscription.",
"no_secret_file": "The credentials file does not exist, use a different login method."
},
"widget": {
"username": "Planet username",
"password": "Planet password",
"apikey": "Planet API key",
"store": "Remember credentials file in the session.",
"method": {
"label": "Login method",
"from_file": "From saved credentials",
"credentials": "Credentials",
"api_key": "Planet API key"
}
Expand Down
40 changes: 29 additions & 11 deletions sepal_ui/planetapi/planet_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import nest_asyncio
import planet.data_filter as filters
import requests
import traitlets as t
from deprecated.sphinx import deprecated
from planet import DataClient
from planet.auth import Auth
from planet.exceptions import NoPermission
Expand All @@ -21,7 +21,6 @@


class PlanetModel(Model):

SUBS_URL: str = (
"https://api.planet.com/auth/v1/experimental/public/my/subscriptions"
)
Expand Down Expand Up @@ -56,11 +55,18 @@ def __init__(self, credentials: Union[str, List[str]] = "") -> None:
if credentials:
self.init_session(credentials)

def init_session(self, credentials: Union[str, List[str]]) -> None:
@deprecated(
version="3.0",
reason="credentials member is deprecated, use self.auth._key instead",
)
def init_session(
self, credentials: Union[str, List[str]], write_secrets: bool = False
) -> None:
"""Initialize planet client with api key or credentials. It will handle errors.
Args:
credentials: planet API key or username and password pair of planet explorer.
credentials: planet API key, username and password pair or a secrets planet.json file.
write_secrets: either to write the credentials in the secret file or not. Defaults to True.
"""
if isinstance(credentials, str):
credentials = [credentials]
Expand All @@ -70,13 +76,21 @@ def init_session(self, credentials: Union[str, List[str]]) -> None:

if len(credentials) == 2:
self.auth = Auth.from_login(*credentials)

# Check if the str is a path to a secret file
elif len(credentials) == 1 and credentials[0].endswith(".json"):
self.auth = Auth.from_file(credentials[0])

else:
self.auth = Auth.from_key(credentials[0])

self.credentials = credentials
self.credentials = self.auth._key
self.session = Session(auth=self.auth)
self._is_active()

if self.active and write_secrets:
self.auth.store()

return

def _is_active(self) -> None:
Expand Down Expand Up @@ -213,10 +227,11 @@ def get_mosaics(self) -> dict:
"quad_download": true
}
"""
url = "https://api.planet.com/basemaps/v1/mosaics?api_key={}"
res = requests.get(url.format(self.credentials[0]))
mosaics_url = "https://api.planet.com/basemaps/v1/mosaics"
request = self.session.request("GET", mosaics_url)
response = asyncio.run(request)

return res.json().get("mosaics", [])
return response.json().get("mosaics", [])

def get_quad(self, mosaic: dict, quad_id: str) -> dict:
"""Get a quad response for a specific mosaic and quad.
Expand Down Expand Up @@ -245,10 +260,13 @@ def get_quad(self, mosaic: dict, quad_id: str) -> dict:
"percent_covered": 100
}
"""
url = "https://api.planet.com/basemaps/v1/mosaics/{}/quads/{}?api_key={}"
res = requests.get(url.format(mosaic["id"], quad_id, self.credentials[0]))
quads_url = "https://api.planet.com/basemaps/v1/mosaics/{}/quads/{}"
quads_url = quads_url.format(mosaic["id"], quad_id)

request = self.session.request("GET", quads_url)
response = asyncio.run(request)

return res.json() or {}
return response.json() or {}

@staticmethod
def search_status(d: dict) -> List[Dict[str, bool]]:
Expand Down
88 changes: 74 additions & 14 deletions sepal_ui/planetapi/planet_view.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The ``Card`` widget to use in application to interface with Planet."""

from pathlib import Path
from typing import Optional

import ipyvuetify as v
Expand All @@ -12,7 +13,6 @@


class PlanetView(sw.Layout):

planet_model: Optional[PlanetModel] = None
"Backend model to manipulate interface actions"

Expand Down Expand Up @@ -47,7 +47,7 @@ def __init__(
):
"""Stand-alone interface to capture planet lab credentials.
It also validate its subscription and connect to the client stored in the model.
It also validate its subscription and connect to the client from_file in the model.
Args:
btn (sw.Btn, optional): Button to trigger the validation process in the associated model.
Expand All @@ -67,18 +67,27 @@ def __init__(
)
self.w_password = sw.PasswordField(label=ms.planet.widget.password)
self.w_key = sw.PasswordField(label=ms.planet.widget.apikey, v_model="").hide()
self.w_secret_file = sw.TextField(
label=ms.planet.widget.store,
v_model=str(Path.home() / ".planet.json"),
readonly=True,
class_="mr-2",
).hide()
self.w_info_view = InfoView(model=self.planet_model)

self.w_method = v.Select(
label=ms.planet.widget.method.label,
class_="mr-2",
v_model="credentials",
v_model="",
items=[
{"value": "from_file", "text": ms.planet.widget.method.from_file},
{"value": "credentials", "text": ms.planet.widget.method.credentials},
{"value": "api_key", "text": ms.planet.widget.method.api_key},
],
)

self.w_store = sw.Checkbox(label=ms.planet.widget.store, v_model=True)

w_validation = v.Flex(
style_="flex-grow: 0 !important;",
children=[self.btn],
Expand All @@ -87,17 +96,22 @@ def __init__(
self.children = [
self.w_method,
sw.Layout(
attributes={"id": "planet_credentials"},
class_="align-center",
children=[
self.w_username,
self.w_password,
self.w_key,
self.w_secret_file,
],
),
self.w_store,
]

if not btn:
self.children[-1].set_children(w_validation, "last")
self.get_children(attr="id", value="planet_credentials")[0].set_children(
w_validation, "last"
)

# Set it here to avoid displacements when using button
self.set_children(self.w_info_view, "last")
Expand All @@ -108,36 +122,82 @@ def __init__(
self.w_method.observe(self._swap_inputs, "v_model")
self.btn.on_event("click", self.validate)

self.set_initial_method()

def validate_secret_file(self) -> None:
"""Validate the secret file path."""
if not Path(self.w_secret_file.v_model).exists():
self.w_secret_file.error_messages = [ms.planet.exception.no_secret_file]
return False

self.w_secret_file.error_messages = []
return True

def set_initial_method(self) -> None:
"""Set the initial method to connect to planet lab."""
self.w_method.v_model = (
"from_file" if self.validate_secret_file() else "credentials"
)

def reset(self) -> None:
"""Empty credentials fields and restart activation mode."""
self.w_username.v_model = None
self.w_password.v_model = None
self.w_key.v_model = None
self.w_username.v_model = ""
self.w_password.v_model = ""
self.w_key.v_model = ""
self.planet_model.__init__()

return

def _swap_inputs(self, change: dict) -> None:
"""Swap between credentials and api key inputs."""
"""Swap between credentials and api key inputs.
Args:
change.new: values of from_file, credentials, api_key
"""
self.alert.reset()
self.reset()

self.w_username.toggle_viz()
self.w_password.toggle_viz()
self.w_key.toggle_viz()
# small detail, but validate the file every time the method is changed
self.validate_secret_file()

if change["new"] == "credentials":
self.w_username.show()
self.w_password.show()
self.w_secret_file.hide()
self.w_store.show()
self.w_key.hide()

elif change["new"] == "api_key":
self.w_username.hide()
self.w_password.hide()
self.w_secret_file.hide()
self.w_store.show()
self.w_key.show()
else:
self.w_username.hide()
self.w_password.hide()
self.w_key.hide()
self.w_store.hide()
self.w_secret_file.show()

return

@loading_button(debug=True)
@loading_button()
def validate(self, *args) -> None:
"""Initialize planet client and validate if is active."""
self.planet_model.__init__()

if self.w_method.v_model == "credentials":
credentials = [self.w_username.v_model, self.w_password.v_model]

elif self.w_method.v_model == "api_key":
credentials = self.w_key.v_model

else:
credentials = [self.w_key.v_model]
if not self.validate_secret_file():
raise Exception(ms.planet.exception.no_secret_file)
credentials = self.w_secret_file.v_model

self.planet_model.init_session(credentials)
self.planet_model.init_session(credentials, write_secrets=self.w_store.v_model)

return
65 changes: 58 additions & 7 deletions tests/test_planetapi/test_PlanetModel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test the planet PlanetModel model."""

import os
from pathlib import Path
from typing import Any, Union

import planet
Expand Down Expand Up @@ -42,7 +43,7 @@ def test_init(planet_key: str, cred: list) -> None:
@pytest.mark.skipif("PLANET_API_KEY" not in os.environ, reason="requires Planet")
@pytest.mark.parametrize("credentials", ["planet_key", "cred"])
def test_init_client(credentials: Any, request: FixtureRequest) -> None:
"""Check init the client with 2 methods.
"""Check init the client with 3 methods.
Args:
credentials: any credentials as set in the parameters
Expand All @@ -53,16 +54,66 @@ def test_init_client(credentials: Any, request: FixtureRequest) -> None:
planet_model.init_session(request.getfixturevalue(credentials))
assert planet_model.active is True

# check the content of cred
# I use a proxy to avoid exposing the credentials in the logs
cred = request.getfixturevalue(credentials)
cred = [cred] if isinstance(cred, str) else cred
is_same = planet_model.credentials == cred
assert is_same is True, "The credentials are not corresponding"
assert (
planet_model.auth._key == planet_model.credentials
), "The credentials are not corresponding"

with pytest.raises(Exception):
planet_model.init_session("wrongkey")

with pytest.raises(Exception):
planet_model.init_session(["wrongkey", "credentials"])

return


@pytest.mark.skipif("PLANET_API_KEY" not in os.environ, reason="requires Planet")
def test_init_with_file(planet_key) -> None:
"""Check init the session from a file."""
planet_secret_file = Path.home() / ".planet.json"
existing = False

# This test will overwrite and remove the file, let's backup it
if planet_secret_file.exists():
existing = True
planet_secret_file.rename(planet_secret_file.with_suffix(".json.bak"))

# Create a file with the credentials
planet_model = PlanetModel()
# We assume that the credentials are valid
planet_model.init_session(planet_key, write_secrets=True)

assert planet_secret_file.exists()

# test init with the file
planet_model = PlanetModel()
planet_model.init_session(str(planet_secret_file))

assert planet_model.active is True

# Check that wrong credentials won't save the secrets file

# remove the file
planet_secret_file.unlink()

planet_model = PlanetModel()

with pytest.raises(Exception):
planet_model.init_session("wrong_key", write_secrets=True)

assert not planet_secret_file.exists()

# Check no save with good credentials

planet_model = PlanetModel()
planet_model.init_session(planet_key, write_secrets=False)

assert not planet_secret_file.exists()

# restore the file
if existing:
planet_secret_file.with_suffix(".json.bak").rename(planet_secret_file)

return


Expand Down
Loading

0 comments on commit 6b1c031

Please sign in to comment.