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 75cbe2b
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 91 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)
60 changes: 27 additions & 33 deletions openeo/rest/models/federation_extension.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
import logging
from typing import List, Union
import textwrap
from typing import Dict, List, Union

_log = logging.getLogger(__name__)

class FederationExtension:
"""
Wrapper the openEO Federation extension as defined by
https://github.com/Open-EO/openeo-api/tree/master/extensions/federation
.. seealso:: :ref:`federation-extension`
"""

__slots__ = ["_data"]

def __init__(self, data: dict):
self._data = data
def get_backend_details(data: dict) -> Union[Dict[str, dict], None]:
"""
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)

@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()`):
def get_federation_missing(data: dict, *, resource_name: str, auto_warn: bool = True) -> Union[List[str], None]:
missing = data.get("federation:missing", None)
if auto_warn and missing:
_log.warning(f"Partial {resource_name}: missing federation components: {missing!r}.")
return missing

.. code-block:: pycon

>>> collections = connection.list_collections()
>>> collections.ext_federation.missing
["backend1"]
def get_federation_missing_doc(attribute_name: str = "ext_federation_missing", prefix: str = " ") -> str:
# TODO: is there a cleaner way to append to doc strings, using some standard Sphinx API?
doc = f"""
.. py:attribute:: {attribute_name}
:type: Union[None, List[str]]
: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)
List of backends IDs (from the federation)
that were not available during the resource listing request.
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}.")
.. seealso:: :ref:`federation-extension`
"""
return textwrap.indent(
"\n\n" + textwrap.dedent(doc).strip() + "\n\n",
prefix=prefix,
)
71 changes: 33 additions & 38 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 @@ -51,14 +51,18 @@ class CollectionListingResponse(list):
.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__doc__ += federation_extension.get_federation_missing_doc()

def __init__(self, response_data: dict, *, warn_on_federation_missing: bool = True):
__slots__ = ["_data", "ext_federation_missing"]

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 = federation_extension.get_federation_missing(
data=response_data, resource_name="collection listing"
)

def _repr_html_(self):
return render_component(component="collections", data=self)
Expand All @@ -68,11 +72,6 @@ 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)


class ProcessListingResponse(list):
"""
Expand All @@ -94,14 +93,18 @@ class ProcessListingResponse(list):
.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__doc__ += federation_extension.get_federation_missing_doc()

def __init__(self, response_data: dict, *, warn_on_federation_missing: bool = True):
__slots__ = ["_data", "ext_federation_missing"]

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 = federation_extension.get_federation_missing(
data=response_data, resource_name="process listing"
)

def _repr_html_(self):
return render_component(
Expand All @@ -113,10 +116,6 @@ 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)


class JobListingResponse(list):
Expand All @@ -140,14 +139,19 @@ class JobListingResponse(list):
.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__doc__ += federation_extension.get_federation_missing_doc()


def __init__(self, response_data: dict, *, warn_on_federation_missing: bool = True):
__slots__ = ["_data", "ext_federation_missing"]

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 = federation_extension.get_federation_missing(
data=response_data, resource_name="job listing"
)

def _repr_html_(self):
return render_component(component="data-table", data=self, parameters={"columns": "jobs"})
Expand All @@ -157,11 +161,6 @@ 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)


class LogsResponse(list):
"""
Expand All @@ -187,11 +186,11 @@ class LogsResponse(list):
.. versionadded:: 0.38.0
"""

__slots__ = ["_data"]
__doc__ += federation_extension.get_federation_missing_doc()

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

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

logs = response_data.get("logs", [])
Expand All @@ -214,8 +213,9 @@ 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 = federation_extension.get_federation_missing(
data=response_data, resource_name="log listing"
)

def _repr_html_(self):
return render_component(component="logs", data=self)
Expand All @@ -229,8 +229,3 @@ def logs(self) -> List[LogEntry]:
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)
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
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
4 changes: 2 additions & 2 deletions tests/rest/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def test_get_job_logs_extra_metadata(con100, requests_mock, log_generator):
assert logs.links == [
Link(rel="next", href="https://oeo.test/jobs/f00ba5/logs?offset=123abc"),
]
assert logs.ext_federation.missing == ["eoeb"]
assert logs.ext_federation_missing == ["eoeb"]


def test_get_job_logs_level_handling_default(con100, requests_mock, log_generator):
Expand Down Expand Up @@ -878,5 +878,5 @@ def get_jobs(request, context):
{"id": "job456", "status": "created", "created": "2021-03-22T10:00:00Z"},
]
assert jobs.links == [Link(rel="next", href="https://oeo.test/jobs?limit=2&offset=2")]
assert jobs.ext_federation.missing == ["oeob"]
assert jobs.ext_federation_missing == ["oeob"]
assert "Partial job listing: missing federation components: ['oeob']." in caplog.text

0 comments on commit 75cbe2b

Please sign in to comment.