Skip to content

Commit

Permalink
Issue #668 eliminate FederationExtension abstraction
Browse files Browse the repository at this point in the history
- for user API that's simpler to navigate
- revert to methods iso properties (to allow future tweaks, e.g. return parsed object instead of raw dicts)
  • Loading branch information
soxofaan committed Jan 31, 2025
1 parent ebe9f9c commit e577200
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 98 deletions.
3 changes: 0 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ openeo.rest.models
.. automodule:: openeo.rest.models.general
:members:

.. automodule:: openeo.rest.models.federation_extension
:members: FederationExtension

.. automodule:: openeo.rest.models.logs
:members: LogEntry, normalize_log_level

Expand Down
12 changes: 7 additions & 5 deletions docs/federation-extension.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ in a couple of resources.
.. versionadded:: 0.38.0
initial support to access federation extension related metadata.

.. warning:: this API is experimental and subject to change.


Backend details
---------------

Participating backends in a federation are listed under the ``federation`` field
of the capabilities document (``GET /``) and can be inspected
using :py:meth:`OpenEoCapabilities.get_federation() <openeo.rest.capabilities.OpenEoCapabilities.get_federation>`:
using :py:meth:`OpenEoCapabilities.ext_federation_backend_details() <openeo.rest.capabilities.OpenEoCapabilities.ext_federation_backend_details>`:

.. code-block:: python
import openeo
connection = openeo.connect(url=...)
capabilities = connection.capabilities()
print("Federated backends:", capabilities.get_federation())
print("Federated backends:", capabilities.ext_federation_backend_details())
Unavailable backends (``federation:missing``)
Expand All @@ -57,12 +60,11 @@ and can be inspected as follows:
connection = openeo.connect(url=...)
collections = connection.list_collections()
print("Number of collections:", len(collections))
print("Missing federation components:", collections.ext_federation.missing)
print("Missing federation components:", collections.ext_federation_missing())
Note that the ``collections`` object in this example, returned by
:py:meth:`Connection.list_collections() <openeo.rest.connection.Connection.list_collections>`,
acts at the surface as a simple list of dictionaries with collection metadata,
but also provides additional properties/methods like
:py:attr:`ext_federation <openeo.rest.models.general.CollectionListingResponse.ext_federation>`.
This is an accessor (an instance of :py:class:`FederationExtension <openeo.rest.models.federation_extension.FederationExtension>`)
:py:attr:`ext_federation_missing() <openeo.rest.models.general.CollectionListingResponse.ext_federation_missing>`.
10 changes: 4 additions & 6 deletions openeo/rest/capabilities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict, List, Optional, Union

from openeo.internal.jupyter import render_component
from openeo.rest.models import federation_extension
from openeo.util import deep_get
from openeo.utils.version import ApiVersionException, ComparableVersion

Expand Down Expand Up @@ -54,16 +55,13 @@ def list_plans(self) -> List[dict]:
def _repr_html_(self):
return render_component("capabilities", data=self.capabilities, parameters={"url": self.url})

def get_federation(self) -> Union[Dict[str, dict], None]:
def ext_federation_backend_details(self) -> Union[Dict[str, dict], None]:
"""
Lists all back-ends (with details, such as URL) that are part of the federation
if this backend acts as a federated backend,
as specified in the openEO Federation Extension.
Returns ``None`` otherwise
Returns ``None`` otherwise.
.. versionadded:: 0.38.0
"""
# TODO: also check related conformance class in `/conformance`?
# TODO: refactor into FederationExtension
# TODO: return a richer object instead of raw dicts?
return self.get("federation")
return federation_extension.get_backend_details(data=self.capabilities)
55 changes: 18 additions & 37 deletions openeo/rest/models/federation_extension.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
import logging
from typing import List, Union
from typing import Dict, List, Union

_log = logging.getLogger(__name__)

class FederationExtension:
def get_backend_details(data: dict) -> Union[Dict[str, dict], None]:
"""
Wrapper the openEO Federation extension as defined by
https://github.com/Open-EO/openeo-api/tree/master/extensions/federation
.. seealso:: :ref:`federation-extension`
Get federated backend details from capabilities document (``GET /``)
at "federation" field
"""
# TODO: return a richer object instead of raw dicts?
return data.get("federation", None)

