From fa373672a2bbfbe9bfc780e09c1c81c895a70dac Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Wed, 27 Dec 2023 09:21:23 +0100 Subject: [PATCH 01/14] docs: correct path to repository --- docs/conf.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 19d01c2..1e5be55 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ ], } html_context = { - "github_user": "12rambau", + "github_user": "gee-community", "github_repo": "ipygee", "github_version": "", "doc_path": "docs", diff --git a/pyproject.toml b/pyproject.toml index 00aedb9..3acbba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ file = "README.rst" content-type = "text/x-rst" [project.urls] -Homepage = "https://github.com/12rambau/ipygee" +Homepage = "https://github.com/gee-community/ipygee" [project.optional-dependencies] test = [ From 26cebeb3d3946920df9a4cf94c095ba1d87c817c Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Wed, 27 Dec 2023 09:23:33 +0100 Subject: [PATCH 02/14] fix: add experimental notebook --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 857afd2..3d4ee4a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,7 @@ dmypy.json .vscode/ # image tmp file -*Zone.Identifier \ No newline at end of file +*Zone.Identifier + +# experimental notebooks for visual debugging +test.ipynb \ No newline at end of file From 682dc962661e6eb6c1d6601a372eae968c7a7cd1 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Wed, 27 Dec 2023 09:26:22 +0100 Subject: [PATCH 03/14] chore: rely on ipyvuetify for widgets --- .pre-commit-config.yaml | 11 ++++++----- pyproject.toml | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67a3941..bdcf075 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,11 +38,12 @@ repos: - id: doc8 stages: [commit] - - repo: https://github.com/FHPythonUtils/LicenseCheck - rev: "2023.5.1" - hooks: - - id: licensecheck - stages: [commit] + # crash on ipyvuetify + #- repo: https://github.com/FHPythonUtils/LicenseCheck + # rev: "2023.5.1" + # hooks: + # - id: licensecheck + # stages: [commit] - repo: https://github.com/codespell-project/codespell rev: v2.2.4 diff --git a/pyproject.toml b/pyproject.toml index 3acbba2..b0bd3fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ requires-python = ">=3.8" dependencies = [ "deprecated>=1.2.14", "earthengine-api", + "ipyvuetify", ] [[project.authors]] From 18b09609eb0b6a727577ebcf697e94e788438372 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Thu, 28 Dec 2023 09:06:25 +0100 Subject: [PATCH 04/14] feat: load the folders --- ipygee/asset.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 ipygee/asset.py diff --git a/ipygee/asset.py b/ipygee/asset.py new file mode 100644 index 0000000..7e207e9 --- /dev/null +++ b/ipygee/asset.py @@ -0,0 +1,83 @@ +"""The asset manager widget code and functionalities.""" +from __future__ import annotations + +from pathlib import Path +from typing import Dict, List + +import ee +import ipyvuetify as v +from google.cloud.resourcemanager import ProjectsClient + + +class AssetTreeView(v.Treeview): + """A Google Earth Engine asset tree view widget.""" + + def __init__(self, folders): + """Initialize the asset tree view widget. + + Args: + folders: the list of folders to display in the tree view as a list of dicts + with the keys "id" and "name" + """ + # restructure the folder list as a items list in a treeview object + # each item is represented by a name, an id and a children list + # that will be init to an empty list + items = [{"name": f["name"], "id": f["id"], "children": []} for f in folders] + + # add icon management with a icon slots + # see https://v2.vuetifyjs.com/en/components/treeview/#slots + v_slots = [ + { + "name": "prepend", + "variable": "props", + "children": [v.Icon(children=["mdi-folder"])], + } + ] + + super().__init__(items=items, v_slots=v_slots, dense=True, xs12=True) + + +class AssetManager(v.Flex): + """The asset manager widget.""" + + legacy_folders: List[Dict[str, str]] + "The list of legacy folders as dicts with the keys 'id' and 'name'" + + gcloud_folders: List[Dict[str, str]] + "The list of gcloud folders as dicts with the keys 'id' and 'name'" + + def __init__(self): + """Initialize the asset manager widget.""" + # create a widget title + w_title = v.Html(tag="h2", children=["Asset Manager"], xs12=True) + + # load all the folders and add them in alphabetic orders + self.legacy_folders = self._get_legacy_folders() + self.gcloud_folders = self._get_gcloud_folders() + folders = sorted( + self.legacy_folders + self.gcloud_folders, key=lambda f: f["name"] + ) + w_tree_view = AssetTreeView(folders) + + super().__init__(children=[w_title, w_tree_view]) + + @staticmethod + def _get_legacy_folders() -> List[Dict[str, str]]: + """Retrieve the list of folders in the legacy asset manager of GEE. + + Returns: + the list of every legacy folder as dicts with the keys "id" and "name" + """ + folders = [ + Path(f["id"]) for f in ee.data.getAssetRoots() if f["type"] == "Folder" + ] + return [{"id": f.as_posix(), "name": f.stem} for f in folders] + + @staticmethod + def _get_gcloud_folders() -> List[Dict[str, str]]: + folders = [ + p.project_id + for p in ProjectsClient().search_projects() + if "earth-engine" in p.labels + ] + return [{"id": f"project/{f}/assets", "name": f} for f in folders] From 56ca2b113c979261d0a12dc86dc8c2dc0294de32 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Mon, 19 Feb 2024 21:45:29 +0100 Subject: [PATCH 05/14] fix: navigate in files --- ipygee/asset.py | 194 ++++++++++++++++++++++++++++++++++++++++++++++-- ipygee/type.py | 9 +++ pyproject.toml | 1 + 3 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 ipygee/type.py diff --git a/ipygee/asset.py b/ipygee/asset.py index 7e207e9..17cac0e 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -2,11 +2,178 @@ from __future__ import annotations from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional import ee +import geetools # noqa import ipyvuetify as v +import traitlets as t from google.cloud.resourcemanager import ProjectsClient +from natsort import humansorted + +ICON_STYLE = { + "PARENT": {"color": "primary", "icon": "mdi-folder-open"}, + "PROJECT": {"color": "primary", "icon": "mdi-google-cloud"}, + "FOLDER": {"color": "primary", "icon": "mdi-folder"}, + "IMAGE": {"color": "primary", "icon": "mdi-image-outline"}, + "IMAGE_COLLECTION": {"color": "primary", "icon": "mdi-image-multiple-outline"}, + "TABLE": {"color": "primary", "icon": "mdi-table"}, + "FEATURE_COLLECTION": {"color": "primary", "icon": "mdi-tabe"}, +} +"The style to apply to each object" + + +class AssetManager(v.Flex): + """A asset manager widget.""" + + # -- Variables ------------------------------------------------------------- + + folder: t.Unicode = t.Unicode(".").tag(sync=True) + "The current folder that the user see" + + selected_item: t.Unicode = t.Unicode("").tag(sync=True) + "The selected item of the asset manager" + + # -- Widgets --------------------------------------------------------------- + + w_new: Optional[v.Btn] = None + "The new btn on the top of the asset manager" + + w_reload: Optional[v.Btn] = None + "The reload btn at the top of the asset manager" + + w_search: Optional[v.TextField] = None + "The search textfield to crowl into the existing items" + + w_selected: Optional[v.TextField] = None + "The field where the user can see the asset Id of the selected item" + + w_loading: Optional[v.ProgressLinear] = None + "Loading topbar of the widget" + + w_list: Optional[v.List] = None + "The list of items displayed in the asset manager" + + def __init__(self): + """Initialize the class.""" + # add a line of buttons to reload and add new projects + self.w_new = v.Btn( + color="error", + children="NEW", + appendIcon="mdi-reload", + elevation=2, + class_="mr-2", + ) + self.w_reload = v.Btn( + children=[v.Icon(color="primary", children="mdi-reload")], elevation=2 + ) + w_line = v.Flex(children=[self.w_new, self.w_reload], class_="pa-3") + + # add a second line with the search field + self.w_search = v.TextField( + prepend_inner_icon="mdi-magnify", placeholder="Search in assets" + ) + + # generate the asset selector + self.w_selected = v.TextField( + readonly=True, placeholder="Selected item", v_model="" + ) + + # generate the initial list + w_group = v.ListItemGroup(children=self.get_items(), v_model="") + self.w_list = v.List(dense=True, flat=True, v_model=True, children=[w_group]) + + super().__init__( + children=[w_line, self.w_search, v.Divider(), self.w_selected, self.w_list], + v_model="", + ) + + # add JS behaviour + t.link((self, "selected_item"), (self, "v_model")) + self.w_list.children[0].observe(self.on_item_select, "v_model") + + def get_items(self) -> List[v.ListItem]: + """Create the list of items inside a folder.""" + # special case when we are at the root of everything + # because of the specific display of cloud projects we will store both the name and the id of everything as a dict + # for all other item types it will simply be the Name + if self.folder == ".": + list_items = [p.project_id for p in ProjectsClient().search_projects() if "earth-engine" in p.labels] # fmt: skip + list_items = [{"id": f"projects/{i}/assets", "name": i} for i in list_items] + else: + list_items = [ + {"id": str(i), "name": i.name} for i in ee.Asset(self.folder).iterdir() + ] + + # split the folders and the files to display the folders first + # cloud bucket will be considered as folders + folder_list, file_list = [], [] + + # walk the list of items and generate a list of ListItem using all the appropriate features + # first we extract the type to deduce the icon and color, then we build the Items with the + # ID and the display name and finally we split them in 2 groups the folders and the files + for i in list_items: + asset = ee.Asset(i["id"]) + type = "PROJECT" if asset.is_project() else asset.type + icon = ICON_STYLE[type]["icon"] + color = ICON_STYLE[type]["color"] + + action = v.ListItemAction( + children=[v.Icon(color=color, children=[icon])], class_="mr-1" + ) + content = v.ListItemContent( + children=[v.ListItemTitle(children=[i["name"]])] + ) + dst_list = folder_list if type in ["FOLDER", "PROJECT"] else file_list + dst_list.append(v.ListItem(value=i["id"], children=[action, content])) + + # humanly sort the 2 lists so that number are treated nicely + folder_list = humansorted(folder_list, key=lambda x: x.value) + file_list = humansorted(file_list, key=lambda x: x.value) + + # add a parent items if necessary. We follow the same mechanism with specific verifications + # if the parent is a project folder or the root + if self.folder != ".": + icon = ICON_STYLE["PARENT"]["icon"] + color = ICON_STYLE["PARENT"]["color"] + + asset = ee.Asset(self.folder) + parent = ee.Asset("") if asset.is_project() else asset.parent + name = parent.parts[1] if parent.is_project() else parent.name + name = name or "." # special case for the root + + action = v.ListItemAction( + children=[v.Icon(color=color, children=[icon])], class_="mr-1" + ) + content = v.ListItemContent(children=[v.ListItemTitle(children=[name])]) + item = v.ListItem(value=str(parent), children=[action, content]) + + folder_list.insert(0, item) + + # return the concatenation of the 2 lists + return folder_list + file_list + + def on_item_select(self, change: dict): + """Act when an item is clicked by the user.""" + # exit if nothing is changed to avoid infinite loop upon loading + selected = change["new"] + if not selected: + return + + # select the item in the item TextField so user can interact with it + self.w_selected.v_model = change["new"] + + # reset files. This is resetting the scroll to top without using js scripts + # set the new content files and folders + ee.Asset(change["new"]) + if ( + selected == "." + or ee.Asset(selected).is_project() + or ee.Asset(selected).is_folder() + ): + self.folder = selected + self.w_list.children[0].cildren = [] + self.w_list.children[0].children = self.get_items() class AssetTreeView(v.Treeview): @@ -22,22 +189,35 @@ def __init__(self, folders): # restructure the folder list as a items list in a treeview object # each item is represented by a name, an id and a children list # that will be init to an empty list - items = [{"name": f["name"], "id": f["id"], "children": []} for f in folders] + items = [ + { + "name": f["name"], + "id": f["id"], + "children": [{"name": "bite", "id": f'{f["id"]}_toto'}], + } + for f in folders + ] # add icon management with a icon slots # see https://v2.vuetifyjs.com/en/components/treeview/#slots v_slots = [ { "name": "prepend", - "variable": "props", - "children": [v.Icon(children=["mdi-folder"])], + "variable": "obj", + "children": [ + v.Icon( + v_if="!obj.item.file", + children=[r"{{ obj.open ? 'mdi-folder-open' : 'mdi-folder' }}"], + ) + # v.Icon(v_else="props.item.file", children=["mdi-file"]) + ], } ] super().__init__(items=items, v_slots=v_slots, dense=True, xs12=True) -class AssetManager(v.Flex): +class TestAssetManager(v.Flex): """The asset manager widget.""" legacy_folders: List[Dict[str, str]] @@ -57,9 +237,9 @@ def __init__(self): folders = sorted( self.legacy_folders + self.gcloud_folders, key=lambda f: f["name"] ) - w_tree_view = AssetTreeView(folders) + self.w_tree_view = AssetTreeView(folders) - super().__init__(children=[w_title, w_tree_view]) + super().__init__(children=[w_title, self.w_tree_view]) @staticmethod def _get_legacy_folders() -> List[Dict[str, str]]: diff --git a/ipygee/type.py b/ipygee/type.py new file mode 100644 index 0000000..6daae28 --- /dev/null +++ b/ipygee/type.py @@ -0,0 +1,9 @@ +""""The different types created for this specific package.""" +import os +from pathlib import Path +from typing import Union + +import ee +import geetools # noqa: F401 + +pathlike = Union[os.PathLike, Path, ee.Asset] diff --git a/pyproject.toml b/pyproject.toml index b0bd3fc..15f4c49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "deprecated>=1.2.14", "earthengine-api", "ipyvuetify", + "natsort", ] [[project.authors]] From 52e8ea532201922f4a28e6ff81adb73e66d6adb1 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Mon, 19 Feb 2024 21:52:21 +0100 Subject: [PATCH 06/14] fix: add colors to the files --- ipygee/asset.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ipygee/asset.py b/ipygee/asset.py index 17cac0e..377fcaf 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -12,13 +12,13 @@ from natsort import humansorted ICON_STYLE = { - "PARENT": {"color": "primary", "icon": "mdi-folder-open"}, - "PROJECT": {"color": "primary", "icon": "mdi-google-cloud"}, - "FOLDER": {"color": "primary", "icon": "mdi-folder"}, - "IMAGE": {"color": "primary", "icon": "mdi-image-outline"}, - "IMAGE_COLLECTION": {"color": "primary", "icon": "mdi-image-multiple-outline"}, - "TABLE": {"color": "primary", "icon": "mdi-table"}, - "FEATURE_COLLECTION": {"color": "primary", "icon": "mdi-tabe"}, + "PARENT": {"color": "black", "icon": "mdi-folder-open"}, + "PROJECT": {"color": "red", "icon": "mdi-google-cloud"}, + "FOLDER": {"color": "grey", "icon": "mdi-folder"}, + "IMAGE": {"color": "purple", "icon": "mdi-image-outline"}, + "IMAGE_COLLECTION": {"color": "purple", "icon": "mdi-image-multiple-outline"}, + "TABLE": {"color": "green", "icon": "mdi-table"}, + "FEATURE_COLLECTION": {"color": "green", "icon": "mdi-tabe"}, } "The style to apply to each object" From 74285d4c60c974cb0e5316879a6792b23035a5e0 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Mon, 19 Feb 2024 22:08:20 +0100 Subject: [PATCH 07/14] fix: move the search to a Dialog --- ipygee/asset.py | 140 ++++++------------------------------------------ pyproject.toml | 3 ++ 2 files changed, 18 insertions(+), 125 deletions(-) diff --git a/ipygee/asset.py b/ipygee/asset.py index 377fcaf..496c543 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -1,8 +1,7 @@ """The asset manager widget code and functionalities.""" from __future__ import annotations -from pathlib import Path -from typing import Dict, List, Optional +from typing import List, Optional import ee import geetools # noqa @@ -42,8 +41,8 @@ class AssetManager(v.Flex): w_reload: Optional[v.Btn] = None "The reload btn at the top of the asset manager" - w_search: Optional[v.TextField] = None - "The search textfield to crowl into the existing items" + w_search: Optional[v.Btn] = None + "The search button to crowl into the existing items" w_selected: Optional[v.TextField] = None "The field where the user can see the asset Id of the selected item" @@ -57,34 +56,24 @@ class AssetManager(v.Flex): def __init__(self): """Initialize the class.""" # add a line of buttons to reload and add new projects - self.w_new = v.Btn( - color="error", - children="NEW", - appendIcon="mdi-reload", - elevation=2, - class_="mr-2", - ) + self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1") self.w_reload = v.Btn( - children=[v.Icon(color="primary", children="mdi-reload")], elevation=2 + children=[v.Icon(color="primary", children="mdi-reload")], elevation=2, class_="ma-1" ) - w_line = v.Flex(children=[self.w_new, self.w_reload], class_="pa-3") - - # add a second line with the search field - self.w_search = v.TextField( - prepend_inner_icon="mdi-magnify", placeholder="Search in assets" + self.w_search = v.Btn( + children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1" ) + w_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search], class_="pa-3") # generate the asset selector - self.w_selected = v.TextField( - readonly=True, placeholder="Selected item", v_model="" - ) + self.w_selected = v.TextField(readonly=True, placeholder="Selected item", v_model="") # generate the initial list w_group = v.ListItemGroup(children=self.get_items(), v_model="") self.w_list = v.List(dense=True, flat=True, v_model=True, children=[w_group]) super().__init__( - children=[w_line, self.w_search, v.Divider(), self.w_selected, self.w_list], + children=[w_line, self.w_selected, self.w_list], v_model="", ) @@ -101,9 +90,7 @@ def get_items(self) -> List[v.ListItem]: list_items = [p.project_id for p in ProjectsClient().search_projects() if "earth-engine" in p.labels] # fmt: skip list_items = [{"id": f"projects/{i}/assets", "name": i} for i in list_items] else: - list_items = [ - {"id": str(i), "name": i.name} for i in ee.Asset(self.folder).iterdir() - ] + list_items = [{"id": str(i), "name": i.name} for i in ee.Asset(self.folder).iterdir()] # split the folders and the files to display the folders first # cloud bucket will be considered as folders @@ -118,12 +105,8 @@ def get_items(self) -> List[v.ListItem]: icon = ICON_STYLE[type]["icon"] color = ICON_STYLE[type]["color"] - action = v.ListItemAction( - children=[v.Icon(color=color, children=[icon])], class_="mr-1" - ) - content = v.ListItemContent( - children=[v.ListItemTitle(children=[i["name"]])] - ) + action = v.ListItemAction(children=[v.Icon(color=color, children=[icon])], class_="mr-1") + content = v.ListItemContent(children=[v.ListItemTitle(children=[i["name"]])]) dst_list = folder_list if type in ["FOLDER", "PROJECT"] else file_list dst_list.append(v.ListItem(value=i["id"], children=[action, content])) @@ -142,9 +125,7 @@ def get_items(self) -> List[v.ListItem]: name = parent.parts[1] if parent.is_project() else parent.name name = name or "." # special case for the root - action = v.ListItemAction( - children=[v.Icon(color=color, children=[icon])], class_="mr-1" - ) + action = v.ListItemAction(children=[v.Icon(color=color, children=[icon])], class_="mr-1") content = v.ListItemContent(children=[v.ListItemTitle(children=[name])]) item = v.ListItem(value=str(parent), children=[action, content]) @@ -166,98 +147,7 @@ def on_item_select(self, change: dict): # reset files. This is resetting the scroll to top without using js scripts # set the new content files and folders ee.Asset(change["new"]) - if ( - selected == "." - or ee.Asset(selected).is_project() - or ee.Asset(selected).is_folder() - ): + if selected == "." or ee.Asset(selected).is_project() or ee.Asset(selected).is_folder(): self.folder = selected self.w_list.children[0].cildren = [] self.w_list.children[0].children = self.get_items() - - -class AssetTreeView(v.Treeview): - """A Google Earth Engine asset tree view widget.""" - - def __init__(self, folders): - """Initialize the asset tree view widget. - - Args: - folders: the list of folders to display in the tree view as a list of dicts - with the keys "id" and "name" - """ - # restructure the folder list as a items list in a treeview object - # each item is represented by a name, an id and a children list - # that will be init to an empty list - items = [ - { - "name": f["name"], - "id": f["id"], - "children": [{"name": "bite", "id": f'{f["id"]}_toto'}], - } - for f in folders - ] - - # add icon management with a icon slots - # see https://v2.vuetifyjs.com/en/components/treeview/#slots - v_slots = [ - { - "name": "prepend", - "variable": "obj", - "children": [ - v.Icon( - v_if="!obj.item.file", - children=[r"{{ obj.open ? 'mdi-folder-open' : 'mdi-folder' }}"], - ) - # v.Icon(v_else="props.item.file", children=["mdi-file"]) - ], - } - ] - - super().__init__(items=items, v_slots=v_slots, dense=True, xs12=True) - - -class TestAssetManager(v.Flex): - """The asset manager widget.""" - - legacy_folders: List[Dict[str, str]] - "The list of legacy folders as dicts with the keys 'id' and 'name'" - - gcloud_folders: List[Dict[str, str]] - "The list of gcloud folders as dicts with the keys 'id' and 'name'" - - def __init__(self): - """Initialize the asset manager widget.""" - # create a widget title - w_title = v.Html(tag="h2", children=["Asset Manager"], xs12=True) - - # load all the folders and add them in alphabetic orders - self.legacy_folders = self._get_legacy_folders() - self.gcloud_folders = self._get_gcloud_folders() - folders = sorted( - self.legacy_folders + self.gcloud_folders, key=lambda f: f["name"] - ) - self.w_tree_view = AssetTreeView(folders) - - super().__init__(children=[w_title, self.w_tree_view]) - - @staticmethod - def _get_legacy_folders() -> List[Dict[str, str]]: - """Retrieve the list of folders in the legacy asset manager of GEE. - - Returns: - the list of every legacy folder as dicts with the keys "id" and "name" - """ - folders = [ - Path(f["id"]) for f in ee.data.getAssetRoots() if f["type"] == "Folder" - ] - return [{"id": f.as_posix(), "name": f.stem} for f in folders] - - @staticmethod - def _get_gcloud_folders() -> List[Dict[str, str]]: - folders = [ - p.project_id - for p in ProjectsClient().search_projects() - if "earth-engine" in p.labels - ] - return [{"id": f"project/{f}/assets", "name": f} for f in folders] diff --git a/pyproject.toml b/pyproject.toml index 15f4c49..2da4773 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,3 +115,6 @@ warn_redundant_casts = true [tool.licensecheck] using = "PEP631" + +[tool.black] +line-length = 105 # super small margin for items that are 2 characters too long From c09aa86aee3ae9f331992b7472da6ef98c41ce97 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Mon, 19 Feb 2024 23:30:30 +0100 Subject: [PATCH 08/14] fix: use a loading bar when changing folder --- ipygee/asset.py | 12 +++++++++-- ipygee/decorator.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 ipygee/decorator.py diff --git a/ipygee/asset.py b/ipygee/asset.py index 496c543..2f08198 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -10,6 +10,8 @@ from google.cloud.resourcemanager import ProjectsClient from natsort import humansorted +from .decorator import switch + ICON_STYLE = { "PARENT": {"color": "black", "icon": "mdi-folder-open"}, "PROJECT": {"color": "red", "icon": "mdi-google-cloud"}, @@ -55,6 +57,11 @@ class AssetManager(v.Flex): def __init__(self): """Initialize the class.""" + # start with the loading bar + self.w_loading = self.loading = v.ProgressLinear( + indeterminate=False, color="primary", background_color="white" + ) + # add a line of buttons to reload and add new projects self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1") self.w_reload = v.Btn( @@ -73,7 +80,7 @@ def __init__(self): self.w_list = v.List(dense=True, flat=True, v_model=True, children=[w_group]) super().__init__( - children=[w_line, self.w_selected, self.w_list], + children=[self.w_loading, w_line, self.w_selected, self.w_list], v_model="", ) @@ -134,6 +141,7 @@ def get_items(self) -> List[v.ListItem]: # return the concatenation of the 2 lists return folder_list + file_list + @switch("indeterminate", member="w_loading") def on_item_select(self, change: dict): """Act when an item is clicked by the user.""" # exit if nothing is changed to avoid infinite loop upon loading @@ -149,5 +157,5 @@ def on_item_select(self, change: dict): ee.Asset(change["new"]) if selected == "." or ee.Asset(selected).is_project() or ee.Asset(selected).is_folder(): self.folder = selected - self.w_list.children[0].cildren = [] + self.w_list.children[0].children = [] self.w_list.children[0].children = self.get_items() diff --git a/ipygee/decorator.py b/ipygee/decorator.py new file mode 100644 index 0000000..be4c39b --- /dev/null +++ b/ipygee/decorator.py @@ -0,0 +1,52 @@ +"""Decorators used in ipygee. + +ported from https://github.com/12rambau/sepal_ui/blob/main/sepal_ui/scripts/decorator.py +""" +from __future__ import annotations + +from functools import wraps +from typing import Any, Optional + + +def switch(*params, member: Optional[str] = None, debug: bool = True) -> Any: + r"""Decorator to switch the state of input boolean parameters on class widgets or the class itself. + + If ``widget`` is defined, it will switch the state of every widget parameter, otherwise + it will change the state of the class (self). You can also set two decorators on the same + function, one could affect the class and other the widget. + + Args: + *params: any boolean member of a Widget. + member: THe widget on which the member are switched. Default to self. + debug: Whether trigger or not an Exception if the decorated function fails. + + Returns: + The return statement of the decorated method + """ + + def decorator_switch(func): + @wraps(func) + def wrapper_switch(self, *args, **kwargs): + + # set the widget to work with. if nothing is set it will be self + widget = getattr(self, member) if member else self + + # create the list of target values based on the initial values + targets = [bool(getattr(widget, p)) for p in params] + not_targets = [not t for t in targets] + + # assgn the parameters to the target inverse + [setattr(widget, p, t) for p, t in zip(params, not_targets)] + + # execute the function and catch errors + try: + func(self, *args, **kwargs) + except Exception as e: + if debug: + raise e + finally: + [setattr(widget, p, t) for p, t in zip(params, targets)] + + return wrapper_switch + + return decorator_switch From 861d251b6649e45fff68c5f2f66236c5efdc1629 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Tue, 20 Feb 2024 08:40:05 +0100 Subject: [PATCH 09/14] refactor: improve display --- ipygee/asset.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/ipygee/asset.py b/ipygee/asset.py index 2f08198..9942943 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -49,19 +49,14 @@ class AssetManager(v.Flex): w_selected: Optional[v.TextField] = None "The field where the user can see the asset Id of the selected item" - w_loading: Optional[v.ProgressLinear] = None - "Loading topbar of the widget" - w_list: Optional[v.List] = None "The list of items displayed in the asset manager" + w_card: Optional[v.Card] = None + "The card hosting the list of items" + def __init__(self): """Initialize the class.""" - # start with the loading bar - self.w_loading = self.loading = v.ProgressLinear( - indeterminate=False, color="primary", background_color="white" - ) - # add a line of buttons to reload and add new projects self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1") self.w_reload = v.Btn( @@ -73,14 +68,16 @@ def __init__(self): w_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search], class_="pa-3") # generate the asset selector - self.w_selected = v.TextField(readonly=True, placeholder="Selected item", v_model="") + self.w_selected = v.TextField( + readonly=True, placeholder="Selected item", v_model="", clearable=True, outlined=True + ) # generate the initial list w_group = v.ListItemGroup(children=self.get_items(), v_model="") - self.w_list = v.List(dense=True, flat=True, v_model=True, children=[w_group]) - + self.w_list = v.List(dense=True, flat=True, v_model=True, children=[w_group], outlined=True) + self.w_card = v.Card(children=[self.w_list], outlined=True) super().__init__( - children=[self.w_loading, w_line, self.w_selected, self.w_list], + children=[w_line, self.w_selected, self.w_card], v_model="", ) @@ -141,7 +138,7 @@ def get_items(self) -> List[v.ListItem]: # return the concatenation of the 2 lists return folder_list + file_list - @switch("indeterminate", member="w_loading") + @switch("loading", "disabled", member="w_card") def on_item_select(self, change: dict): """Act when an item is clicked by the user.""" # exit if nothing is changed to avoid infinite loop upon loading @@ -157,5 +154,6 @@ def on_item_select(self, change: dict): ee.Asset(change["new"]) if selected == "." or ee.Asset(selected).is_project() or ee.Asset(selected).is_folder(): self.folder = selected - self.w_list.children[0].children = [] - self.w_list.children[0].children = self.get_items() + items = self.get_items() + self.w_list.children[0].children = [] # trick to scroll up + self.w_list.children[0].children = items From d37078edf033a3dd60fbcb5ada50ef839878a685 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Tue, 20 Feb 2024 08:50:50 +0100 Subject: [PATCH 10/14] fix: code the reload action --- ipygee/asset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ipygee/asset.py b/ipygee/asset.py index 9942943..640ff30 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -84,6 +84,7 @@ def __init__(self): # add JS behaviour t.link((self, "selected_item"), (self, "v_model")) self.w_list.children[0].observe(self.on_item_select, "v_model") + self.w_reload.on_event("click", self.on_reload) def get_items(self) -> List[v.ListItem]: """Create the list of items inside a folder.""" @@ -157,3 +158,7 @@ def on_item_select(self, change: dict): items = self.get_items() self.w_list.children[0].children = [] # trick to scroll up self.w_list.children[0].children = items + + def on_reload(self, *args): + """Reload the current folder.""" + self.on_item_select(change={"new": self.folder}) From 54a4cec5fb8ec0ab847f0604fce5bb693bd7beb5 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Sat, 24 Feb 2024 09:49:34 +0100 Subject: [PATCH 11/14] feat: first mockup --- ipygee/asset.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/ipygee/asset.py b/ipygee/asset.py index 640ff30..d9190e8 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -57,29 +57,32 @@ class AssetManager(v.Flex): def __init__(self): """Initialize the class.""" + # start by defining al the widgets + # We deactivated the formatting to define each one of them on 1 single line + # fmt: off + # add a line of buttons to reload and add new projects self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1") - self.w_reload = v.Btn( - children=[v.Icon(color="primary", children="mdi-reload")], elevation=2, class_="ma-1" - ) - self.w_search = v.Btn( - children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1" - ) - w_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search], class_="pa-3") - - # generate the asset selector - self.w_selected = v.TextField( - readonly=True, placeholder="Selected item", v_model="", clearable=True, outlined=True - ) + self.w_reload = v.Btn(children=[v.Icon(color="primary", children="mdi-reload")], elevation=2, class_="ma-1") + self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1") + w_main_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search]) + + # generate the asset selector and the CRUD buttons + self.w_selected = v.TextField(readonly=True, placeholder="Selected item", v_model="", clearable=True, outlined=True, class_="ma-1") + self.w_view = v.Btn(children=[v.Icon(color="primary", children="mdi-eye")]) + self.w_copy = v.Btn(children=[v.Icon(color="primary", children="mdi-content-copy")]) + self.w_move = v.Btn(children=[v.Icon(color="primary", children="mdi-file-move")]) + self.w_delete = v.Btn(children=[v.Icon(color="primary", children="mdi-trash-can")]) + w_btn_list = v.ItemGroup(class_="ma-1 v-btn-toggle",children=[self.w_view, self.w_copy, self.w_move, self.w_delete]) + w_selected_line = v.Layout(row=True, children=[w_btn_list, self.w_selected], class_="ma-1") # generate the initial list w_group = v.ListItemGroup(children=self.get_items(), v_model="") - self.w_list = v.List(dense=True, flat=True, v_model=True, children=[w_group], outlined=True) - self.w_card = v.Card(children=[self.w_list], outlined=True) - super().__init__( - children=[w_line, self.w_selected, self.w_card], - v_model="", - ) + self.w_list = v.List(dense=True, v_model=True, children=[w_group], outlined=True) + self.w_card = v.Card(children=[self.w_list], outlined=True, class_="ma-1") + + super().__init__(children=[w_main_line, w_selected_line, self.w_card], v_model="", class_="ma-1") + # fmt: on # add JS behaviour t.link((self, "selected_item"), (self, "v_model")) From 5c60f82f899315935c8e9f2955d39a5451e08a7b Mon Sep 17 00:00:00 2001 From: Rambaud Pierrick <12rambau@users.noreply.github.com> Date: Sun, 25 Feb 2024 15:44:51 +0000 Subject: [PATCH 12/14] build: add jupyterlab to the container features --- .devcontainer/devcontainer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 56910d3..0a979fa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,6 +2,9 @@ "name": "Python 3", "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", "features": { + "ghcr.io/devcontainers/features/python:1": { + "installJupyterlab": true + }, "ghcr.io/devcontainers-contrib/features/nox:2": {}, "ghcr.io/devcontainers-contrib/features/pre-commit:2": {} }, From d0a70defa298cfc3e083da467c5448dcdcd717d3 Mon Sep 17 00:00:00 2001 From: Rambaud Pierrick <12rambau@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:08:41 +0000 Subject: [PATCH 13/14] fix: solve nox issues --- docs/conf.py | 41 ----------------------------------------- ipygee/__init__.py | 15 --------------- ipygee/asset.py | 16 ++++++++-------- tests/conftest.py | 2 +- tests/test_ipygee.py | 7 ------- 5 files changed, 9 insertions(+), 72 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1e5be55..64a6e48 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,13 +6,7 @@ """ # -- Path setup ---------------------------------------------------------------- -import os -import re from datetime import datetime -from pathlib import Path - -import ee -import httplib2 # -- Project information ------------------------------------------------------- project = "ipygee" @@ -70,38 +64,3 @@ # -- Options for intersphinx output -------------------------------------------- intersphinx_mapping = {} - -# -- Script to authenticate to Earthengine using a token ----------------------- -def gee_configure() -> None: - """Initialize earth engine according to the environment. - - It will use the creddential file if the EARTHENGINE_TOKEN env variable exist. - Otherwise it use the simple Initialize command (asking the user to register if necessary). - """ - # only do the initialization if the credential are missing - if not ee.data._credentials: - - # if the credentials token is asved in the environment use it - if "EARTHENGINE_TOKEN" in os.environ: - - # get the token from environment variable - ee_token = os.environ["EARTHENGINE_TOKEN"] - - # as long as RDT quote the token, we need to remove the quotes before writing - # the string to the file - pattern = r"^'[^']*'$" - if re.match(pattern, ee_token) is not None: - ee_token = ee_token[1:-1] - - # write the token to the appropriate folder - credential_folder_path = Path.home() / ".config" / "earthengine" - credential_folder_path.mkdir(parents=True, exist_ok=True) - credential_file_path = credential_folder_path / "credentials" - credential_file_path.write_text(ee_token) - - # if the user is in local development the authentication should - # already be available - ee.Initialize(http_transport=httplib2.Http()) - - -gee_configure() diff --git a/ipygee/__init__.py b/ipygee/__init__.py index d33f2b0..944f9b1 100644 --- a/ipygee/__init__.py +++ b/ipygee/__init__.py @@ -3,18 +3,3 @@ __version__ = "0.0.0" __author__ = "Pierrick Rambaud" __email__ = "pierrick.rambaud49@gmail.com" - - -class Hello: - """Hello world class.""" - - msg = "hello world !" - "the message to print" - - def hello_world(self) -> str: - """Hello world demo method. - - Returns: - the hello world string - """ - return self.msg diff --git a/ipygee/asset.py b/ipygee/asset.py index d9190e8..253499d 100644 --- a/ipygee/asset.py +++ b/ipygee/asset.py @@ -1,7 +1,7 @@ """The asset manager widget code and functionalities.""" from __future__ import annotations -from typing import List, Optional +from typing import List import ee import geetools # noqa @@ -37,22 +37,22 @@ class AssetManager(v.Flex): # -- Widgets --------------------------------------------------------------- - w_new: Optional[v.Btn] = None + w_new: v.Btn "The new btn on the top of the asset manager" - w_reload: Optional[v.Btn] = None + w_reload: v.Btn "The reload btn at the top of the asset manager" - w_search: Optional[v.Btn] = None + w_search: v.Btn "The search button to crowl into the existing items" - w_selected: Optional[v.TextField] = None + w_selected: v.TextField "The field where the user can see the asset Id of the selected item" - w_list: Optional[v.List] = None + w_list: v.List "The list of items displayed in the asset manager" - w_card: Optional[v.Card] = None + w_card: v.Card "The card hosting the list of items" def __init__(self): @@ -102,7 +102,7 @@ def get_items(self) -> List[v.ListItem]: # split the folders and the files to display the folders first # cloud bucket will be considered as folders - folder_list, file_list = [], [] + folder_list, file_list = [], [] # type: ignore[var-annotated] # walk the list of items and generate a list of ListItem using all the appropriate features # first we extract the type to deduce the icon and color, then we build the Items with the diff --git a/tests/conftest.py b/tests/conftest.py index 193fe44..ece41e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,4 +5,4 @@ def pytest_configure(): """Configure test environment.""" - pytest_gee.init_ee_from_token() + pytest_gee.init_ee_from_service_account() diff --git a/tests/test_ipygee.py b/tests/test_ipygee.py index 962c277..e01301d 100644 --- a/tests/test_ipygee.py +++ b/tests/test_ipygee.py @@ -1,13 +1,6 @@ """Test the ipygee package.""" import ee -import ipygee - - -def test_hello_world(): - """Hello world test.""" - assert ipygee.Hello().hello_world() == "hello world !" - def test_gee_connection(): """Test the geeconnection is working.""" From 45ce0ac7d51b94d4a9dabc3d50a8f836477c388c Mon Sep 17 00:00:00 2001 From: Rambaud Pierrick <12rambau@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:11:36 +0000 Subject: [PATCH 14/14] build: add the earthengine variables to the build --- .github/workflows/unit.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 5296788..26f5686 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -7,7 +7,8 @@ on: pull_request: env: - EARTHENGINE_TOKEN: ${{ secrets.EARTHENGINE_TOKEN }} + EARTHENGINE_PROJECT: ${{ secrets.EARTHENGINE_PROJECT }} + EARTHENGINE_SERVICE_ACCOUNT: ${{ secrets.EARTHENGINE_SERVICE_ACCOUNT }} jobs: lint: