Skip to content

Commit 861e6a7

Browse files
authored
Merge branch 'develop' into 21-referencing-a-workflow-crate
2 parents 21d02d7 + cce4652 commit 861e6a7

5 files changed

Lines changed: 187 additions & 16 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (c) 2024-2025 CRS4
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
@prefix ro: <./> .
16+
@prefix ro-crate: <https://github.com/crs4/rocrate-validator/profiles/ro-crate/> .
17+
@prefix five-safes-crate: <https://github.com/eScienceLab/rocrate-validator/profiles/five-safes-crate/> .
18+
@prefix schema_org: <http://schema.org/> .
19+
@prefix sh: <http://www.w3.org/ns/shacl#> .
20+
@prefix bioschemas: <https://bioschemas.org/> .
21+
22+
# TODO: note that this applies to all Projects, not just those targeted by `funding` relationships
23+
five-safes-crate:FundingBody a sh:NodeShape ;
24+
sh:name "Funding body Project" ;
25+
sh:description "Project which is funding this work" ;
26+
sh:targetClass schema_org:Project ;
27+
sh:property [
28+
a sh:PropertyShape ;
29+
sh:name "Project Name" ;
30+
sh:description """Check if the Project Entity `name` (as specified by schema.org)
31+
to clearly identify the dataset and distinguish it from other projects.""" ;
32+
sh:minCount 1 ;
33+
sh:nodeKind sh:Literal ;
34+
sh:path schema_org:name;
35+
sh:message "The Project Entity MUST have a `name` property (as specified by schema.org)" ;
36+
] .
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (c) 2024-2025 CRS4
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
@prefix ro: <./> .
16+
@prefix ro-crate: <https://github.com/crs4/rocrate-validator/profiles/ro-crate/> .
17+
@prefix five-safes-crate: <https://github.com/eScienceLab/rocrate-validator/profiles/five-safes-crate/> .
18+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
19+
@prefix schema_org: <http://schema.org/> .
20+
@prefix sh: <http://www.w3.org/ns/shacl#> .
21+
@prefix validator: <https://github.com/crs4/rocrate-validator/> .
22+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
23+
24+
five-safes-crate:RootDataEntityRequiredProperties
25+
a sh:NodeShape ;
26+
sh:name "Five Safes Crate Root Data Entity REQUIRED properties" ;
27+
sh:description "The Root Data Entity MUST have a `sourceOrganisation`" ;
28+
sh:targetClass ro-crate:RootDataEntity ;
29+
sh:property [
30+
a sh:PropertyShape ;
31+
sh:name "Root Data Entity: `sourceOrganization` property" ;
32+
sh:description """Check if the Root Data Entity includes a `sourceOrganization` (as specified by schema.org).""" ;
33+
sh:path schema_org:sourceOrganization;
34+
sh:minCount 1 ;
35+
sh:nodeKind sh:IRI;
36+
sh:message """The Root Data Entity MUST have a `sourceOrganization` property (as specified by schema.org).
37+
SHOULD link to a Contextual Entity in the RO-Crate Metadata File with a name.""" ;
38+
] .

tests/integration/profiles/five-safes-crate/test_5src_funding.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
import logging
1616

1717
from rocrate_validator.models import Severity
18-
from tests.ro_crates import Invalid5sROC
19-
from tests.shared import do_entity_test
18+
from tests.ro_crates import ValidROC
19+
from tests.shared import do_entity_test, SPARQL_PREFIXES
2020

2121
# set up logging
2222
logger = logging.getLogger(__name__)
@@ -26,13 +26,19 @@ def test_5src_funding_project_no_name():
2626
"""\
2727
Test a Five Safes Crate where the funding Project does not have a name.
2828
"""
29+
sparql = SPARQL_PREFIXES + """DELETE WHERE {
30+
<#project-be6ffb55-4f5a-4c14-b60e-47e0951090c70> schema:name "Investigation of cancer (TRE72 project 81)"
31+
}
32+
"""
33+
2934
do_entity_test(
30-
rocrate_path=Invalid5sROC().funding_project_no_name,
35+
rocrate_path=ValidROC().five_safes_crate_result,
3136
requirement_severity=Severity.REQUIRED,
3237
expected_validation_result=False,
3338
expected_triggered_requirements=["Funding body Project"],
3439
expected_triggered_issues=[
3540
"The Project Entity MUST have a `name` property (as specified by schema.org)"
3641
],
3742
profile_identifier="five-safes-crate",
43+
rocrate_entity_mod_sparql=sparql,
3844
)

