diff --git a/pipeline/src/base.py b/pipeline/src/base.py index d952b2ea..f20e6877 100644 --- a/pipeline/src/base.py +++ b/pipeline/src/base.py @@ -68,11 +68,14 @@ def to_jsonld(self, include_empty_properties=True, embed_linked_nodes=True, with """ data = {"@type": self.type_} + if isinstance(self, LinkedMetadata): + data["schema:schemaVersion"] = self.schema_version if with_context: if self.type_.startswith("https://openminds.ebrains.eu/"): data["@context"] = {"@vocab": "https://openminds.ebrains.eu/vocab/"} else: data["@context"] = {"@vocab": "https://openminds.om-i.org/props/"} + data["@context"]["schema"] = "https://schema.org/" if hasattr(self, "id") and self.id: data["@id"] = self.id for property in self.__class__.properties: @@ -122,6 +125,10 @@ def from_jsonld(cls, data, ignore_unexpected_keys=False): """ data_copy = data.copy() context = data_copy.pop("@context", None) + schema_version = data_copy.pop("schema:schemaVersion", None) + # todo: also handle an expanded key, i.e., "https://schema.org/schemaVersion" + # todo: check major part of schema_version against self.schema_version + # i.e. v4.1 and v4.0 would be ok, v5.0 and v4.0 would not type_ = data_copy.pop("@type") if isinstance(type_, list) and len(type_) == 1: type_ = type_[0] @@ -138,7 +145,7 @@ def from_jsonld(cls, data, ignore_unexpected_keys=False): else: # todo: implement or import a function that does a full JSON-LD expansion # not just this special case - expanded_path = f"{cls.context['@vocab']}{property.path}" + expanded_path = f"{cls.context['@vocab']}{property.path}" if expanded_path in data_copy: value = data_copy.pop(expanded_path) found = True @@ -278,9 +285,8 @@ def __init__(self, identifier, allowed_types=None): self.allowed_types = allowed_types def to_jsonld(self): - return { - "@id": self.identifier - } + return {"@id": self.identifier} + class IRI: """ diff --git a/pipeline/src/collection.py b/pipeline/src/collection.py index 11dfa7df..bd0a6d12 100644 --- a/pipeline/src/collection.py +++ b/pipeline/src/collection.py @@ -91,9 +91,14 @@ def save(self, path, individual_files=False, include_empty_properties=False): for node in tuple(self.nodes.values()): if node.type_.startswith("https://openminds.ebrains.eu/"): - data_context = {"@vocab": "https://openminds.ebrains.eu/vocab/"} + data_context = { + "@vocab": "https://openminds.ebrains.eu/vocab/" + } else: - data_context = {"@vocab": "https://openminds.om-i.org/props/"} + data_context = { + "@vocab": "https://openminds.om-i.org/props/" + } + data_context["schema"] = "https://schema.org/" for linked_node in node.links: self._add_node(linked_node) @@ -169,12 +174,15 @@ def load(self, *paths): data = json.load(fp) if "@graph" in data: if data["@context"]["@vocab"].startswith("https://openminds.ebrains.eu/"): - version = "v3" + default_version = "v3" else: - version = "latest" + default_version = "v4" for item in data["@graph"]: if "@type" in item: - cls = lookup_type(item["@type"], version=version) + version = default_version + if "schema:schemaVersion" in item: # todo: expand this using the context + version = item["schema:schemaVersion"] + cls = lookup_type(item["@type"], version=version.split(".")[0]) node = cls.from_jsonld(item) else: # allow links to metadata instances outside this collection @@ -245,4 +253,3 @@ def sort_nodes_for_upload(self): newly_sorted.append(node_id) unsorted -= set(newly_sorted) return [self.nodes[node_id] for node_id in sorted] - \ No newline at end of file diff --git a/pipeline/tests/test_collections.py b/pipeline/tests/test_collections.py index add5ddd7..47bb854f 100644 --- a/pipeline/tests/test_collections.py +++ b/pipeline/tests/test_collections.py @@ -96,17 +96,22 @@ def test_collection_sort_by_id(): os.remove("test_collection_sort_by_id.jsonld") expected_saved_data = { - "@context": {"@vocab": "https://openminds.om-i.org/props/"}, + "@context": { + "@vocab": "https://openminds.om-i.org/props/", + "schema": "https://schema.org/", + }, "@graph": [ { "@id": "_:001", "@type": "https://openminds.om-i.org/types/Organization", "fullName": "University of That Place", + "schema:schemaVersion": "latest" }, { "@id": "_:002", "@type": "https://openminds.om-i.org/types/Organization", "fullName": "University of This Place", + "schema:schemaVersion": "latest" }, { "@id": "_:004", @@ -117,6 +122,7 @@ def test_collection_sort_by_id(): ], "familyName": "Professor", "givenName": "A", + "schema:schemaVersion": "latest" }, ], } diff --git a/pipeline/tests/test_instantiation.py b/pipeline/tests/test_instantiation.py index bee2fef1..9e49e2f3 100644 --- a/pipeline/tests/test_instantiation.py +++ b/pipeline/tests/test_instantiation.py @@ -104,8 +104,10 @@ def test_link(): expected = { "@context": { "@vocab": "https://openminds.om-i.org/props/", + "schema": "https://schema.org/" }, "@type": "https://openminds.om-i.org/types/DatasetVersion", + "schema:schemaVersion": "v4.0", "studyTarget": [ { "@id": "https://openminds.om-i.org/instances/species/musMusculus", diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index a7e43694..cd53d93e 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -2,7 +2,7 @@ import json import os from openminds import Collection, IRI -from openminds.latest import core as omcore +import openminds.v4.core as omcore from utils import build_fake_node @@ -38,18 +38,24 @@ def test_issue_0003(): ) # on export, a single item should be wrapped in a list, where the property expects an array expected = { - "@context": {"@vocab": "https://openminds.om-i.org/props/"}, + "@context": { + "@vocab": "https://openminds.om-i.org/props/", + "schema": "https://schema.org/" + }, "@type": "https://openminds.om-i.org/types/FileArchive", "IRI": "http://example.com/archive.zip", "format": { "@type": "https://openminds.om-i.org/types/ContentType", "name": "application/zip", + "schema:schemaVersion": "v4.0", }, + "schema:schemaVersion": "v4.0", "sourceData": [ { "@type": "https://openminds.om-i.org/types/File", "IRI": "http://example.com/some_file.txt", "name": "some_file.txt", + "schema:schemaVersion": "v4.0", } ], } @@ -90,11 +96,15 @@ def test_issue0007(): actual = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=False, with_context=True) expected = { - "@context": {"@vocab": "https://openminds.om-i.org/props/"}, + "@context": { + "@vocab": "https://openminds.om-i.org/props/", + "schema": "https://schema.org/" + }, "@id": "_:001", "@type": "https://openminds.om-i.org/types/Person", "familyName": "Professor", "givenName": "A", + "schema:schemaVersion": "v4.0", "affiliation": [ { "@type": "https://openminds.om-i.org/types/Affiliation", @@ -105,6 +115,7 @@ def test_issue0007(): "memberOf": {"@id": "_:003"}, }, ], + } assert actual == expected @@ -116,7 +127,10 @@ def test_issue0007(): saved_data = json.load(fp) os.remove("issue0007.jsonld") expected_saved_data = { - "@context": {"@vocab": "https://openminds.om-i.org/props/"}, + "@context": { + "@vocab": "https://openminds.om-i.org/props/", + "schema": "https://schema.org/" + }, "@graph": [ { "@id": "_:001", @@ -133,16 +147,19 @@ def test_issue0007(): ], "familyName": "Professor", "givenName": "A", + "schema:schemaVersion": "v4.0", }, { "@id": "_:002", "@type": "https://openminds.om-i.org/types/Organization", "fullName": "University of This Place", + "schema:schemaVersion": "v4.0", }, { "@id": "_:003", "@type": "https://openminds.om-i.org/types/Organization", "fullName": "University of That Place", + "schema:schemaVersion": "v4.0", }, ], } @@ -163,7 +180,10 @@ def test_issue0008(): ) actual = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=False, with_context=True) expected = { - "@context": {"@vocab": "https://openminds.om-i.org/props/"}, + "@context": { + "@vocab": "https://openminds.om-i.org/props/", + "schema": "https://schema.org/" + }, "@id": "_:002", "@type": "https://openminds.om-i.org/types/Person", "affiliation": [ @@ -175,6 +195,7 @@ def test_issue0008(): ], "familyName": "Professor", "givenName": "A", + "schema:schemaVersion": "v4.0", } assert actual == expected @@ -261,12 +282,12 @@ def test_issue0073(): # Infinite recursion in validate() ds1 = omcore.DatasetVersion( short_name="ds1", - is_variant_of=None + is_alternative_version_of=None ) ds2 = omcore.DatasetVersion( short_name="ds2", - is_variant_of=ds1 + is_alternative_version_of=ds1 ) - ds1.is_variant_of = ds2 + ds1.is_alternative_version_of = ds2 failures = ds1.validate()