Skip to content
This repository was archived by the owner on Aug 20, 2025. It is now read-only.

Commit 7e5edb8

Browse files
committed
Add parsing/support for UDP links
1 parent 11edc62 commit 7e5edb8

File tree

3 files changed

+167
-8
lines changed

3 files changed

+167
-8
lines changed

src/esa_apex_toolbox/algorithms.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import requests
99

1010

11+
class LINK_REL:
12+
UDP = "udp"
13+
14+
1115
def _load_json_resource(src: Union[dict, str, Path]) -> dict:
1216
"""Load a JSON resource from a file or a string."""
1317
if isinstance(src, dict):
@@ -32,11 +36,38 @@ def _load_json_resource(src: Union[dict, str, Path]) -> dict:
3236
raise ValueError(f"Unsupported JSON resource type {type(src)}")
3337

3438

39+
class InvalidMetadataError(ValueError):
40+
pass
41+
42+
43+
@dataclasses.dataclass(frozen=True)
44+
class UdpLink:
45+
href: str
46+
title: Optional[str] = None
47+
48+
@classmethod
49+
def from_link_object(cls, data: dict) -> UdpLink:
50+
"""Parse a link object (dict/mapping) into a UdpLink object."""
51+
if "rel" not in data:
52+
raise InvalidMetadataError("Missing 'rel' attribute in link object")
53+
if data["rel"] != LINK_REL.UDP:
54+
raise InvalidMetadataError(f"Expected link with rel='udp' but got {data['rel']!r}")
55+
if "type" in data and data["type"] != "application/json":
56+
raise InvalidMetadataError(f"Expected link with type='application/json' but got {data['type']!r}")
57+
if "href" not in data:
58+
raise InvalidMetadataError("Missing 'href' attribute in link object")
59+
return cls(
60+
href=data["href"],
61+
title=data.get("title"),
62+
)
63+
64+
3565
@dataclasses.dataclass(frozen=True)
3666
class Algorithm:
3767
id: str
3868
title: Optional[str] = None
3969
description: Optional[str] = None
70+
udp_link: Optional[UdpLink] = None
4071
# TODO more fields
4172

