Skip to content

Support for the federation extension in Jupyter visual outputs #771

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- More extensive band detection for `load_stac` use cases, including the common `bands` metadata introduced with STAC 1.1 ([#699](https://github.com/Open-EO/openeo-python-client/issues/699), [#692](https://github.com/Open-EO/openeo-python-client/issues/692), [#586](https://github.com/Open-EO/openeo-python-client/issues/586)).
- Improved support for Federation Extension in Jupyter notebook context ([#668](https://github.com/Open-EO/openeo-python-client/issues/668))

### Changed

Expand Down
7 changes: 4 additions & 3 deletions openeo/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,13 +519,14 @@ class CollectionMetadata(CubeMetadata):

"""

def __init__(self, metadata: dict, dimensions: List[Dimension] = None):
def __init__(self, metadata: dict, dimensions: List[Dimension] = None, _federation: Optional[dict] = None):
self._orig_metadata = metadata
if dimensions is None:
dimensions = self._parse_dimensions(self._orig_metadata)

super().__init__(dimensions=dimensions)

self._federation = _federation

@classmethod
def _parse_dimensions(cls, spec: dict, complain: Callable[[str], None] = warnings.warn) -> List[Dimension]:
"""
Expand Down Expand Up @@ -640,7 +641,7 @@ def extent(self) -> dict:
return self._orig_metadata.get("extent")

def _repr_html_(self):
return render_component("collection", data=self._orig_metadata)
return render_component("collection", data=self._orig_metadata, parameters={"federation": self._federation})

def __str__(self) -> str:
bands = self.band_names if self.has_band_dimension() else "no bands dimension"
Expand Down
67 changes: 52 additions & 15 deletions openeo/rest/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
JobListingResponse,
ProcessListingResponse,
ValidationResponse,
federation_extension,
)
from openeo.rest.result import SaveResult
from openeo.rest.service import Service
Expand Down Expand Up @@ -734,7 +735,7 @@ def list_collections(self) -> CollectionListingResponse:
# TODO: add caching #383, but reset cache on auth change #254
# TODO #677 add pagination support?
data = self.get("/collections", expected_status=200).json()
return CollectionListingResponse(response_data=data)
return CollectionListingResponse(response_data=data, connection=self)

def list_collection_ids(self) -> List[str]:
"""
Expand Down Expand Up @@ -776,7 +777,13 @@ def list_file_formats(self) -> dict:
key="file_formats",
load=lambda: self.get('/file_formats', expected_status=200).json()
)
return VisualDict("file-formats", data=formats)
federation_missing = federation_extension.get_federation_missing(data=formats, resource_name="file_formats")
federation = self.capabilities().ext_federation_backend_details()
return VisualDict(
"file-formats",
data=formats,
parameters={"missing": federation_missing, "federation": federation},
)

def list_service_types(self) -> dict:
"""
Expand All @@ -800,17 +807,24 @@ def list_udf_runtimes(self) -> dict:
key="udf_runtimes",
load=lambda: self.get('/udf_runtimes', expected_status=200).json()
)
return VisualDict("udf-runtimes", data=runtimes)
federation = self.capabilities().ext_federation_backend_details()
return VisualDict("udf-runtimes", data=runtimes, parameters={"federation": federation})

def list_services(self) -> dict:
def list_services(self) -> list:
"""
Loads all available services of the authenticated user.

:return: data_dict: Dict All available services
"""
# TODO return parsed service objects
services = self.get('/services', expected_status=200).json()["services"]
return VisualList("data-table", data=services, parameters={'columns': 'services'})
federation_missing = federation_extension.get_federation_missing(data=services, resource_name="services")
federation = self.capabilities().ext_federation_backend_details()
return VisualList(
"data-table",
data=services,
parameters={"columns": "services", "missing": federation_missing, "federation": federation},
)

def describe_collection(self, collection_id: str) -> dict:
"""
Expand All @@ -827,7 +841,8 @@ def describe_collection(self, collection_id: str) -> dict:
# TODO: duplication with `Connection.collection_metadata`: deprecate one or the other?
# TODO: add caching #383
data = self.get(f"/collections/{collection_id}", expected_status=200).json()
return VisualDict("collection", data=data)
federation = self.capabilities().ext_federation_backend_details()
return VisualDict("collection", data=data, parameters={"federation": federation})

def collection_items(
self,
Expand Down Expand Up @@ -865,11 +880,22 @@ def collection_items(
if limit is not None and limit > 0:
params['limit'] = limit

return paginate(self, url, params, lambda response, page: VisualDict("items", data = response, parameters = {'show-map': True, 'heading': 'Page {} - Items'.format(page)}))
federation = self.capabilities().ext_federation_backend_details()
return paginate(
self,
url,
params,
lambda response, page: VisualDict(
"items",
data=response,
parameters={"show-map": True, "heading": "Page {} - Items".format(page), "federation": federation},
),
)

def collection_metadata(self, name) -> CollectionMetadata:
# TODO: duplication with `Connection.describe_collection`: deprecate one or the other?
return CollectionMetadata(metadata=self.describe_collection(name))
federation = self.capabilities().ext_federation_backend_details()
return CollectionMetadata(metadata=self.describe_collection(name), _federation=federation)

def list_processes(self, namespace: Optional[str] = None) -> ProcessListingResponse:
"""
Expand All @@ -891,7 +917,7 @@ def list_processes(self, namespace: Optional[str] = None) -> ProcessListingRespo
)
else:
response = self.get("/processes/" + namespace, expected_status=200).json()
return ProcessListingResponse(response_data=response)
return ProcessListingResponse(response_data=response, connection=self)

def describe_process(self, id: str, namespace: Optional[str] = None) -> dict:
"""
Expand All @@ -904,9 +930,14 @@ def describe_process(self, id: str, namespace: Optional[str] = None) -> dict:
"""

processes = self.list_processes(namespace)
federation = self.capabilities().ext_federation_backend_details()
for process in processes:
if process["id"] == id:
return VisualDict("process", data=process, parameters={'show-graph': True, 'provide-download': False})
return VisualDict(
"process",
data=process,
parameters={"show-graph": True, "provide-download": False, "federation": federation},
)

raise OpenEoClientException("Process does not exist.")

Expand All @@ -933,7 +964,7 @@ def list_jobs(self, limit: Union[int, None] = 100) -> JobListingResponse:
# TODO: Parse the result so that Job classes returned?
# TODO: when pagination is enabled: how to expose link to next page?
resp = self.get("/jobs", params={"limit": limit}, expected_status=200).json()
return JobListingResponse(response_data=resp)
return JobListingResponse(response_data=resp, connection=self)

def assert_user_defined_process_support(self):
"""
Expand Down Expand Up @@ -996,7 +1027,7 @@ def list_user_defined_processes(self) -> ProcessListingResponse:
# TODO #677 add pagination support?
self.assert_user_defined_process_support()
data = self.get("/process_graphs", expected_status=200).json()
return ProcessListingResponse(response_data=data)
return ProcessListingResponse(response_data=data, connection=self)

def user_defined_process(self, user_defined_process_id: str) -> RESTUserDefinedProcess:
"""
Expand Down Expand Up @@ -1496,9 +1527,15 @@ def list_files(self) -> List[UserFile]:

:return: List of the user-uploaded files.
"""
files = self.get('/files', expected_status=200).json()['files']
files = [UserFile.from_metadata(metadata=f, connection=self) for f in files]
return VisualList("data-table", data=files, parameters={'columns': 'files'})
data = self.get("/files", expected_status=200).json()
files = [UserFile.from_metadata(metadata=f, connection=self) for f in data.get("files", [])]
federation_missing = federation_extension.get_federation_missing(data=data, resource_name="files")
federation = self.capabilities().ext_federation_backend_details()
return VisualList(
"data-table",
data=files,
parameters={"columns": "files", "missing": federation_missing, "federation": federation},
)

def get_file(
self, path: Union[str, PurePosixPath], metadata: Optional[dict] = None
Expand Down
13 changes: 10 additions & 3 deletions openeo/rest/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,15 @@ def __repr__(self):

def _repr_html_(self):
data = self.describe()
currency = self.connection.capabilities().currency()
return render_component('job', data=data, parameters={'currency': currency})
capabilities = self.connection.capabilities()
return render_component(
"job",
data=data,
parameters={
"currency": capabilities.currency(),
"federation": capabilities.ext_federation_backend_details(),
},
)

@openeo_endpoint("GET /jobs/{job_id}")
def describe(self) -> dict:
Expand Down Expand Up @@ -235,7 +242,7 @@ def logs(self, offset: Optional[str] = None, level: Union[str, int, None] = None
if level is not None:
params["level"] = log_level_name(level)
response_data = self.connection.get(url, params=params, expected_status=200).json()
return LogsResponse(response_data=response_data, log_level=level)
return LogsResponse(response_data=response_data, log_level=level, connection=self.connection)

@deprecated("Use start_and_wait instead", version="0.39.0")
def run_synchronous(
Expand Down
71 changes: 58 additions & 13 deletions openeo/rest/models/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import functools
from dataclasses import dataclass
from typing import List, Optional, Union
from typing import TYPE_CHECKING, List, Optional, Union

from openeo.internal.jupyter import render_component
from openeo.rest.models import federation_extension
from openeo.rest.models.logs import LogEntry, normalize_log_level

if TYPE_CHECKING:
from openeo.rest.connection import Connection

@dataclass(frozen=True)
class Link:
Expand Down Expand Up @@ -43,23 +45,33 @@ class CollectionListingResponse(list):
but now also provides methods/properties to access additional response data.

:param response_data: response data from a ``GET /collections`` request
:param connection: optional connection object to use for federation extension

.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_collections()`

.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__slots__ = ["_data", "_connection"]

def __init__(self, response_data: dict):
def __init__(self, response_data: dict, connection: Optional[Connection] = None):
self._data = response_data
# Mimic original list of collection metadata dictionaries
super().__init__(response_data["collections"])

self._connection = connection
self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
return render_component(component="collections", data=self)
federation = self._connection.capabilities().ext_federation_backend_details() if self._connection else None
return render_component(
component="collections",
data=self,
parameters={
"missing": self.ext_federation_missing(),
"federation": federation,
},
)

@property
def links(self) -> List[Link]:
Expand Down Expand Up @@ -94,24 +106,34 @@ class ProcessListingResponse(list):
but now also provides methods/properties to access additional response data.

:param response_data: response data from a ``GET /processes`` request
:param connection: optional connection object to use for federation extension

.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_processes()`

.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__slots__ = ["_data", "_connection"]

def __init__(self, response_data: dict):
def __init__(self, response_data: dict, connection: Optional[Connection] = None):
self._data = response_data
# Mimic original list of process metadata dictionaries
super().__init__(response_data["processes"])

self._connection = connection
self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
federation = self._connection.capabilities().ext_federation_backend_details() if self._connection else None
return render_component(
component="processes", data=self, parameters={"show-graph": True, "provide-download": False}
component="processes",
data=self,
parameters={
"show-graph": True,
"provide-download": False,
"missing": self.ext_federation_missing(),
"federation": federation,
},
)

@property
Expand Down Expand Up @@ -148,23 +170,34 @@ class JobListingResponse(list):
but now also provides methods/properties to access additional response data.

:param response_data: response data from a ``GET /jobs`` request
:param connection: optional connection object to use for federation extension

.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_jobs()`

.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__slots__ = ["_data", "_connection"]

def __init__(self, response_data: dict):
def __init__(self, response_data: dict, connection: Optional[Connection] = None):
self._data = response_data
# Mimic original list of process metadata dictionaries
super().__init__(response_data["jobs"])

self._connection = connection
self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
return render_component(component="data-table", data=self, parameters={"columns": "jobs"})
federation = self._connection.capabilities().ext_federation_backend_details() if self._connection else None
return render_component(
component="data-table",
data=self,
parameters={
"columns": "jobs",
"missing": self.ext_federation_missing(),
"federation": federation,
},
)

@property
def links(self) -> List[Link]:
Expand Down Expand Up @@ -202,16 +235,19 @@ class LogsResponse(list):

:param response_data: response data from a ``GET /jobs/{job_id}/logs``
or ``GET /services/{service_id}/logs`` request.
:param connection: optional connection object to use for federation extension

.. seealso:: :py:meth:`~openeo.rest.job.BatchJob.logs()`
and :py:meth:`~openeo.rest.service.Service.logs()`

.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__slots__ = ["_data", "_connection"]

def __init__(self, response_data: dict, *, log_level: Optional[str] = None):
def __init__(
self, response_data: dict, *, log_level: Optional[str] = None, connection: Optional[Connection] = None
):
self._data = response_data

logs = response_data.get("logs", [])
Expand All @@ -234,10 +270,19 @@ def accept_level(level: str) -> bool:
# Mimic original list of process metadata dictionaries
super().__init__(logs)

self._connection = connection
self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
return render_component(component="logs", data=self)
federation = self._connection.capabilities().ext_federation_backend_details() if self._connection else None
return render_component(
component="logs",
data=self,
parameters={
"missing": self.ext_federation_missing(),
"federation": federation,
},
)

@property
def logs(self) -> List[LogEntry]:
Expand Down
Loading