Skip to content

Commit 7af6173

Browse files
committed
Merge remote-tracking branch 'origin/develop' into five-safes-on-upstream-develop
2 parents 69db1e8 + 68a6a3b commit 7af6173

6 files changed

Lines changed: 147 additions & 46 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ extend-ignore-re = ["[0-9a-f]{7,40}"] # Ignore long hexadecimal strings, which a
133133
[tool.typos.files]
134134
extend-exclude = ["tests/data", "docs/diagrams", "*.json", "*.html", "*__init__.py"]
135135

136+
[tool.typos.default.extend-words]
137+
TRE = "TRE"
138+
136139
# Pylint configuration.
137140
# Ruff (see ruff.toml) is the primary linter/formatter; this section keeps
138141
# pylint consistent with it so the two tools do not disagree on style or on

rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,12 @@ ro-crate:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ;
9595
sh:class schema_org:Dataset ;
9696
sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ;
9797
] ;
98-
sh:property [
99-
a sh:PropertyShape ;
100-
sh:name "Metadata File Descriptor entity: `conformsTo` property" ;
101-
sh:description """Check if the RO-Crate Metadata File Descriptor has a `conformsTo` property which points to the RO-Crate specification version""" ;
102-
sh:minCount 1 ;
103-
sh:nodeKind sh:IRI ;
104-
sh:path dct:conformsTo ;
105-
sh:hasValue <https://w3id.org/ro/crate/1.1> ;
106-
sh:message "The RO-Crate metadata file descriptor MUST have a `conformsTo` property with the RO-Crate specification version" ;
107-
] .
98+
sh:property ro-crate:conformsToROCrateSpec .
99+
100+
ro-crate:conformsToROCrateSpec sh:name "Metadata File Descriptor entity: `conformsTo` property" ;
101+
sh:description """Check if the RO-Crate Metadata File Descriptor has a `conformsTo` property which points to the RO-Crate specification version""" ;
102+
sh:minCount 1 ;
103+
sh:nodeKind sh:IRI ;
104+
sh:path dct:conformsTo ;
105+
sh:hasValue <https://w3id.org/ro/crate/1.1> ;
106+
sh:message "The RO-Crate metadata file descriptor MUST have a `conformsTo` property with the RO-Crate specification version" .

rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ro-crate:RootDataEntityType
6060

6161

