diff --git a/docs/api.rst b/docs/api.rst index 0f298867f..cf61ba115 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 diff --git a/docs/federation-extension.rst b/docs/federation-extension.rst index a36fd6a28..33dc6d590 100644 --- a/docs/federation-extension.rst +++ b/docs/federation-extension.rst @@ -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() `: +using :py:meth:`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``) @@ -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() `, acts at the surface as a simple list of dictionaries with collection metadata, but also provides additional properties/methods like -:py:attr:`ext_federation `. -This is an accessor (an instance of :py:class:`FederationExtension `) +:py:attr:`ext_federation_missing `. diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index fb5fa0e9b..768093f6f 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -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 @@ -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) diff --git a/openeo/rest/models/federation_extension.py b/openeo/rest/models/federation_extension.py index bff7d5f62..ce07008a5 100644 --- a/openeo/rest/models/federation_extension.py +++ b/openeo/rest/models/federation_extension.py @@ -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, + ) diff --git a/openeo/rest/models/general.py b/openeo/rest/models/general.py index d3df0c4c2..aef6cfc2c 100644 --- a/openeo/rest/models/general.py +++ b/openeo/rest/models/general.py @@ -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 @@ -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) @@ -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): """ @@ -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( @@ -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): @@ -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"}) @@ -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): """ @@ -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", []) @@ -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) @@ -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) diff --git a/tests/rest/models/test_general.py b/tests/rest/models/test_general.py index aab66c73b..c45f31405 100644 --- a/tests/rest/models/test_general.py +++ b/tests/rest/models/test_general.py @@ -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 diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index de310461b..0b5ea9cf2 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -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 @@ -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 @@ -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 diff --git a/tests/rest/test_job.py b/tests/rest/test_job.py index dc6bda3ce..5136f0139 100644 --- a/tests/rest/test_job.py +++ b/tests/rest/test_job.py @@ -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): @@ -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