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 30, 2025
1 parent 99969e1 commit 7297d9f
Show file tree
Hide file tree
Showing 9 changed files with 88 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
9 changes: 4 additions & 5 deletions docs/federation-extension.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ 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 +57,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 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
93 changes: 54 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,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.
.. seealso:: :ref:`federation-extension`
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="collection listing", auto_warn=auto_warn
)

class ProcessListingResponse(list):
"""
Expand All @@ -86,22 +90,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 +116,16 @@ 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.
.. seealso:: :ref:`federation-extension`
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="process listing", auto_warn=auto_warn
)

class JobListingResponse(list):
"""
Expand All @@ -132,22 +140,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 +164,16 @@ 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.
.. seealso:: :ref:`federation-extension`
"""
return federation_extension.get_federation_missing(
data=self._data, resource_name="job listing", auto_warn=auto_warn
)

class LogsResponse(list):
"""
Expand All @@ -178,20 +190,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 +223,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 +239,13 @@ 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.
.. seealso:: :ref:`federation-extension`
"""
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"},
}
6 changes: 3 additions & 3 deletions tests/rest/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3325,7 +3325,7 @@ def test_list_collections_extra_metadata(requests_mock, caplog):
collections = con.list_collections()
assert collections == [{"id": "S2"}, {"id": "NDVI"}]
assert collections.links == [Link(rel="next", href="https://oeo.test/collections?page=2", type=None, title=None)]
assert collections.ext_federation.missing == ["oeob"]
assert collections.ext_federation_missing() == ["oeob"]
assert "Partial collection listing: missing federation components: ['oeob']." in caplog.text


Expand Down Expand Up @@ -3403,7 +3403,7 @@ def test_list_processes_extra_metadata(requests_mock, caplog):
processes = conn.list_processes()
assert processes == [{"id": "add"}, {"id": "mask"}]
assert processes.links == [Link(rel="next", href="https://oeo.test/processes?page=2", type=None, title=None)]
assert processes.ext_federation.missing == ["oeob"]
assert processes.ext_federation_missing() == ["oeob"]
assert "Partial process listing: missing federation components: ['oeob']." in caplog.text


Expand Down Expand Up @@ -3715,7 +3715,7 @@ def test_list_udps_extra_metadata(self, requests_mock, test_data, caplog):
udps = conn.list_user_defined_processes()
assert udps == [{"id": "myevi"}]
assert udps.links == [Link(rel="about", href="https://oeo.test/my-evi")]
assert udps.ext_federation.missing == ["oeob"]
assert udps.ext_federation_missing() == ["oeob"]
assert "Partial process listing: missing federation components: ['oeob']." in caplog.text


Expand Down
Loading

0 comments on commit 7297d9f

Please sign in to comment.