Skip to content

Commit

Permalink
Issue #668 support federation extension on list_collections
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Jan 27, 2025
1 parent 73b81f9 commit 7eb6838
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 3 deletions.
10 changes: 10 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ openeo.rest.capabilities
:members: OpenEoCapabilities


openeo.rest.models
-------------------

.. automodule:: openeo.rest.models.general
:members:

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


openeo.api.process
--------------------

Expand Down
7 changes: 4 additions & 3 deletions openeo/rest/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
from openeo.rest.graph_building import CollectionProperty
from openeo.rest.job import BatchJob, RESTJob
from openeo.rest.mlmodel import MlModel
from openeo.rest.models.general import CollectionListingResponse
from openeo.rest.service import Service
from openeo.rest.udp import Parameter, RESTUserDefinedProcess
from openeo.rest.userfile import UserFile
Expand Down Expand Up @@ -898,7 +899,7 @@ def describe_account(self) -> dict:
def user_jobs(self) -> List[dict]:
return self.list_jobs()

def list_collections(self) -> List[dict]:
def list_collections(self) -> CollectionListingResponse:
"""
List basic metadata of all collections provided by the back-end.
Expand All @@ -911,8 +912,8 @@ def list_collections(self) -> List[dict]:
:return: list of dictionaries with basic collection metadata.
"""
# TODO: add caching #383, but reset cache on auth change #254
data = self.get('/collections', expected_status=200).json()["collections"]
return VisualList("collections", data=data)
data = self.get("/collections", expected_status=200).json()
return CollectionListingResponse(data)

def list_collection_ids(self) -> List[str]:
"""
Expand Down
Empty file added openeo/rest/models/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions openeo/rest/models/federation_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import List, Union


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

__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.
: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)
56 changes: 56 additions & 0 deletions openeo/rest/models/general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from dataclasses import dataclass
from typing import List, Optional, Union

from openeo.internal.jupyter import render_component
from openeo.rest.models.federation_extension import FederationExtension


@dataclass(frozen=True)
class Link:
"""
Container for (web) link data, used throughout the openEO API,
to point to alternate representations, a license, extra detailed information, and more.
"""

rel: str
href: str
type: Optional[str] = None
title: Optional[str] = None

@classmethod
def from_dict(cls, data: dict):
return cls(rel=data["rel"], href=data["href"], type=data.get("type"), title=data.get("title"))

# TODO: add _html_repr_ for Jupyter integration


class CollectionListingResponse(list):
"""
Container for collection metadata listing received from a ``GET /collections`` request.
This object mimics a list of collection metadata dictionaries,
which was the original return API of :py:meth:`~openeo.rest.connection.Connection.list_collections()`,
but now also includes additional metadata like links and extensions.
:param data: response data from a ``GET /collections`` request
"""

__slots__ = ["_data"]

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

def _repr_html_(self):
return render_component(component="collections", data=self)

@property
def links(self) -> List[Link]:
"""Get links from collections response."""
return [Link.from_dict(d) for d in self._data.get("links", [])]

@property
def ext_federation(self) -> FederationExtension:
"""Accessor for federation extension data."""
return FederationExtension(self._data)
Empty file added tests/rest/models/__init__.py
Empty file.
62 changes: 62 additions & 0 deletions tests/rest/models/test_general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest

from openeo.rest.models.general import CollectionListingResponse, Link


class TestLink:
def test_basic(self):
link = Link(rel="about", href="https://example.com/about")
assert link.rel == "about"
assert link.href == "https://example.com/about"
assert link.title is None
assert link.type is None

def test_full(self):
link = Link(rel="about", href="https://example.com/about", type="text/html", title="About example")
assert link.rel == "about"
assert link.href == "https://example.com/about"
assert link.title == "About example"
assert link.type == "text/html"

def test_repr(self):
link = Link(rel="about", href="https://example.com/about")
assert repr(link) == "Link(rel='about', href='https://example.com/about', type=None, title=None)"


class TestCollectionListingResponse:
def test_basic(self):
data = {"collections": [{"id": "S2"}, {"id": "S3"}]}
collections = CollectionListingResponse(data)
assert collections == [{"id": "S2"}, {"id": "S3"}]
assert repr(collections) == "[{'id': 'S2'}, {'id': 'S3'}]"

def test_links(self):
data = {
"collections": [{"id": "S2"}, {"id": "S3"}],
"links": [
{"rel": "self", "href": "https://openeo.test/collections"},
{"rel": "next", "href": "https://openeo.test/collections?page=2"},
],
}
collections = CollectionListingResponse(data)
assert collections.links == [
Link(rel="self", href="https://openeo.test/collections"),
Link(rel="next", href="https://openeo.test/collections?page=2"),
]

@pytest.mark.parametrize(
["data", "expected"],
[
(
{"collections": [{"id": "S2"}], "federation:missing": ["wwu"]},
["wwu"],
),
(
{"collections": [{"id": "S2"}]},
None,
),
],
)
def test_federation_missing(self, data, expected):
collections = CollectionListingResponse(data)
assert collections.ext_federation.missing == expected
18 changes: 18 additions & 0 deletions tests/rest/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
extract_connections,
paginate,
)
from openeo.rest.models.general import Link
from openeo.rest.vectorcube import VectorCube
from openeo.testing.stac import StacDummyBuilder
from openeo.util import ContextTimer, deep_get, dict_no_none
Expand Down Expand Up @@ -3329,6 +3330,23 @@ def test_list_collections(requests_mock):
assert con.list_collections() == collections


def test_list_collections_extra_metadata(requests_mock):
requests_mock.get(API_URL, json={"api_version": "1.0.0"})
requests_mock.get(
API_URL + "collections",
json={
"collections": [{"id": "S2"}, {"id": "NDVI"}],
"links": [{"rel": "next", "href": "https://oeo.test/collections?page=2"}],
"federation:missing": ["oeob"],
},
)
con = Connection(API_URL)
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"]


def test_describe_collection(requests_mock):
requests_mock.get(API_URL, json={"api_version": "1.0.0"})
requests_mock.get(
Expand Down

0 comments on commit 7eb6838

Please sign in to comment.