tests/integration/profiles/five-safes-crate/test_5src_root_data_entity_metadata.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
import logging
1616

1717
from rocrate_validator.models import Severity
18-
from tests.ro_crates import Invalid5sROC
19-
from tests.shared import do_entity_test
18+
from tests.ro_crates import ValidROC
19+
from tests.shared import do_entity_test, SPARQL_PREFIXES
2020

2121
# set up logging
2222
logger = logging.getLogger(__name__)
@@ -26,8 +26,13 @@ def test_5src_root_data_entity_no_source_organization():
2626
"""\
2727
Test a Five Safes Crate where the Root Data Entity it does not reference a sourceOrganization.
2828
"""
29+
sparql = SPARQL_PREFIXES + """DELETE WHERE {
30+
<./> schema:sourceOrganization ?object
31+
}
32+
"""
33+
2934
do_entity_test(
30-
rocrate_path=Invalid5sROC().root_data_entity_no_source_organization,
35+
rocrate_path=ValidROC().five_safes_crate_result,
3136
requirement_severity=Severity.REQUIRED,
3237
expected_validation_result=False,
3338
expected_triggered_requirements=["Five Safes Crate Root Data Entity REQUIRED properties"],
@@ -36,15 +41,28 @@ def test_5src_root_data_entity_no_source_organization():
3641
SHOULD link to a Contextual Entity in the RO-Crate Metadata File with a name."""
3742
],
3843
profile_identifier="five-safes-crate",
44+
rocrate_entity_mod_sparql=sparql,
3945
)
4046

4147

4248
def test_5src_root_data_entity_source_organization_not_entity():
4349
"""\
4450
Test a Five Safes Crate where the Root Data Entity it does not reference a sourceOrganization.
4551
"""
52+
sparql = SPARQL_PREFIXES + """DELETE {
53+
<./> schema:sourceOrganization ?o
54+
}
55+
INSERT {
56+
<./> schema:sourceOrganization "Investigation of cancer (TRE72 project 81)"
57+
}
58+
WHERE {
59+
<./> schema:sourceOrganization ?o
60+
}
61+
62+
"""
63+
4664
do_entity_test(
47-
rocrate_path=Invalid5sROC().root_data_entity_source_organization_not_entity,
65+
rocrate_path=ValidROC().five_safes_crate_result,
4866
requirement_severity=Severity.REQUIRED,
4967
expected_validation_result=False,
5068
expected_triggered_requirements=["Five Safes Crate Root Data Entity REQUIRED properties"],
@@ -53,4 +71,5 @@ def test_5src_root_data_entity_source_organization_not_entity():
5371
SHOULD link to a Contextual Entity in the RO-Crate Metadata File with a name."""
5472
],
5573
profile_identifier="five-safes-crate",
74+
rocrate_entity_mod_sparql=sparql,
5675
)

tests/shared.py

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import logging
2121
import shutil
2222
import tempfile
23+
import rdflib
2324
from collections.abc import Collection
2425
from pathlib import Path
2526
from typing import Optional, TypeVar, Union
27+
from urllib.parse import urljoin
2628

2729
from rocrate_validator import models, services
2830
from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER
@@ -32,11 +34,61 @@
3234

3335
T = TypeVar("T")
3436

37+
SPARQL_PREFIXES = """PREFIX schema: <http://schema.org/>
38+
"""
39+
3540

3641
def first(c: Collection[T]) -> T:
3742
return next(iter(c))
3843

3944