4273
@classmethod
@@ -47,18 +78,27 @@ def from_ogc_api_record(cls, src: Union[dict, str, Path]) -> Algorithm:
4778
"""
4879
data = _load_json_resource(src)
4980

50-
# TODO dedicated exceptions for structure/schema violations
5181
if not data.get("type") == "Feature":
52-
raise ValueError("Expected a GeoJSON Feature object")
82+
raise InvalidMetadataError(f"Expected a GeoJSON 'Feature' object, but got type {data.get('type')!r}.")
5383
if "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core" not in data.get("conformsTo", []):
54-
raise ValueError("Expected an OGC API - Records record object")
84+
raise InvalidMetadataError(
85+
f"Expected an 'OGC API - Records' record object, but got {data.get('conformsTo')!r}."
86+
)
5587

5688
properties = data.get("properties", {})
57-
if not properties.get("type") == "apex_algorithm":
58-
raise ValueError("Expected an APEX algorithm object")
89+
if properties.get("type") != "apex_algorithm":
90+
raise InvalidMetadataError(f"Expected an APEX algorithm object, but got type {properties.get('type')!r}.")
91+
92+
links = data.get("links", [])
93+
udp_links = [UdpLink.from_link_object(link) for link in links if link.get("rel") == LINK_REL.UDP]
94+
if len(udp_links) > 1:
95+
raise InvalidMetadataError("Multiple UDP links found")
96+
# TODO: is having a UDP link a requirement?
97+
udp_link = udp_links[0] if udp_links else None
5998

6099
return cls(
61100
id=data["id"],
62101
title=properties.get("title"),
63102
description=properties.get("description"),
103+
udp_link=udp_link,
64104
)

tests/data/ogcapi-records/algorithm01.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
{
1414
"rel": "udp",
1515
"type": "application/json",
16-
"title": "openEO UDP",
16+
"title": "UDP One",
1717
"href": "https://esa-apex.test/udp/algorithm01.json"
1818
}
1919
]

tests/test_algorithms.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,116 @@
1+
import re
12
from pathlib import Path
23

34
import pytest
45

5-
from esa_apex_toolbox.algorithms import Algorithm
6+
from esa_apex_toolbox.algorithms import Algorithm, InvalidMetadataError, UdpLink
67

78
DATA_ROOT = Path(__file__).parent / "data"
89

910

11+
class TestUdpLink:
12+
def test_from_link_object_basic(self):
13+
data = {
14+
"rel": "udp",
15+
"href": "https://esa-apex.test/udp/basic.json",
16+
}
17+
link = UdpLink.from_link_object(data)
18+
assert link.href == "https://esa-apex.test/udp/basic.json"
19+
assert link.title is None
20+
21+
def test_from_link_object_with_title(self):
22+
data = {
23+
"rel": "udp",
24+
"href": "https://esa-apex.test/udp/basic.json",
25+
"title": "My basic UDP",
26+
}
27+
link = UdpLink.from_link_object(data)
28+
assert link.href == "https://esa-apex.test/udp/basic.json"
29+
assert link.title == "My basic UDP"
30+
31+
def test_from_link_object_missing_rel(self):
32+
data = {
33+
"href": "https://esa-apex.test/udp/basic.json",
34+
}
35+
with pytest.raises(InvalidMetadataError, match="Missing 'rel' attribute"):
36+
_ = UdpLink.from_link_object(data)
37+
38+
def test_from_link_object_wrong_rel(self):
39+
data = {
40+
"rel": "self",
41+
"href": "https://esa-apex.test/udp/basic.json",
42+
}
43+
with pytest.raises(InvalidMetadataError, match="Expected link with rel='udp'"):
44+
_ = UdpLink.from_link_object(data)
45+
46+
def test_from_link_object_no_href(self):
47+
data = {
48+
"rel": "udp",
49+
}
50+
with pytest.raises(InvalidMetadataError, match="Missing 'href' attribute"):
51+
_ = UdpLink.from_link_object(data)
52+
53+
def test_from_link_object_wrong_type(self):
54+
data = {
55+
"rel": "udp",
56+
"href": "https://esa-apex.test/udp/basic.json",
57+
"type": "application/xml",
58+
}
59+
with pytest.raises(InvalidMetadataError, match="Expected link with type='application/json'"):
60+
_ = UdpLink.from_link_object(data)
61+
62+
1063
class TestAlgorithm:
64+
def test_from_ogc_api_record_minimal(self):
65+
data = {
66+
"id": "minimal",
67+
"type": "Feature",
68+
"conformsTo": ["http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core"],
69+
"properties": {
70+
"type": "apex_algorithm",
71+
},
72+
}
73+
algorithm = Algorithm.from_ogc_api_record(data)
74+
assert algorithm.id == "minimal"
75+
76+
def test_from_ogc_api_record_wrong_type(self):
77+
data = {
78+
"id": "wrong",
79+
"type": "apex_algorithm",
80+
"conformsTo": ["http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core"],
81+
}
82+
with pytest.raises(
83+
InvalidMetadataError, match="Expected a GeoJSON 'Feature' object, but got type 'apex_algorithm'."
84+
):
85+
_ = Algorithm.from_ogc_api_record(data)
86+
87+
def test_from_ogc_api_record_wrong_conform(self):
88+
data = {
89+
"id": "wrong",
90+
"type": "Feature",
91+
"conformsTo": ["http://nope.test/"],
92+
"properties": {
93+
"type": "apex_algorithm",
94+
},
95+
}
96+
with pytest.raises(
97+
InvalidMetadataError,
98+
match=re.escape("Expected an 'OGC API - Records' record object, but got ['http://nope.test/']."),
99+
):
100+
_ = Algorithm.from_ogc_api_record(data)
101+
102+
def test_from_ogc_api_record_wrong_properties_type(self):
103+
data = {
104+
"id": "wrong",
105+
"type": "Feature",
106+
"conformsTo": ["http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core"],
107+
"properties": {
108+
"type": "udp",
109+
},
110+
}
111+
with pytest.raises(InvalidMetadataError, match="Expected an APEX algorithm object, but got type 'udp'"):
112+
_ = Algorithm.from_ogc_api_record(data)
113+
11114
def test_from_ogc_api_record_basic(self):
12115
data = {
13116
"id": "basic",
@@ -22,7 +125,7 @@ def test_from_ogc_api_record_basic(self):
22125
{
23126
"rel": "udp",
24127
"type": "application/json",
25-
"title": "openEO UDP",
128+
"title": "Basic UDP",
26129
"href": "https://esa-apex.test/udp/basic.json",
27130
}
28131
],
@@ -31,6 +134,10 @@ def test_from_ogc_api_record_basic(self):
31134
assert algorithm.id == "basic"
32135
assert algorithm.title == "Basic"
33136
assert algorithm.description == "The basics."
137+
assert algorithm.udp_link == UdpLink(
138+
href="https://esa-apex.test/udp/basic.json",
139+
title="Basic UDP",
140+
)
34141

35142
@pytest.mark.parametrize("path_type", [str, Path])
36143
def test_from_ogc_api_record_path(self, path_type):
@@ -39,13 +146,21 @@ def test_from_ogc_api_record_path(self, path_type):
39146
assert algorithm.id == "algorithm01"
40147
assert algorithm.title == "Algorithm One"
41148
assert algorithm.description == "A first algorithm."
149+
assert algorithm.udp_link == UdpLink(
150+
href="https://esa-apex.test/udp/algorithm01.json",
151+
title="UDP One",
152+
)
42153

43154
def test_from_ogc_api_record_str(self):
44155
dump = (DATA_ROOT / "ogcapi-records/algorithm01.json").read_text()
45156
algorithm = Algorithm.from_ogc_api_record(dump)
46157
assert algorithm.id == "algorithm01"
47158
assert algorithm.title == "Algorithm One"
48159
assert algorithm.description == "A first algorithm."
160+
assert algorithm.udp_link == UdpLink(
161+
href="https://esa-apex.test/udp/algorithm01.json",
162+
title="UDP One",
163+
)
49164

50165
def test_from_ogc_api_record_url(self, requests_mock):
51166
url = "https://esa-apex.test/algorithms/a1.json"
@@ -55,3 +170,7 @@ def test_from_ogc_api_record_url(self, requests_mock):
55170
assert algorithm.id == "algorithm01"
56171
assert algorithm.title == "Algorithm One"
57172
assert algorithm.description == "A first algorithm."
173+
assert algorithm.udp_link == UdpLink(
174+
href="https://esa-apex.test/udp/algorithm01.json",
175+
title="UDP One",
176+
)

0 commit comments

Comments
 (0)