__slots__ = ["_data"]

def __init__(self, data: dict):
self._data = data

@property
def missing(self) -> Union[List[str], None]:
"""
Get the ``federation:missing`` property (if any) of the resource,
which lists back-ends that were not available during the request.
Example usage with collection listing request
(using :py:meth:`~openeo.rest.connection.Connection.list_collections()`):
.. code-block:: pycon

>>> collections = connection.list_collections()
>>> collections.ext_federation.missing
["backend1"]
:return: list of back-end IDs that were not available.
Or None, when ``federation:missing`` is not present in response.
"""
return self._data.get("federation:missing", None)

def warn_on_missing(self, resource_name: str) -> None:
"""
Warn about presence of non-empty ``federation:missing`` in the resource.
"""
missing = self.missing
if missing:
_log.warning(f"Partial {resource_name}: missing federation components: {missing!r}.")
def get_federation_missing(data: dict, *, resource_name: str, auto_warn: bool = False) -> Union[List[str], None]:
"""
Get "federation:missing" field from response data, if present.
:param data: response data
:param resource_name: name of the requested resource (listing)
:param auto_warn: whether to automatically log a warning if missing federation components are detected.
"""
# TODO: options to return richer objects (e.g. resolve backend id to URL/title)
missing = data.get("federation:missing", None)
if auto_warn and missing:
_log.warning(f"Partial {resource_name}: missing federation components: {missing!r}.")
return missing
108 changes: 69 additions & 39 deletions openeo/rest/models/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import List, Optional, Union

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


Expand Down Expand Up @@ -43,8 +43,6 @@ 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 warn_on_federation_missing: whether to automatically warn
about missing federation components.
.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_collections()`
Expand All @@ -53,12 +51,12 @@ class CollectionListingResponse(list):

__slots__ = ["_data"]

def __init__(self, response_data: dict, *, warn_on_federation_missing: bool = True):
def __init__(self, response_data: dict):
self._data = response_data
# Mimic original list of collection metadata dictionaries
super().__init__(response_data["collections"])
if warn_on_federation_missing:
self.ext_federation.warn_on_missing(resource_name="collection listing")

self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
return render_component(component="collections", data=self)
Expand All @@ -68,11 +66,20 @@ def links(self) -> List[Link]:
"""Get links related to this resource."""
return [Link.from_dict(d) for d in self._data.get("links", [])]

@property
def ext_federation(self) -> FederationExtension:
"""Accessor for federation extension data related to this resource."""
return FederationExtension(self._data)
def ext_federation_missing(self, *, auto_warn: bool = False) -> Union[None, List[str]]:
"""
List of backends IDs (from the federation)
that were not available during the resource listing request.
:param auto_warn: whether to automatically log a warning if missing federation components are detected.
.. seealso:: :ref:`federation-extension`
.. warning:: this API is experimental and subject to change.
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="collection listing", auto_warn=auto_warn
)

class ProcessListingResponse(list):
"""
Expand All @@ -86,22 +93,21 @@ 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 warn_on_federation_missing: whether to automatically warn
about missing federation components.
.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_processes()`
.. versionadded:: 0.38.0
"""


__slots__ = ["_data"]

def __init__(self, response_data: dict, *, warn_on_federation_missing: bool = True):
def __init__(self, response_data: dict):
self._data = response_data
# Mimic original list of process metadata dictionaries
super().__init__(response_data["processes"])
if warn_on_federation_missing:
self.ext_federation.warn_on_missing(resource_name="process listing")

self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
return render_component(
Expand All @@ -113,11 +119,20 @@ def links(self) -> List[Link]:
"""Get links related to this resource."""
return [Link.from_dict(d) for d in self._data.get("links", [])]

@property
def ext_federation(self) -> FederationExtension:
"""Accessor for federation extension data related to this resource."""
return FederationExtension(self._data)
def ext_federation_missing(self, *, auto_warn: bool = False) -> Union[None, List[str]]:
"""
List of backends IDs (from the federation)
that were not available during the resource listing request.
:param auto_warn: whether to automatically log a warning if missing federation components are detected.
.. seealso:: :ref:`federation-extension`
.. warning:: this API is experimental and subject to change.
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="process listing", auto_warn=auto_warn
)