45+
def load_graph_and_preserve_relative_ids(json_data, base="http://example.org/"):
46+
47+
rel_ids = set()
48+
49+
def collect_ids(obj):
50+
if isinstance(obj, dict):
51+
if "@id" in obj:
52+
idv = obj["@id"]
53+
if isinstance(idv, str) and (idv.startswith("./") or idv.startswith("../") or idv.startswith("#")):
54+
rel_ids.add(idv)
55+
for v in obj.values():
56+
collect_ids(v)
57+
elif isinstance(obj, list):
58+
for item in obj:
59+
collect_ids(item)
60+
61+
collect_ids(json_data)
62+
63+
g = rdflib.Graph()
64+
g.parse(data=json_data, format="json-ld", publicID=base)
65+
66+
mapping = {}
67+
for rid in rel_ids:
68+
expanded = urljoin(base, rid)
69+
mapping[expanded] = rid
70+
71+
def replace_uri_in_graph(graph, old_uri_str, new_uri_str):
72+
new = rdflib.URIRef(new_uri_str)
73+
triples_to_add = []
74+
triples_to_remove = []
75+
for s, p, o in graph.triples((None, None, None)):
76+
s2 = new if (isinstance(s, rdflib.URIRef) and str(s) == old_uri_str) else s
77+
o2 = new if (isinstance(o, rdflib.URIRef) and str(o) == old_uri_str) else o
78+
if (s2, p, o2) != (s, p, o):
79+
triples_to_remove.append((s, p, o))
80+
triples_to_add.append((s2, p, o2))
81+
for t in triples_to_remove:
82+
graph.remove(t)
83+
for t in triples_to_add:
84+
graph.add(t)
85+
86+
for expanded, rel in mapping.items():
87+
replace_uri_in_graph(g, expanded, rel)
88+
89+
return g
90+
91+
4092
def do_entity_test(
4193
rocrate_path: Union[Path, str],
4294
requirement_severity: models.Severity,
@@ -46,11 +98,15 @@ def do_entity_test(
4698
abort_on_first: bool = False,
4799
profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER,
48100
rocrate_entity_patch: Optional[dict] = None,
101+
rocrate_entity_mod_sparql: Optional[str] = None,
49102
skip_checks: Optional[list[str]] = ()
50103
):
51104
"""
52105
Shared function to test a RO-Crate entity
53106
"""
107+
assert not (rocrate_entity_patch and rocrate_entity_mod_sparql), \
108+
"Cannot use rocrate_entity_patch and rocrate_entity_mod_sparql together"
109+
54110
# declare variables
55111
failed_requirements = None
56112
detected_issues = None
@@ -59,7 +115,7 @@ def do_entity_test(
59115
rocrate_path = Path(rocrate_path)
60116

61117
temp_rocrate_path = None
62-
if rocrate_entity_patch is not None and rocrate_path.is_dir():
118+
if any([rocrate_entity_patch, rocrate_entity_mod_sparql]) and rocrate_path.is_dir():
63119
# create a temporary copy of the RO-Crate
64120
temp_rocrate_path = Path(tempfile.TemporaryDirectory().name)
65121
# copy the RO-Crate to the temporary path using shutil
@@ -68,14 +124,30 @@ def do_entity_test(
68124
with open(temp_rocrate_path / "ro-crate-metadata.json", "r") as f:
69125
rocrate = json.load(f)
70126
# update the RO-Crate metadata with the patch
71-
for key, value in rocrate_entity_patch.items():
72-
for entity in rocrate["@graph"]:
73-
if entity["@id"] == key:
74-
entity.update(value)
75-
break
76-
# save the updated RO-Crate metadata
77-
with open(temp_rocrate_path / "ro-crate-metadata.json", "w") as f:
78-
json.dump(rocrate, f)
127+
if rocrate_entity_patch is not None:
128+
for key, value in rocrate_entity_patch.items():
129+
for entity in rocrate["@graph"]:
130+
if entity["@id"] == key:
131+
entity.update(value)
132+
break
133+
# save the updated RO-Crate metadata
134+
with open(temp_rocrate_path / "ro-crate-metadata.json", "w") as f:
135+
json.dump(rocrate, f)
136+
# update the RO-Crate metadata using SPARQL, if required
137+
if rocrate_entity_mod_sparql is not None:
138+
rocrate_graph = load_graph_and_preserve_relative_ids(rocrate)
139+
140+
rocrate_graph.update(rocrate_entity_mod_sparql)
141+
142+
# save the updated RO-Crate metadata
143+
context = "https://w3id.org/ro/crate/1.1/context"
144+
rocrate_graph.serialize(
145+
Path(temp_rocrate_path, "ro-crate-metadata.json"),
146+
format="json-ld",
147+
context=context,
148+
indent=2,
149+
use_native_types=True,
150+
)
79151
rocrate_path = temp_rocrate_path
80152

81153
if expected_triggered_requirements is None:

0 commit comments

Comments
 (0)