Skip to content

Commit 63e827d

Browse files
Merge pull request #54 from openMetadataInitiative/improved-external-links
Better support for external links
2 parents 5d343b6 + 85bf622 commit 63e827d

File tree

4 files changed

+82
-12
lines changed

4 files changed

+82
-12
lines changed

pipeline/src/base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,14 @@ def __init__(self, **properties):
232232
class Link:
233233
"""Representation of a metadata node for which only the identifier is currently known."""
234234

235-
def __init__(self, identifier):
235+
def __init__(self, identifier, allowed_types=None):
236236
self.identifier = identifier
237+
self.allowed_types = allowed_types
237238

239+
def to_jsonld(self):
240+
return {
241+
"@id": self.identifier
242+
}
238243

239244
class IRI:
240245
"""

pipeline/src/init_template.py.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ openMINDS Python package
66

77
__version__ = "{{version}}"
88

9-
from .base import Node, EmbeddedMetadata, LinkedMetadata, IRI
9+
from .base import Node, EmbeddedMetadata, LinkedMetadata, IRI, Link
1010
from .collection import Collection
1111
from .properties import Property

pipeline/src/properties.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
from typing import Optional, Union, Iterable
1111

1212
from .registry import lookup
13-
from .base import Node, IRI, Link, Node
13+
from .base import Node, IRI, Link
14+
15+
16+
def _could_be_instance(value, types):
17+
"""
18+
True if a Link's allowed types are consistent with the given types
19+
"""
20+
return isinstance(value, Link) and value.allowed_types and set(value.allowed_types).issubset(types)
1421

1522

1623
class Property:
@@ -89,6 +96,10 @@ def types(self):
8996
self._resolved_types = True
9097
return self._types
9198

99+
@property
100+
def is_link(self) -> bool:
101+
return issubclass(self.types[0], Node)
102+
92103
def validate(self, value, ignore=None):
93104
"""
94105
Check whether `value` satisfies all constraints.
@@ -113,7 +124,7 @@ def validate(self, value, ignore=None):
113124
if not isinstance(value, (list, tuple)):
114125
value = [value]
115126
for item in value:
116-
if not isinstance(item, self.types):
127+
if not (isinstance(item, self.types) or _could_be_instance(item, self.types)):
117128
if "type" not in ignore:
118129
failures["type"].append(
119130
f"{self.name}: Expected {', '.join(t.__name__ for t in self.types)}, "
@@ -145,7 +156,7 @@ def validate(self, value, ignore=None):
145156
failures["multiplicity"].append(
146157
f"{self.name} does not accept multiple values, but contains {len(value)}"
147158
)
148-
elif not isinstance(value, self.types):
159+
elif not (isinstance(value, self.types) or _could_be_instance(value, self.types)):
149160
if "type" not in ignore:
150161
failures["type"].append(
151162
f"{self.name}: Expected {', '.join(t.__name__ for t in self.types)}, "
@@ -163,8 +174,9 @@ def deserialize(self, data):
163174
Args:
164175
data: the JSON-LD data
165176
"""
166-
167177
# todo: check data type
178+
link_keys = set(("@id", "@type"))
179+
168180
def deserialize_item(item):
169181
if self.types == (str,):
170182
if self.formatting != "text/plain":
@@ -188,9 +200,18 @@ def deserialize_item(item):
188200
if "@type" in item:
189201
for cls in self.types:
190202
if cls.type_ == item["@type"]:
191-
return cls.from_jsonld(item)
203+
if set(item.keys()) == link_keys:
204+
# if we only have @id and @type, it's a Link
205+
return Link(item["@id"], allowed_types=[cls])
206+
else:
207+
# otherwise it's a Node
208+
return cls.from_jsonld(item)
209+
raise TypeError(
210+
f"Mismatched types. Data has '{item['@type']}' "
211+
f"but property only allows {[cls.type_ for cls in self.types]}"
212+
)
192213
else:
193-
return Link(item["@id"])
214+
return Link(item["@id"], allowed_types=self.types)
194215
else:
195216
raise NotImplementedError()
196217

pipeline/tests/test_instantiation.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
import pytest
99

10-
from openminds.base import Node, IRI
11-
10+
from openminds.base import Node, IRI, Link
1211
from utils import build_fake_node
1312

1413
module_names = (
@@ -39,7 +38,9 @@
3938

4039
def classes_in_module(module):
4140
contents = [getattr(module, name) for name in dir(module)]
42-
return [item for item in contents if isinstance(item, type) and issubclass(item, Node)]
41+
return [
42+
item for item in contents if isinstance(item, type) and issubclass(item, Node)
43+
]
4344

4445

4546
def test_instantiation_random_data():
@@ -61,7 +62,10 @@ def test_json_roundtrip():
6162

6263

6364
def test_IRI():
64-
valid_iris = ["https://example.com/path/to/my/file.txt", "file:///path/to/my/file.txt"]
65+
valid_iris = [
66+
"https://example.com/path/to/my/file.txt",
67+
"file:///path/to/my/file.txt",
68+
]
6569
for value in valid_iris:
6670
iri = IRI(value)
6771
assert iri.value == value
@@ -75,3 +79,43 @@ def test_IRI():
7579
with pytest.raises(ValueError) as exc_info:
7680
iri = IRI(value)
7781
assert exc_info.value.args[0] == "Invalid IRI"
82+
83+
84+
def test_link():
85+
from openminds.v4.controlled_terms import Species
86+
from openminds.v4.core import DatasetVersion
87+
88+
maybe_mouse = Link("https://openminds.om-i.org/instances/species/musMusculus")
89+
90+
definitely_mouse = Link(
91+
"https://openminds.om-i.org/instances/species/musMusculus",
92+
allowed_types=[Species],
93+
)
94+
95+
my_dsv1 = DatasetVersion(study_targets=[maybe_mouse])
96+
failures1 = my_dsv1.validate(ignore=["required"])
97+
assert len(failures1["type"]) == 1
98+
assert "study_targets" in failures1["type"][0]
99+
100+
my_dsv2 = DatasetVersion(study_targets=[definitely_mouse])
101+
failures2 = my_dsv2.validate(ignore=["required"])
102+
assert len(failures2) == 0
103+
104+
expected = {
105+
"@context": {
106+
"@vocab": "https://openminds.om-i.org/props/",
107+
},
108+
"@type": "https://openminds.om-i.org/types/DatasetVersion",
109+
"studyTarget": [
110+
{
111+
"@id": "https://openminds.om-i.org/instances/species/musMusculus",
112+
},
113+
],
114+
}
115+
assert my_dsv1.to_jsonld(
116+
include_empty_properties=False,
117+
embed_linked_nodes=False
118+
) == my_dsv2.to_jsonld(
119+
include_empty_properties=False,
120+
embed_linked_nodes=False
121+
) == expected

0 commit comments

Comments
 (0)