class JobListingResponse(list):
"""
Expand All @@ -132,22 +147,21 @@ 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 warn_on_federation_missing: whether to automatically warn
about missing federation components.
.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_jobs()`
.. versionadded:: 0.38.0
"""


__slots__ = ["_data"]

def __init__(self, response_data: dict, *, warn_on_federation_missing: bool = True):
def __init__(self, response_data: dict):
self._data = response_data
# Mimic original list of process metadata dictionaries
super().__init__(response_data["jobs"])
if warn_on_federation_missing:
self.ext_federation.warn_on_missing(resource_name="job listing")

self.ext_federation_missing(auto_warn=True)

def _repr_html_(self):
return render_component(component="data-table", data=self, parameters={"columns": "jobs"})
Expand All @@ -157,11 +171,20 @@ def links(self) -> List[Link]:
"""Get links related to this resource."""
return [Link.from_dict(d) for d in self._data.get("links", [])]

@property
def ext_federation(self) -> FederationExtension:
"""Accessor for federation extension data related to this resource."""
return FederationExtension(self._data)
def ext_federation_missing(self, *, auto_warn: bool = False) -> Union[None, List[str]]:
"""
List of backends IDs (from the federation)
that were not available during the resource listing request.
:param auto_warn: whether to automatically log a warning if missing federation components are detected.
.. seealso:: :ref:`federation-extension`
.. warning:: this API is experimental and subject to change.
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="job listing", auto_warn=auto_warn
)

class LogsResponse(list):
"""
Expand All @@ -178,20 +201,17 @@ class LogsResponse(list):
:param response_data: response data from a ``GET /jobs/{job_id}/logs``
or ``GET /services/{service_id}/logs`` request.
:param warn_on_federation_missing: whether to automatically warn
about missing federation components.
.. seealso:: :py:meth:`~openeo.rest.job.BatchJob.logs()`
and :py:meth:`~openeo.rest.service.Service.logs()`
.. versionadded:: 0.38.0
"""


__slots__ = ["_data"]

def __init__(
self, response_data: dict, *, log_level: Optional[str] = None, warn_on_federation_missing: bool = True
):
def __init__(self, response_data: dict, *, log_level: Optional[str] = None):
self._data = response_data

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

if warn_on_federation_missing:
self.ext_federation.warn_on_missing(resource_name="log listing")
self.ext_federation_missing(auto_warn=True)


def _repr_html_(self):
return render_component(component="logs", data=self)
Expand All @@ -230,7 +250,17 @@ def links(self) -> List[Link]:
"""Get links related to this resource."""
return [Link.from_dict(d) for d in self._data.get("links", [])]

@property
def ext_federation(self) -> FederationExtension:
"""Accessor for federation extension data related to this resource."""
return FederationExtension(self._data)
def ext_federation_missing(self, *, auto_warn: bool = False) -> Union[None, List[str]]:
"""
List of backends IDs (from the federation)
that were not available during the resource listing request.
:param auto_warn: whether to automatically log a warning if missing federation components are detected.
.. seealso:: :ref:`federation-extension`
.. warning:: this API is experimental and subject to change.
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="log listing", auto_warn=auto_warn
)
2 changes: 1 addition & 1 deletion tests/rest/models/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ def test_links(self):
)
def test_federation_missing(self, data, expected):
collections = CollectionListingResponse(data)
assert collections.ext_federation.missing == expected
assert collections.ext_federation_missing() == expected
4 changes: 2 additions & 2 deletions tests/rest/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_list_plans(self):
assert OpenEoCapabilities({"billing": {"plans": [{"name": "free"}]}}).list_plans() == [{"name": "free"}]

def test_federation_absent(self):
assert OpenEoCapabilities({}).get_federation() is None
assert OpenEoCapabilities({}).ext_federation_backend_details() is None

def test_federation_present(self):
data = {
Expand All @@ -62,7 +62,7 @@ def test_federation_present(self):
},
}
capabilities = OpenEoCapabilities(data)
assert capabilities.get_federation() == {
assert capabilities.ext_federation_backend_details() == {
"a": {"url": "https://a.test/openeo/v2", "title": "A backend"},
"bb": {"url": "https://openeo.b.test/v9"},
}
Loading

0 comments on commit e577200

Please sign in to comment.