Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jupyter integration: UI components for data discovery #170

Merged
merged 14 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions openeo/internal/jupyter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from collections import Mapping
import json

# These classes are proxies to visualize openEO responses nicely in Jupyter
# To show the actual list or dict in Jupyter, use repr() or print()

SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/@openeo/[email protected]/assets/openeo.js'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a todo note here

 TODO: get this script url dynamically e.g. from a https://editor.openeo.org/ quert or from a local config file, with hardcoded fallback?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. We could add it to a config file, but we can't and shouldn't get it through other means dynamically. The implementation is bound to the specific version and once we have a stable version 2.0.0 (in one or two weeks) it will be SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/@openeo/vue-components@2/assets/openeo.js', which will cover always the latest version of the 2.x branch and then we won't change it any time soon again as 3.x would be breaking anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep this PR open until we have a 2.0.0 release.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or merge now? This will not be released immediately anyway, but it will make it available to other people testing on master?

Copy link
Member Author

@m-mohr m-mohr Jan 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdries Sure, I'm fine with that. Just released another RC for vue-components so that all recent changes are in. Feel free to merge at any time.

Copy link
Member Author

@m-mohr m-mohr Jan 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not be released immediately anyway

@jdries But four hours later 😂 👍

COMPONENT_MAP = {
'file-format': 'format',
'file-formats': 'formats',
'service-type': 'service',
'service-types': 'services',
'udf-runtime': 'runtime',
'udf-runtimes': 'runtimes',
}

class JupyterIntegration:

def __init__(self, component: str, data = None, parameters: dict = {}):
self.component = component
self.parameters = parameters

# Set the data as the corresponding parameter in the Vue components
key = COMPONENT_MAP.get(component, component)
if data != None:
self.parameters[key] = data

def _repr_html_(self):
# Construct HTML, load Vue Components source files only if the openEO HTML tag is not yet defined
return """
<script>
if (!window.customElements || !window.customElements.get('openeo-{component}')) {{
var el = document.createElement('script');
el.src = "{script}";
document.head.appendChild(el);
}}
</script>
<openeo-{component}>
<script type="application/json">{props}</script>
</openeo-{component}>
""".format(
script = SCRIPT_URL,
component = self.component,
props = json.dumps(self.parameters)
)


class VisualDict(dict, JupyterIntegration):
m-mohr marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, component: str, data : dict, parameters: dict = {}):
JupyterIntegration.__init__(self, component, data, parameters)
dict.__init__(self, data)


class VisualList(list, JupyterIntegration):

def __init__(self, component: str, data : list, parameters: dict = {}):
JupyterIntegration.__init__(self, component, data, parameters)
list.__init__(self, data)
4 changes: 4 additions & 0 deletions openeo/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import List, Union, Tuple, Callable

from openeo.util import deep_get
from openeo.internal.jupyter import JupyterIntegration


class MetadataException(Exception):
Expand Down Expand Up @@ -392,3 +393,6 @@ def add_dimension(self, name: str, label: Union[str, float], type: str = None) -
else:
dim = Dimension(type=type or "other", name=name)
return self._clone_and_update(dimensions=self._dimensions + [dim])

def _repr_html_(self):
return JupyterIntegration('collection', data = self._orig_metadata)._repr_html_()
28 changes: 22 additions & 6 deletions openeo/rest/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from openeo.rest.rest_capabilities import RESTCapabilities
from openeo.rest.udp import RESTUserDefinedProcess, Parameter
from openeo.util import ensure_list, legacy_alias, dict_no_none
from openeo.internal.jupyter import VisualDict, VisualList

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -522,7 +523,8 @@ def list_collections(self) -> List[dict]:

:return: list of collection meta data dictionaries
"""
return self.get('/collections').json()["collections"]
data = self.get('/collections').json()["collections"]
return VisualList("collections", data = data)

def list_collection_ids(self) -> List[str]:
"""
Expand All @@ -539,7 +541,7 @@ def capabilities(self) -> RESTCapabilities:
:return: data_dict: Dict All available data types
"""
if "capabilities" not in self._capabilities_cache:
self._capabilities_cache["capabilities"] = RESTCapabilities(self.get('/').json())
self._capabilities_cache["capabilities"] = RESTCapabilities(self.get('/').json(), self._orig_url)
return self._capabilities_cache["capabilities"]


Expand All @@ -558,15 +560,27 @@ def list_file_formats(self) -> dict:
"""
if "file_formats" not in self._capabilities_cache:
self._capabilities_cache["file_formats"] = self.get('/file_formats').json()
return self._capabilities_cache["file_formats"]
return VisualDict("file-formats", data = self._capabilities_cache["file_formats"])

def list_service_types(self) -> dict:
"""
Loads all available service types.

:return: data_dict: Dict All available service types
"""
return self.get('/service_types').json()
if "service_types" not in self._capabilities_cache:
self._capabilities_cache["service_types"] = self.get('/service_types').json()
return VisualDict("service-types", data = self._capabilities_cache["service_types"])

def list_udf_runtimes(self) -> dict:
"""
Loads all available UDF runtimes.

:return: data_dict: Dict All available UDF runtimes
"""
if "udf_runtimes" not in self._capabilities_cache:
self._capabilities_cache["udf_runtimes"] = self.get('/udf_runtimes').json()
return VisualDict("udf-runtimes", data = self._capabilities_cache["udf_runtimes"])

def list_services(self) -> dict:
"""
Expand All @@ -585,7 +599,8 @@ def describe_collection(self, name) -> dict:
:param name: String Id of the collection
:return: data_dict: Dict Detailed information about the collection
"""
return self.get('/collections/{}'.format(name)).json()
data = self.get('/collections/{}'.format(name)).json()
return VisualDict("collection", data = data)

def collection_metadata(self, name) -> CollectionMetadata:
return CollectionMetadata(metadata=self.describe_collection(name))
Expand All @@ -597,7 +612,8 @@ def list_processes(self) -> List[dict]:

:return: processes_dict: Dict All available processes of the back end.
"""
return self.get('/processes').json()["processes"]
data = self.get('/processes').json()["processes"]
return VisualList("processes", data = data)

def list_jobs(self) -> dict:
"""
Expand Down
7 changes: 6 additions & 1 deletion openeo/rest/rest_capabilities.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from openeo.capabilities import Capabilities
from openeo.internal.jupyter import JupyterIntegration


class RESTCapabilities(Capabilities):
"""Represents REST capabilities of a connection / back end."""

def __init__(self, data: dict):
def __init__(self, data: dict, url: str = None):
super(RESTCapabilities, self).__init__(data)
self.capabilities = data
self.url = url

def api_version(self) -> str:
""" Get openEO version."""
Expand All @@ -32,3 +34,6 @@ def currency(self):
def list_plans(self):
""" List all billing plans."""
return self.capabilities.get('billing', {}).get('plans')

def _repr_html_(self):
return JupyterIntegration("capabilities", data = self.capabilities, parameters = {"url": self.url})._repr_html_()