6262
ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape;
63+
sh:order 0 ; # load this check before any other check which has sh:order. pySHACL sorts rules by sh:order before execution
6364
sh:name "Identify the Root Data Entity of the RO-Crate" ;
6465
sh:description """The Root Data Entity is the top-level Data Entity in the RO-Crate and serves as the starting point for the description of the RO-Crate.
6566
It is a schema:Dataset and is indirectly identified by the about property of the resource ro-crate-metadata.json in the RO-Crate

rocrate_validator/profiles/ro-crate/1.1/prefixes.ttl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,8 @@ ro-crate:sparqlPrefixes
4646
sh:declare [
4747
sh:prefix "ro" ;
4848
sh:namespace "./"^^xsd:anyURI ;
49-
] .
49+
] ;
50+
sh:declare [
51+
sh:prefix "ro-crate" ;
52+
sh:namespace "https://github.com/crs4/rocrate-validator/profiles/ro-crate/"^^xsd:anyURI ;
53+
].

tests/integration/profiles/test_metadata_only.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pytest
2222

2323
from rocrate_validator import models
24+
from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER
2425
from tests.ro_crates import ValidROC
2526
from tests.shared import do_entity_test
2627

@@ -55,7 +56,17 @@ def test_valid_ro_crates_from_folder(valid_roc_path):
5556
temp_path = Path(tmpdirname) / valid_roc_path.name
5657
shutil.copytree(valid_roc_path, temp_path)
5758
valid_roc_path = temp_path
58-
do_entity_test(valid_roc_path, models.Severity.REQUIRED, True, [], [], metadata_only=True)
59+
do_entity_test(
60+
valid_roc_path,
61+
models.Severity.REQUIRED,
62+
True,
63+
[],
64+
[],
65+
profile_identifier=(
66+
"five-safes-crate" if "five-safes-crate" in valid_roc_path.name else DEFAULT_PROFILE_IDENTIFIER
67+
),
68+
metadata_only=True,
69+
)
5970

6071

6172
@pytest.mark.parametrize("valid_roc_path", valid_roc_paths())
@@ -69,5 +80,14 @@ def test_valid_ro_crates_from_metadata_dict(valid_roc_path):
6980
assert metadata_dict is not None, "Failed to load metadata dict"
7081
assert isinstance(metadata_dict, dict), "Metadata dict is not a dictionary"
7182
do_entity_test(
72-
valid_roc_path, models.Severity.REQUIRED, True, [], [], metadata_dict=metadata_dict, metadata_only=True
83+
valid_roc_path,
84+
models.Severity.REQUIRED,
85+
True,
86+
[],
87+
[],
88+
profile_identifier=(
89+
"five-safes-crate" if "five-safes-crate" in valid_roc_path.name else DEFAULT_PROFILE_IDENTIFIER
90+
),
91+
metadata_dict=metadata_dict,
92+
metadata_only=True,
7393
)

tests/shared.py

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright (c) 2024-2026 CRS4
2+
# Copyright (c) 2025-2026 eScience Lab, The University of Manchester
23
#
34
# Licensed under the Apache License, Version 2.0 (the "License");
45
# you may not use this file except in compliance with the License.
@@ -49,7 +50,6 @@ def first(c: Collection[T]) -> T:
4950

5051

5152
def load_graph_and_preserve_relative_ids(json_data, base="http://example.org/"):
52-
5353
rel_ids = set()
5454

5555
def collect_ids(obj):
@@ -78,54 +78,90 @@ def replace_uri_in_graph(graph, old_uri_str, new_uri_str):
7878
new = rdflib.URIRef(new_uri_str)
7979
triples_to_add = []
8080
triples_to_remove = []
81+
8182
for s, p, o in graph.triples((None, None, None)):
8283
s2 = new if (isinstance(s, rdflib.URIRef) and str(s) == old_uri_str) else s
8384
o2 = new if (isinstance(o, rdflib.URIRef) and str(o) == old_uri_str) else o
85+
8486
if (s2, p, o2) != (s, p, o):
8587
triples_to_remove.append((s, p, o))
8688
triples_to_add.append((s2, p, o2))
87-
for t in triples_to_remove:
88-
graph.remove(t)
89-
for t in triples_to_add:
90-
graph.add(t)
89+
90+
for triple in triples_to_remove:
91+
graph.remove(triple)
92+
93+
for triple in triples_to_add:
94+
graph.add(triple)
9195

9296
for expanded, rel in mapping.items():
9397
replace_uri_in_graph(g, expanded, rel)
9498

9599
return g
96100

97101

102+
def _uses_https_schema(graph: rdflib.Graph) -> bool:
103+
for s, p, o in graph.triples((None, None, None)):
104+
for term in (s, p, o):
105+
if isinstance(term, rdflib.URIRef) and str(term).startswith("https://schema.org/"):
106+
return True
107+
108+
return False
109+
110+
98111
def _prepare_temp_rocrate(
99112
rocrate_path: Path,
100113
rocrate_entity_patch: dict | None,
101114
rocrate_entity_mod_sparql: str | None,
102115
) -> Path:
103116
# `mkdtemp` returns a stable path the test owns; `TemporaryDirectory().name`
104117
# was deleted on GC before copytree ran. `dirs_exist_ok=True` lets us copy
105-
# into the (already-created) mkdtemp directory.
118+
# into the already-created directory.
106119
temp_rocrate_path = Path(tempfile.mkdtemp())
107-
shutil.copytree(rocrate_path, temp_rocrate_path, dirs_exist_ok=True)
108-
with (temp_rocrate_path / "ro-crate-metadata.json").open(encoding="utf-8") as f:
120+
shutil.copytree(
121+
rocrate_path,
122+
temp_rocrate_path,
123+
dirs_exist_ok=True,
124+
)
125+
126+
metadata_path = temp_rocrate_path / "ro-crate-metadata.json"
127+
128+
with metadata_path.open(encoding="utf-8") as f:
109129
rocrate = json.load(f)
130+
110131
if rocrate_entity_patch is not None:
111132
for key, value in rocrate_entity_patch.items():
112133
for entity in rocrate["@graph"]:
113134
if entity["@id"] == key:
114135
entity.update(value)
115136
break
116-
with (temp_rocrate_path / "ro-crate-metadata.json").open("w", encoding="utf-8") as f:
137+
138+
with metadata_path.open("w", encoding="utf-8") as f:
117139
json.dump(rocrate, f)
140+
118141
if rocrate_entity_mod_sparql is not None:
119142
rocrate_graph = load_graph_and_preserve_relative_ids(rocrate)
143+
144+
if _uses_https_schema(rocrate_graph):
145+
rocrate_entity_mod_sparql = rocrate_entity_mod_sparql.replace(
146+
"http://schema.org/",
147+
"https://schema.org/",
148+
)
149+
120150
rocrate_graph.update(rocrate_entity_mod_sparql)
121-
context_uri = "https://w3id.org/ro/crate/1.1/context"
151+
152+
context = rocrate.get(
153+
"@context",
154+
"https://w3id.org/ro/crate/1.2/context",
155+
)
156+
122157
rocrate_graph.serialize(
123-
Path(temp_rocrate_path, "ro-crate-metadata.json"),
158+
metadata_path,
124159
format="json-ld",
125-
context=context_uri,
160+
context=context,
126161
indent=2,
127162
use_native_types=True,
128163
)
164+
129165
return temp_rocrate_path
130166

131167

@@ -151,30 +187,36 @@ def do_entity_test( # pylint: disable=too-many-locals
151187
"""
152188
Shared function to test a RO-Crate entity.
153189
154-
Additional keyword arguments (kwargs) are passed along to initialise ValidationSettings.
190+
Additional keyword arguments (kwargs) are passed along to initialise
191+
ValidationSettings.
155192
"""
156193
assert not (rocrate_entity_patch and rocrate_entity_mod_sparql), (
157194
"Cannot use rocrate_entity_patch and rocrate_entity_mod_sparql together"
158195
)
159196

160-
# declare variables
161197
failed_requirements = None
162198
detected_issues = None
163199

164200
if not isinstance(rocrate_path, Path) and not rocrate_path.startswith("http"):
165201
rocrate_path = Path(rocrate_path)
166202

167203
temp_rocrate_path = None
204+
168205
if (
169206
any([rocrate_entity_patch, rocrate_entity_mod_sparql])
170207
and isinstance(rocrate_path, Path)
171208
and rocrate_path.is_dir()
172209
):
173-
temp_rocrate_path = _prepare_temp_rocrate(rocrate_path, rocrate_entity_patch, rocrate_entity_mod_sparql)
210+
temp_rocrate_path = _prepare_temp_rocrate(
211+
rocrate_path,
212+
rocrate_entity_patch,
213+
rocrate_entity_mod_sparql,
214+
)
174215
rocrate_path = temp_rocrate_path
175216

176217
if expected_triggered_requirements is None:
177218
expected_triggered_requirements = []
219+
178220
if expected_triggered_issues is None:
179221
expected_triggered_issues = []
180222

@@ -183,8 +225,8 @@ def do_entity_test( # pylint: disable=too-many-locals
183225
logger.debug("Requirement severity: %s", requirement_severity)
184226
logger.debug("Checks to skip: %s", skip_checks)
185227

186-
# validate RO-Crate
187228
relative_root_path = Path(rocrate_relative_root_path) if rocrate_relative_root_path is not None else None
229+
188230
result: models.ValidationResult = services.validate(
189231
models.ValidationSettings(
190232
rocrate_uri=models.URI(rocrate_path),
@@ -198,50 +240,82 @@ def do_entity_test( # pylint: disable=too-many-locals
198240
**kwargs,
199241
)
200242
)
201-
logger.debug("Expected validation result: %s", expected_validation_result)
243+
244+
logger.debug(
245+
"Expected validation result: %s",
246+
expected_validation_result,
247+
)
202248

203249
assert result.context is not None, "Validation context should not be None"
250+
204251
logger.debug(
205252
"Expected requirement severity to be %s, got %s",
206253
requirement_severity,
207254
result.context.requirement_severity,
208255
)
256+
209257
assert result.passed() == expected_validation_result, (
210258
f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}"
211259
)
212260

213-
# check requirement
214-
failed_requirements = [_.name for _ in result.failed_requirements]
261+
failed_requirements = [requirement.name for requirement in result.failed_requirements]
215262

216-
# check that the expected requirements are triggered
217263
for expected_triggered_requirement in expected_triggered_requirements:
218264
if expected_triggered_requirement not in failed_requirements:
219265
raise AssertionError(
220-
f"The expected requirement "
221-
f'"{expected_triggered_requirement}" was not found in the failed requirements'
266+
"The expected requirement "
267+
f'"{expected_triggered_requirement}" was not found in the '
268+
"failed requirements"
222269
)
223270

224-
# check requirement issues
225271
detected_issues = [
226272
issue.message for issue in result.get_issues(requirement_severity) if issue.message is not None
227273
]
274+
228275
logger.debug("Detected issues: %s", detected_issues)
229276
logger.debug("Expected issues: %s", expected_triggered_issues)
277+
230278
for expected_issue in expected_triggered_issues:
231-
if not any(expected_issue in issue for issue in detected_issues): # support partial match
279+
if not any(expected_issue in issue for issue in detected_issues):
232280
raise AssertionError(f'The expected issue "{expected_issue}" was not found in the detected issues')
281+
233282
except Exception:
234283
if logger.isEnabledFor(logging.DEBUG):
235-
logger.exception("Failed to validate RO-Crate @ path: %s", rocrate_path)
236-
logger.debug("Requirement severity: %s", requirement_severity)
237-
logger.debug("Expected validation result: %s", expected_validation_result)
238-
logger.debug("Expected triggered requirements: %s", expected_triggered_requirements)
239-
logger.debug("Expected triggered issues: %s", expected_triggered_issues)
240-
logger.debug("Failed requirements: %s", failed_requirements)
241-
logger.debug("Detected issues: %s", detected_issues)
284+
logger.exception(
285+
"Failed to validate RO-Crate @ path: %s",
286+
rocrate_path,
287+
)
288+
logger.debug(
289+
"Requirement severity: %s",
290+
requirement_severity,
291+
)
292+
logger.debug(
293+
"Expected validation result: %s",
294+
expected_validation_result,
295+
)
296+
logger.debug(
297+
"Expected triggered requirements: %s",
298+
expected_triggered_requirements,
299+
)
300+
logger.debug(
301+
"Expected triggered issues: %s",
302+
expected_triggered_issues,
303+
)
304+
logger.debug(
305+
"Failed requirements: %s",
306+
failed_requirements,
307+
)
308+
logger.debug(
309+
"Detected issues: %s",
310+
detected_issues,
311+
)
312+
242313
raise
314+
243315
finally:
244-
# cleanup
245316
if temp_rocrate_path is not None:
246-
logger.debug("Cleaning up temporary RO-Crate @ path: %s", temp_rocrate_path)
317+
logger.debug(
318+
"Cleaning up temporary RO-Crate @ path: %s",
319+
temp_rocrate_path,
320+
)
247321
shutil.rmtree(temp_rocrate_path)

0 commit comments

Comments
 (0)