From ab66c7737ba062f23182364c3ca70c26b46d3e3f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 Jul 2026 11:34:00 +0200 Subject: [PATCH 001/271] =?UTF-8?q?fix(cli):=20=F0=9F=90=9B=20disable=20Ri?= =?UTF-8?q?ch=20word-wrap=20in=20JSON=20validation=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rich's Console.print() was wrapping long string values (messages, URLs) in the JSON output at the configured line width, inserting literal newlines inside quoted strings and producing invalid JSON. Pass soft_wrap=True to emit the report unmodified. --- rocrate_validator/cli/commands/validate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 5c759ed03..91e1e6c79 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -670,7 +670,10 @@ def _emit_json_report( with output_file.open("w", encoding="utf-8") if output_file else nullcontext(sys.stdout) as f: out = Console(width=output_line_width, file=f) out.register_formatter(JSONOutputFormatter()) - out.print(results) + # Disable word-wrap/cropping: Rich would otherwise insert literal line + # breaks into long string values (e.g. messages, URLs), producing + # invalid, unescaped control characters in the JSON output. + out.print(results, soft_wrap=True) if interactive and output_file: console.print("[bold]DONE![/bold]", end="\n\n") From aea7f3e691a8dd62596c5c925fdce14f817ff7c4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 Jul 2026 17:32:02 +0200 Subject: [PATCH 002/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.11.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 11f872202..bcb27df93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.11.2" +version = "0.11.3" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From bec32940ac923fc6d5948dd223e0c90e7958a982 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 Jul 2026 17:32:51 +0200 Subject: [PATCH 003/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.11.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f818785aa..bd550dcfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.11.3] - 2026-07-28 + +Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.2...0.11.3 + +### 🐛 Fixed + +- fix(cli): disable Rich word-wrap in the JSON validation report to prevent literal newlines inside quoted strings that produced invalid JSON ([ab66c773](https://github.com/crs4/rocrate-validator/commit/ab66c773)) + ## [0.11.2] - 2026-06-25 Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.1...0.11.2 From be6aa6ffe98cb55869c6e9b57634d08e1cc53f0d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Aug 2026 13:59:08 +0200 Subject: [PATCH 004/271] =?UTF-8?q?perf(profiles):=20=E2=9A=A1=EF=B8=8F=20?= =?UTF-8?q?eliminate=20cross=20products=20in=20SPARQL=20target=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `sh:select` queries of several `sh:SPARQLTarget` shapes combined two high-cardinality patterns sharing no variables — `?this a ` and `?metadatafile schema:about ?root` — related only by the FILTERs applied afterwards. rdflib materialises the full cartesian product before filtering. Pre-compute the metadata descriptor in a `SELECT DISTINCT` subquery, which SPARQL evaluates and materialises first, so the outer pattern joins against an already-known `?root` instead of building the product. --- .../1.1/must/4_data_entity_metadata.ttl | 18 ++++++++++++------ .../1.2/must/4_data_entity_metadata.ttl | 18 ++++++++++++------ .../ro-crate/1.2/must/8_workflow_metadata.ttl | 18 ++++++++++++------ .../ro-crate/1.2/should/0_entity_metadata.ttl | 9 +++++---- .../1.2/should/4_data_entity_metadata.ttl | 6 ++++-- .../1.2/should/4_dataset_data_entity.ttl | 6 ++++-- .../1.2/should/4_referenced_rocrate.ttl | 12 ++++++++---- .../1.2/should/8_workflow_metadata.ttl | 6 ++++-- 8 files changed, 61 insertions(+), 32 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl index 2a458045d..f69cb2a8c 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl @@ -47,9 +47,11 @@ ro-crate:FileDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:MediaObject . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } """ @@ -86,10 +88,12 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . # Exclude all dataset entities that ends with `./#` - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } @@ -157,9 +161,11 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?root schema:hasPart ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(?this != ?metadatafile) FILTER NOT EXISTS { diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl index ada5c742c..44750088b 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl @@ -44,9 +44,11 @@ ro-crate:FileDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:MediaObject . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } """ @@ -93,10 +95,12 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . # Exclude all dataset entities that ends with `./#` - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } @@ -161,9 +165,11 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?root schema:hasPart ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(?this != ?metadatafile) FILTER NOT EXISTS { diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl index 3e1aa66e1..9f771b58e 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl @@ -26,9 +26,11 @@ ro-crate:SoftwareSourceCodeTarget a sh:SPARQLTarget ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:SoftwareSourceCode . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) } """ . @@ -48,9 +50,11 @@ ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:SoftwareSourceCode . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER NOT EXISTS { ?this a bioschemas:ComputationalWorkflow } } """ @@ -78,9 +82,11 @@ ro-crate:WorkflowDefinition a sh:NodeShape, validator:HiddenShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a bioschemas:ComputationalWorkflow . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl index 007c0f5cb..96a43f606 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl @@ -27,10 +27,11 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; WHERE { ?this a ?type . - # Exclude Root and RO-Crate Metadata File entities - ?root a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } FILTER(?this != ?metadatafile) FILTER (?this != ?root) # Exclude entities with non-IRI identifiers or those from specific namespaces diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl index bcb836835..a1dda19d0 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl @@ -164,9 +164,11 @@ ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) FILTER regex(str(?this), "^https?://", "i") diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl index d46263091..f5ed4db9f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl @@ -21,9 +21,11 @@ ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) FILTER(!regex(str(?this), "^https?://", "i")) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl index 26a96b3a2..e7bd059ae 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl @@ -35,8 +35,10 @@ ro-crate:ReferencedROCrateDataEntityTarget a sh:SPARQLTarget ; ?this a schema:Dataset ; dct:conformsTo ?profile . FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } FILTER(?this != ?root) } """ . @@ -56,8 +58,10 @@ ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; dct:conformsTo ?profile ; schema:subjectOf ?this . FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } FILTER(?crate != ?root) } """ . diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl index c4a9aeebd..bb87d0583 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl @@ -29,8 +29,10 @@ ro-crate:ScriptOrWorkflowImageTarget a sh:SPARQLTarget ; WHERE { ?sw a schema:SoftwareSourceCode . ?sw schema:image ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } } """ . From 88ded678cd0009df06209d8a48bbc7346ae5fbc6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Aug 2026 15:04:28 +0200 Subject: [PATCH 005/271] =?UTF-8?q?perf(profiles):=20=E2=9A=A1=EF=B8=8F=20?= =?UTF-8?q?deduplicate=20contextual=20entity=20target=20before=20filtering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `ro-crate:ContextualEntityDefinition` target matched every triple with `?this ?p ?o` and then applied 6 `FILTER NOT EXISTS` plus 17 `STRSTARTS` exclusions to each match. The `SELECT DISTINCT` collapsed the duplicates only afterwards, so the filters ran once per triple rather than once per entity. --- .../1.2/must/6_contextual_entity_metadata.ttl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl index 80168a0b1..dd1c9e149 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl @@ -37,14 +37,20 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ - SELECT DISTINCT ?this + SELECT ?this WHERE { # Structural criteria: IRI or blank node, subject of some triple. # SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) # tag even referenced-but-undescribed entities so they become # subjects and are correctly classified as Contextual Entities. - ?this ?p ?o . - FILTER(isIRI(?this) || isBlank(?this)) + # + # The distinct subjects are materialised by a subquery FIRST so the + # exclusion FILTERs below run once per entity instead of once per + # triple (~2.5k subjects vs ~40k triples on a large crate). + { SELECT DISTINCT ?this WHERE { + ?this ?p ?o . + FILTER(isIRI(?this) || isBlank(?this)) + } } # Structural exclusions — Metadata Descriptor, Root Data Entity, # Data Entities (File/Dataset or ro-crate:DataEntity), Ontology. From 7b7829750bac3a6b28293310d6f5de4b71c2ae0b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 25 Aug 2026 13:12:20 +0200 Subject: [PATCH 006/271] =?UTF-8?q?refactor(profiles):=20=E2=99=BB=20resol?= =?UTF-8?q?ve=20bare=20profile=20tokens=20to=20the=20highest=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile.py | 59 +++++++++++++++++++++++--- rocrate_validator/models/validation.py | 46 ++++++++++---------- tests/conftest.py | 7 ++- 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 6d245fea2..9d350f17e 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -768,10 +768,62 @@ def all(cls) -> list[Profile]: """ return list(cls.__profiles_map.values()) + @staticmethod + def version_sort_key(version: str | None) -> tuple[int, ...]: + """ + Sort key for a profile version. + + The version is compared component by component as a tuple of integers, so that + e.g. `1.10` sorts after `1.9` (a plain string comparison would get this wrong). + Non-numeric components and missing versions sort lowest. + + :param version: the version string + :type version: str | None + + :return: the sort key + :rtype: tuple[int, ...] + """ + if not version: + return () + return tuple(int(part) if part.isdigit() else 0 for part in str(version).split(".")) + + @classmethod + def resolve_in_list(cls, profiles: Collection[Profile], profile_identifier: str) -> Profile | None: + """ + Resolve the given identifier against the given list of profiles. + + An exact match on the profile identifier (e.g. `ro-crate-1.2`) always wins. + Failing that, the identifier is treated as a bare token (e.g. `ro-crate`) and + resolves to the **highest available version** of that token. + + :param profiles: the list of profiles + :type profiles: Collection[Profile] + + :param profile_identifier: the identifier or token + :type profile_identifier: str + + :return: the profile if found, None otherwise + :rtype: Optional[Profile] + """ + exact_match = next((p for p in profiles if p.identifier == profile_identifier), None) + if exact_match is not None: + return exact_match + candidates = [ + p + for p in profiles + if p.token == profile_identifier or str(p.identifier).replace(f"-{p.version}", "") == profile_identifier + ] + if not candidates: + return None + return max(candidates, key=lambda p: cls.version_sort_key(p.version)) + @classmethod def find_in_list(cls, profiles: Collection[Profile], profile_identifier: str) -> Profile | None: """ - Find a profile with the given identifier in the given list of profiles + Find a profile with the given identifier in the given list of profiles. + + Resolution follows :meth:`resolve_in_list`: an exact identifier match wins, + otherwise a bare token resolves to the highest available version. :param profiles: the list of profiles :type profiles: Collection[Profile] @@ -782,10 +834,7 @@ def find_in_list(cls, profiles: Collection[Profile], profile_identifier: str) -> :return: the profile if found, None otherwise :rtype: Optional[Profile] """ - profile = next((p for p in profiles if p.identifier == profile_identifier), None) or next( - (p for p in profiles if str(p.identifier).replace(f"-{p.version}", "") == profile_identifier), - None, - ) + profile = cls.resolve_in_list(profiles, profile_identifier) if not profile: raise ProfileNotFound(profile_identifier) return profile diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 9974d482b..3bf0a66d1 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -626,30 +626,28 @@ def __load_profiles__(self) -> list[Profile]: allow_requirement_check_override=self.allow_requirement_check_override, ) - # Check if the target profile is in the list of profiles - profile = Profile.get_by_identifier(self.profile_identifier) - if not profile: - try: - candidate_profiles = Profile.get_by_token(self.profile_identifier) - logger.debug("Candidate profiles found by token: %s", profile) - if candidate_profiles: - # Find the profile with the highest version number - profile = max(candidate_profiles, key=lambda p: p.version or "") - self.settings.profile_identifier = profile.identifier - logger.debug("Profile with the highest version number: %s", profile) - except AttributeError as e: - # raised when the profile is not found - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Profile not found: %s", self.profile_identifier) - raise ProfileNotFound( - self.profile_identifier, - message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", - ) from e - if profile is None: - raise ProfileNotFound( - self.profile_identifier, - message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", - ) + # Check if the target profile is in the list of profiles. A bare token + # (e.g. `ro-crate`) resolves to the highest available version. + try: + profile = Profile.resolve_in_list(profiles, self.profile_identifier) + except AttributeError as e: + # raised when the profile is not found + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Profile not found: %s", self.profile_identifier) + raise ProfileNotFound( + self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", + ) from e + if profile is None: + raise ProfileNotFound( + self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", + ) + # Record the resolved identifier, so that downstream consumers (e.g. the + # statistics) agree on which profile was actually used. + if profile.identifier != self.profile_identifier: + logger.debug("Profile %r resolved to %r", self.profile_identifier, profile.identifier) + self.settings.profile_identifier = profile.identifier # if the inheritance is enabled, return only the target profile if not self.inheritance_enabled: diff --git a/tests/conftest.py b/tests/conftest.py index 4f4ad1e36..883b24a20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,8 +39,11 @@ # profiles paths PROFILES_PATH = str(Path(f"{CURRENT_PATH}/../rocrate_validator/profiles").resolve()) -# Dynamically update the SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER -rocrate_profile = services.get_profile("ro-crate") +# Dynamically update the SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER. +# Pinned to `ro-crate-1.1` on purpose: the suites using this identifier validate +# against 1.1 or against profiles that inherit from it, so the check id has to +# come from 1.1 regardless of which profile is the default. +rocrate_profile = services.get_profile("ro-crate-1.1") if not rocrate_profile: raise RuntimeError("Unable to load the RO-Crate profile") check_local_data_entity_existence = rocrate_profile.get_requirement_check("Data Entity: REQUIRED resource availability") From e60d0558248670402b830d5a8f8f7f3972ab7218 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 25 Aug 2026 16:35:19 +0200 Subject: [PATCH 007/271] =?UTF-8?q?test:=20=E2=9C=85=20decouple=20the=20RO?= =?UTF-8?q?-Crate=20test=20suite=20from=20the=20default=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/shared.py | 41 +++++++++++++++++-- .../requirements/test_load_requirements.py | 9 ++-- tests/unit/test_services.py | 21 ++++++++-- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/tests/shared.py b/tests/shared.py index 9fbe3bce2..257cdc950 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -28,7 +28,6 @@ import rdflib from rocrate_validator import models, services -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER logger = logging.getLogger(__name__) @@ -37,6 +36,24 @@ SPARQL_PREFIXES = "PREFIX schema: " +# Identifier of the RO-Crate 1.1 profile. It is the profile assumed by the tests +# that exercise 1.1 semantics on the 1.1 crate corpus, and the default used by +# `do_entity_test()`; tests that target another profile (e.g. 1.2) pass its +# identifier explicitly. +# +# This is a hard-coded literal on purpose, NOT `DEFAULT_PROFILE_IDENTIFIER`: the +# validator's default profile changes over time (it is 1.2 today), while the tests +# that use this constant must keep targeting 1.1 even when the default moves on. +RO_CRATE_1_1_PROFILE_IDENTIFIER = "ro-crate-1.1" + +# JSON-LD context to use when re-serialising a crate patched via SPARQL, per +# profile. Profiles that are not listed inherit from RO-Crate 1.1 and use its context. +_PROFILE_CONTEXT_URIS = { + "ro-crate-1.1": "https://w3id.org/ro/crate/1.1/context", + "ro-crate-1.2": "https://w3id.org/ro/crate/1.2/context", +} +_DEFAULT_CONTEXT_URI = _PROFILE_CONTEXT_URIS[RO_CRATE_1_1_PROFILE_IDENTIFIER] + def first(c: Collection[T]) -> T: return next(iter(c)) @@ -89,10 +106,22 @@ def replace_uri_in_graph(graph, old_uri_str, new_uri_str): return g +def _context_uri_for_profile(profile_identifier: str) -> str: + """ + Return the JSON-LD context matching the profile under test. + + Keeping the context aligned with the profile matters: re-serialising a 1.1 + context while validating against 1.2 trips the `ro-crate-1.2_2.2` check and + the test would fail for a reason unrelated to what it is asserting. + """ + return _PROFILE_CONTEXT_URIS.get(profile_identifier, _DEFAULT_CONTEXT_URI) + + def _prepare_temp_rocrate( rocrate_path: Path, rocrate_entity_patch: dict | None, rocrate_entity_mod_sparql: str | None, + context_uri: str = _DEFAULT_CONTEXT_URI, ) -> Path: # `mkdtemp` returns a stable path the test owns; `TemporaryDirectory().name` # was deleted on GC before copytree ran. `dirs_exist_ok=True` lets us copy @@ -112,7 +141,6 @@ def _prepare_temp_rocrate( if rocrate_entity_mod_sparql is not None: rocrate_graph = load_graph_and_preserve_relative_ids(rocrate) rocrate_graph.update(rocrate_entity_mod_sparql) - context_uri = "https://w3id.org/ro/crate/1.1/context" rocrate_graph.serialize( Path(temp_rocrate_path, "ro-crate-metadata.json"), format="json-ld", @@ -133,7 +161,7 @@ def do_entity_test( # pylint: disable=too-many-locals expected_triggered_requirements: list[str] | None = None, expected_triggered_issues: list[str] | None = None, abort_on_first: bool = False, - profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, + profile_identifier: str = RO_CRATE_1_1_PROFILE_IDENTIFIER, rocrate_entity_patch: dict | None = None, rocrate_entity_mod_sparql: str | None = None, skip_checks: list[str] | None = None, @@ -164,7 +192,12 @@ def do_entity_test( # pylint: disable=too-many-locals and isinstance(rocrate_path, Path) and rocrate_path.is_dir() ): - temp_rocrate_path = _prepare_temp_rocrate(rocrate_path, rocrate_entity_patch, rocrate_entity_mod_sparql) + temp_rocrate_path = _prepare_temp_rocrate( + rocrate_path, + rocrate_entity_patch, + rocrate_entity_mod_sparql, + context_uri=_context_uri_for_profile(profile_identifier), + ) rocrate_path = temp_rocrate_path if expected_triggered_requirements is None: diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index c21ccbe10..854aef099 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -18,11 +18,11 @@ import pytest -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.errors import CheckDependencyError from rocrate_validator.models import LevelCollection, Profile, RequirementLoader, Severity from rocrate_validator.requirements.shacl.requirements import SHACLRequirement from tests.ro_crates import InvalidFileDescriptorEntity +from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER # set up logging logger = logging.getLogger(__name__) @@ -111,8 +111,11 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): # The number of profiles should be greater than 0 assert len(profiles) > 0 - # The first profile should be the default profile - assert profiles[0].identifier == DEFAULT_PROFILE_IDENTIFIER + # The first profile should be the base RO-Crate 1.1 profile: it is the root of + # the inheritance graph, and profiles are ordered parents-first. This is a + # literal rather than `DEFAULT_PROFILE_IDENTIFIER` because the assertion is + # about load *ordering*, not about which profile happens to be the default. + assert profiles[0].identifier == RO_CRATE_1_1_PROFILE_IDENTIFIER # Get the first profile profile = profiles[0] diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py index 009d8223a..be58b7b04 100644 --- a/tests/unit/test_services.py +++ b/tests/unit/test_services.py @@ -22,6 +22,7 @@ from rocrate_validator.services import detect_profiles, get_profiles, validate from rocrate_validator.utils import log as logging from tests.ro_crates import InvalidFileDescriptorEntity, InvalidMultiProfileROC, ValidROC +from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER # set up logging logger = logging.getLogger(__name__) @@ -107,7 +108,14 @@ def test_disable_inherited_profiles_issue_reporting(): logger.debug("Validating a local RO-Crate: %s", crate_path) # First, validate with inherited profiles issue reporting enabled - settings = ValidationSettings(rocrate_uri=URI(crate_path), disable_inherited_profiles_issue_reporting=False) + # Pinned to the profile the crate declares: the assertions below are about + # issues being attributed to the main profile rather than to the inherited + # ones, so validating against anything else would make them vacuous. + settings = ValidationSettings( + rocrate_uri=URI(crate_path), + profile_identifier="workflow-testing-ro-crate-0.1", + disable_inherited_profiles_issue_reporting=False, + ) result = validate(settings) total_issues_with_inheritance = len(result.get_issues()) logger.debug("Total issues with inherited profiles issue reporting enabled: %d", total_issues_with_inheritance) @@ -135,7 +143,7 @@ def test_skip_pycheck_on_workflow_ro_crate(): # Set the rocrate_uri to the workflow testing RO-Crate crate_path = InvalidFileDescriptorEntity().invalid_conforms_to logger.debug("Validating a local RO-Crate: %s", crate_path) - settings = ValidationSettings(rocrate_uri=URI(crate_path)) + settings = ValidationSettings(rocrate_uri=URI(crate_path), profile_identifier=RO_CRATE_1_1_PROFILE_IDENTIFIER) result = validate(settings) assert not result.passed(), ( "The RO-Crate is expected to be invalid because of an incorrect conformsTo field and missing resources" @@ -186,7 +194,11 @@ def test_valid_crate_folder_with_metadata_only(): shutil.copy(metadata_src, metadata_dst) # Define shared settings object - settings = ValidationSettings(rocrate_uri=URI(Path(tmpdirname)), metadata_only=True) + settings = ValidationSettings( + rocrate_uri=URI(Path(tmpdirname)), + profile_identifier=RO_CRATE_1_1_PROFILE_IDENTIFIER, + metadata_only=True, + ) profiles = detect_profiles(settings) @@ -211,7 +223,8 @@ def test_valid_crate_metadata_dict_with_metadata_only(): # Define shared settings object settings = ValidationSettings( # type: ignore[call-arg] # rocrate_uri not needed in metadata-dict mode - metadata_dict=metadata_dict + metadata_dict=metadata_dict, + profile_identifier=RO_CRATE_1_1_PROFILE_IDENTIFIER, ) profiles = detect_profiles(settings) From cae8c703d1bfff08a3d95629d2123bcab3fa22a5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 25 Aug 2026 16:36:35 +0200 Subject: [PATCH 008/271] =?UTF-8?q?fix(profiles):=20=F0=9F=90=9B=20state?= =?UTF-8?q?=20the=201.2=20preview=20doctype=20issue=20as=20a=20MUST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/ro-crate/1.2/must/1_ro_crate_preview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index baa7274d6..07991488b 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -44,7 +44,7 @@ def check_preview_html(self, context: ValidationContext) -> CheckResultValue: content = context.ro_crate.get_file_content(preview_path, binary_mode=False) if " Date: Tue, 25 Aug 2026 17:17:01 +0200 Subject: [PATCH 009/271] =?UTF-8?q?feat(profiles)!:=20=E2=9C=A8=20default?= =?UTF-8?q?=20to=20the=20RO-Crate=201.2=20base=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DEFAULT_PROFILE_IDENTIFIER` moves from `ro-crate-1.1` to `ro-crate-1.2`, so `ValidationSettings` and `profiles describe` target 1.2 when no profile is given. **BREAKING CHANGE**: validating without an explicit profile now runs the RO-Crate 1.2 profile. Pass `profile_identifier='ro-crate-1.1'` (or `-p ro-crate-1.1`) to keep the previous behaviour. --- rocrate_validator/cli/commands/profiles.py | 2 +- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/constants.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index c2cc6a3db..9e353766b 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -186,7 +186,7 @@ def describe_profile( \b The check identifier accepts either form: * relative: . (e.g. "1.2") - * full: _. (e.g. "ro-crate-1.1_1.2") + * full: _. (e.g. "ro-crate-1.2_1.2") With -v on a single check, the source code of the check is shown. """ diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 640164e40..6fccaca8d 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -167,7 +167,7 @@ def validate_uri(ctx, param, value): help=( "[bold yellow]Fully-Qualified-Check-IDs[/bold yellow] is a comma-separated list of checks to skip " "(may be specified multiple times). Each check must be specified by its " - "Fully Qualified Identifier, e.g., [bold cyan]ro-crate-1.1_12.1[/bold cyan]. The fully qualified " + "Fully Qualified Identifier, e.g., [bold cyan]ro-crate-1.2_12.1[/bold cyan]. The fully qualified " "check identifier has the format _., " "where is the position number of the Requirement in the profile, " "and is the position number of the RequirementCheck within that Requirement. " diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 37adb5444..e487370c1 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -42,7 +42,7 @@ ROCRATE_METADATA_FILE = "ro-crate-metadata.json" # Define the default profiles name -DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.1" +DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.2" # Define the default profiles path DEFAULT_PROFILES_PATH = "profiles" From a33601aa0c019e86b8ce773e43120b4684cf8b5c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 25 Aug 2026 17:25:38 +0200 Subject: [PATCH 010/271] =?UTF-8?q?test:=20=E2=9C=85=20cover=20the=20new?= =?UTF-8?q?=20default=20profile=20and=20token=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/test_offline_mode.py | 15 +++- tests/test_cli.py | 31 +++++++ tests/unit/test_default_profile.py | 115 +++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_default_profile.py diff --git a/tests/integration/test_offline_mode.py b/tests/integration/test_offline_mode.py index 76039acac..127370400 100644 --- a/tests/integration/test_offline_mode.py +++ b/tests/integration/test_offline_mode.py @@ -301,7 +301,16 @@ def test_cli_cache_reset_yes_clears_entries(cli_runner, tmp_path, network_interc assert HttpRequester().cache_info()["entries"] == 0 -def test_cli_cache_warm_populates_profile_urls(cli_runner, tmp_path, network_interceptor): +@pytest.mark.parametrize( + ("profile_identifier", "context_url"), + [ + ("ro-crate-1.1", "https://w3id.org/ro/crate/1.1/context"), + ("ro-crate-1.2", "https://w3id.org/ro/crate/1.2/context"), + ], +) +def test_cli_cache_warm_populates_profile_urls( + cli_runner, tmp_path, network_interceptor, profile_identifier, context_url +): cache_path = tmp_path / "cache" result = cli_runner.invoke( cli, @@ -312,7 +321,7 @@ def test_cli_cache_warm_populates_profile_urls(cli_runner, tmp_path, network_int "--cache-path", str(cache_path), "--profile-identifier", - "ro-crate-1.1", + profile_identifier, ], ) assert result.exit_code == 0, result.output @@ -322,7 +331,7 @@ def test_cli_cache_warm_populates_profile_urls(cli_runner, tmp_path, network_int # The URL must now be cached for offline use. HttpRequester.reset() HttpRequester.initialize_cache(cache_path=str(cache_path), cache_max_age=3600, offline=True) - assert HttpRequester().has_cached("https://w3id.org/ro/crate/1.1/context") is True + assert HttpRequester().has_cached(context_url) is True def test_cli_cache_warm_crate_caches_remote_archive(cli_runner, tmp_path, network_interceptor): diff --git a/tests/test_cli.py b/tests/test_cli.py index 91971a321..31f15c20f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -28,6 +28,7 @@ from rocrate_validator.utils.versioning import get_version from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import InvalidFileDescriptor, InvalidFileDescriptorEntity, ValidROC +from tests.ro_crates_v1_2 import ValidROCrate12 # set up logging logger = logging.getLogger(__name__) @@ -290,6 +291,36 @@ def test_profiles_list(cli_runner: CliRunner): assert "ro-crate-1.1" in result.output # Check for a known profile +def test_validate_no_auto_profile_falls_back_to_1_2(cli_runner: CliRunner): + """ + With auto-detection disabled the base `ro-crate` profile is used, and that + now resolves to 1.2. + """ + result = cli_runner.invoke( + cli, + ["validate", str(ValidROCrate12().attached), "--no-auto-profile", "--no-paging"], + ) + assert result.exit_code == 0 + assert "ro-crate-1.2" in result.output + + +def test_validate_auto_profile_still_uses_the_declared_profile(cli_runner: CliRunner): + """ + Auto-detection keeps a 1.1 crate on the 1.1 profile: the new default only + applies when no profile can be detected. + """ + result = cli_runner.invoke(cli, ["validate", str(ValidROC().wrroc_paper), "--no-paging"]) + assert result.exit_code == 0 + assert "ro-crate-1.1" in result.output + + +def test_profiles_describe_defaults_to_1_2(cli_runner: CliRunner): + """`profiles describe` with no argument describes the default profile.""" + result = cli_runner.invoke(cli, ["profiles", "describe", "--no-paging"]) + assert result.exit_code == 0 + assert "Profile: ro-crate-1.2" in result.output + + def test_extra_profiles_list(cli_runner: CliRunner, fake_profiles_path: Path): """ Test the list of extra profiles. diff --git a/tests/unit/test_default_profile.py b/tests/unit/test_default_profile.py new file mode 100644 index 000000000..1e23c5989 --- /dev/null +++ b/tests/unit/test_default_profile.py @@ -0,0 +1,115 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for the default validation profile and for the resolution of bare +profile tokens (e.g. `ro-crate`) to a concrete profile version. +""" + +import logging + +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER +from rocrate_validator.models import URI, Profile, Severity, ValidationSettings +from rocrate_validator.services import get_profile, get_profiles, validate +from tests.ro_crates import ValidROC +from tests.ro_crates_v1_2 import ValidROCrate12 +from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER + +logger = logging.getLogger(__name__) + +EXPECTED_DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.2" + + +def test_default_profile_identifier_is_ro_crate_1_2(): + """The base RO-Crate profile used by default is 1.2.""" + assert DEFAULT_PROFILE_IDENTIFIER == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_validation_settings_default_profile(): + """`ValidationSettings` picks up the default profile when none is given.""" + settings = ValidationSettings(rocrate_uri=URI(str(ValidROC().wrroc_paper))) + assert settings.profile_identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_bare_token_resolves_to_highest_version_via_services(): + """`get_profile` resolves a bare token to the highest available version.""" + assert get_profile("ro-crate").identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_explicit_identifier_wins_over_token_resolution(): + """An exact identifier is never overridden by the highest-version rule.""" + assert get_profile(RO_CRATE_1_1_PROFILE_IDENTIFIER).identifier == RO_CRATE_1_1_PROFILE_IDENTIFIER + assert get_profile("ro-crate-1.2").identifier == "ro-crate-1.2" + + +def test_bare_token_resolves_to_highest_version_in_validation_context(): + """ + The validation context resolves a bare token the same way `get_profile` does. + + These two used to disagree: the context picked the highest version while + `Profile.find_in_list` returned the first match in load order. + """ + settings = ValidationSettings( + rocrate_uri=URI(str(ValidROC().wrroc_paper)), + profile_identifier="ro-crate", + requirement_severity=Severity.REQUIRED, + ) + validate(settings) + # The resolved identifier is written back to the settings + assert settings.profile_identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_version_sort_key_orders_versions_numerically(): + """Versions are compared component-wise, so 1.10 sorts after 1.9.""" + profiles = get_profiles() + ro_crate_profiles = [p for p in profiles if p.token == "ro-crate"] + assert len(ro_crate_profiles) >= 2, "Expected at least two versions of the RO-Crate profile" + highest = max(ro_crate_profiles, key=lambda p: Profile.version_sort_key(p.version)) + assert highest.identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + # A plain string comparison would rank "1.9" above "1.10" + assert Profile.version_sort_key("1.10") > Profile.version_sort_key("1.9") + assert Profile.version_sort_key(None) < Profile.version_sort_key("0.1") + + +def test_default_profile_validates_a_1_2_crate(): + """A 1.2 crate validates against the default profile without pinning it.""" + settings = ValidationSettings( + rocrate_uri=URI(str(ValidROCrate12().attached)), + requirement_severity=Severity.REQUIRED, + ) + result = validate(settings) + assert result.passed(), "A valid RO-Crate 1.2 should pass under the default profile" + + +def test_default_profile_rejects_a_1_1_context(): + """ + A 1.1-era crate no longer passes under the default profile. + + This documents the breaking part of the default switch: the crate declares the + 1.1 JSON-LD context, which 1.2 requires to be the 1.2 one. + """ + settings = ValidationSettings( + rocrate_uri=URI(str(ValidROC().wrroc_paper)), + requirement_severity=Severity.REQUIRED, + ) + result = validate(settings) + assert not result.passed() + failed_check_ids = {issue.check.identifier for issue in result.get_issues()} + assert "ro-crate-1.2_2.2" in failed_check_ids, ( + f"Expected the 1.2 context check to fail, got: {sorted(failed_check_ids)}" + ) + + # ...and it still passes when explicitly pinned to 1.1 + settings.profile_identifier = RO_CRATE_1_1_PROFILE_IDENTIFIER + assert validate(settings).passed(), "Pinning `ro-crate-1.1` should restore the previous behaviour" From 393e30e6c36ac0fd1fc3e51d99dd051c93bc03bc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 25 Aug 2026 17:40:31 +0200 Subject: [PATCH 011/271] =?UTF-8?q?docs:=20=F0=9F=93=9D=20document=20the?= =?UTF-8?q?=20switch=20to=20the=20RO-Crate=201.2=20default=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 11 +++++++---- docs/5_offline_mode.rst | 6 +++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fca8a775..7f04168fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### ⚠️ Breaking Changes + +- **The default validation profile is now `ro-crate-1.2`** (previously `ro-crate-1.1`). Since 1.2 + is self-contained, validation now applies 95 requirements instead of 30; crates declaring the + 1.1 context fail the `ro-crate-1.2_2.2` check under the default profile. +- This primarily affects API users when `profile_identifier` is omitted. CLI auto-detection is + unchanged unless no profile is detected or `--no-auto-profile` is used. +- To keep the previous behaviour, explicitly select `ro-crate-1.1` with `profile_identifier` or + `-p ro-crate-1.1`. + +### 🔧 Changed + +- refactor(profiles): resolve bare profile tokens to the highest available version using numeric + version ordering. +- refactor(validation): store the resolved profile identifier in validation settings so downstream + consumers use the profile that was actually applied. + + ## [0.11.4] - 2026-09-16 Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.3...0.11.4 diff --git a/README.md b/README.md index f554c7424..8bd2e0890 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ against different profiles, including the base RO-Crate profile and various exte - Validates RO-Crates against the profiles they declare to conform to. Currently, validation for the following profiles is implemented: - - [RO-Crate](https://w3id.org/ro/crate/1.1) *(base profile)* + - [RO-Crate 1.2](https://w3id.org/ro/crate/1.2) *(base profile, used by default)* + - [RO-Crate 1.1](https://w3id.org/ro/crate/1.1) *(base profile)* - [Workflow RO-Crate](https://w3id.org/workflowhub/workflow-ro-crate/1.0) - [Workflow Testing RO-Crate](https://w3id.org/ro/wftest) - [Workflow Run Crate](https://w3id.org/ro/wfrun/workflow) @@ -134,8 +135,10 @@ settings = services.ValidationSettings( # Set the path to the RO-Crate root directory rocrate_uri='/path/to/ro-crate', # Set the identifier of the RO-Crate profile to use for validation. - # If not set, the system will attempt to automatically determine the appropriate validation profile. - profile_identifier='ro-crate-1.1', + # If not set, the base RO-Crate profile is used, which defaults to `ro-crate-1.2`. + # Use `services.detect_profiles(settings)` to determine the profiles a crate declares + # conformance to, and pass one of them here to validate against it instead. + profile_identifier='ro-crate-1.2', # Set the requirement level for the validation requirement_severity=models.Severity.REQUIRED, ) @@ -158,7 +161,7 @@ The following is a possible output: ```bash RO-Crate is invalid! -Detected issue of severity REQUIRED with check "ro-crate-1.1:root_entity_exists: The RO-Crate must contain a root entity. +Detected issue of severity REQUIRED with check "ro-crate-1.2:root_entity_exists: The RO-Crate must contain a root entity. ``` ## Running the tests diff --git a/docs/5_offline_mode.rst b/docs/5_offline_mode.rst index b65e6831f..7bc08ec50 100644 --- a/docs/5_offline_mode.rst +++ b/docs/5_offline_mode.rst @@ -100,11 +100,11 @@ Before going offline you can pre-fetch everything you will need with rocrate-validator cache warm --all-profiles # Warm only specific profiles - rocrate-validator cache warm -p ro-crate-1.1 -p workflow-ro-crate-1.0 + rocrate-validator cache warm -p ro-crate-1.2 -p workflow-ro-crate-1.0 # Also fetch and cache remote RO-Crates or arbitrary URLs rocrate-validator cache warm --crate https://example.org/crate.zip - rocrate-validator cache warm -u https://w3id.org/ro/crate/1.1/context + rocrate-validator cache warm -u https://w3id.org/ro/crate/1.2/context When invoked without any source option, ``cache warm`` defaults to warming all installed profiles. A summary table reports which URLs were cached, skipped or @@ -125,7 +125,7 @@ The same offline behaviour can be enabled programmatically through settings = services.ValidationSettings( rocrate_uri='/path/to/ro-crate', - profile_identifier='ro-crate-1.1', + profile_identifier='ro-crate-1.2', # Serve every HTTP request from the cache; uncached resources fail. offline=True, # Optional: use a dedicated cache directory (defaults to the user cache). From d9e115cb71ee11bff7422b68ea6b6351b432360c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 2 Sep 2026 07:41:11 +0200 Subject: [PATCH 012/271] =?UTF-8?q?fix(profiles):=20=F0=9F=90=9B=20use=20c?= =?UTF-8?q?anonical=20namespace=20in=20RO-Crate=201.2=20ontology.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align the ontology prefix with the hyphenated namespace used by SHACL shapes and SPARQL prefixes, preventing external profiles from missing RootData and producing incorrect validation results. --- rocrate_validator/profiles/ro-crate/1.2/ontology.ttl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl index 84331ccc7..bade971e6 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl @@ -19,7 +19,7 @@ @prefix xsd: . @prefix rdfs: . @prefix schema: . -@prefix ro-crate: . +@prefix ro-crate: . @prefix bioschemas: . # @base <./.> . @@ -54,13 +54,13 @@ schema:SoftwareSourceCode rdf:type owl:Class ; bioschemas:ComputationalWorkflow rdf:type owl:Class . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Script +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/Script ro-crate:Script rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject ; rdfs:label "Script"@en . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Workflow +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/Workflow ro-crate:Workflow rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject, bioschemas:ComputationalWorkflow ; rdfs:label "Workflow"@en . From 568b8c414541de31410c513901573cc617fa2aec Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 3 Sep 2026 17:26:37 +0200 Subject: [PATCH 013/271] =?UTF-8?q?fix(rocrate):=20=F0=9F=90=9B=20normaliz?= =?UTF-8?q?e=20singleton-array=20`mainEntity`=20in=20`get=5Fmain=5Fworkflo?= =?UTF-8?q?w()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow-ro-crate/must/0_main_workflow.py | 15 +++++++++------ rocrate_validator/rocrate/metadata.py | 10 +++++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py index 8d13b1acd..e2ae6951f 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py @@ -42,12 +42,15 @@ def check_workflow(self, context: ValidationContext) -> bool: except ROCrateMetadataNotFoundError: logger.debug("Skipping main workflow check: metadata descriptor is not available") return True - except ValueError: - context.result.add_issue( - "Unable to check the existence of the main workflow file " - "because the metadata file descriptor doesn't contain a `mainEntity`", - self, - ) + except (TypeError, ValueError) as error: + if str(error) == "no main workflow in metadata file descriptor": + message = ( + "Unable to check the existence of the main workflow file " + "because the metadata file descriptor doesn't contain a `mainEntity`" + ) + else: + message = f"Unable to check the existence of the main workflow file: {error}" + context.result.add_issue(message, self) if logger.isEnabledFor(logging.DEBUG): logger.exception("Unable to check main workflow file existence") return False diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index 3c30f7f87..a12926d87 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -91,8 +91,16 @@ def get_root_data_entity_conforms_to(self) -> list[str] | None: def get_main_workflow(self) -> ROCrateEntity: root_data_entity = self.get_root_data_entity() main_workflow = root_data_entity.get_property("mainEntity") - if not main_workflow: + if main_workflow is None: raise ValueError("no main workflow in metadata file descriptor") + if isinstance(main_workflow, list): + if len(main_workflow) != 1: + raise ValueError("mainEntity must contain exactly one entity") + main_workflow = main_workflow[0] + if not isinstance(main_workflow, ROCrateEntity): + raise TypeError("mainEntity must reference an entity") + if not isinstance(main_workflow.id, str) or not main_workflow.id: + raise ValueError("mainEntity must reference an entity with a valid @id") return main_workflow def get_entity(self, entity_id: str) -> ROCrateEntity | None: From cd67075cb9c1cecd53f3f0c560216cc49d12d3ef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 3 Sep 2026 17:32:59 +0200 Subject: [PATCH 014/271] =?UTF-8?q?test(profiles):=20=E2=9C=85=20add=20reg?= =?UTF-8?q?ression=20tests=20for=20mainEntity=20singleton=20array?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow-ro-crate/test_main_workflow.py | 54 ++++++++++++++++- tests/unit/test_rocrate.py | 58 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py index 5c98c1f59..5df53d0e3 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -14,8 +14,11 @@ import logging +import pytest + from rocrate_validator.models import Severity -from tests.ro_crates import InvalidMainWorkflow +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER +from tests.ro_crates import InvalidMainWorkflow, ValidROC from tests.shared import do_entity_test # set up logging @@ -125,6 +128,55 @@ def test_main_workflow_file_existence(): ) +def test_main_workflow_singleton_array(): + """A singleton-array mainEntity resolves to the same workflow as an object.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_file_existence(): + """A missing workflow in a singleton array produces the normal validation issue.""" + do_entity_test( + InvalidMainWorkflow().main_workflow_no_files, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_identifier="workflow-ro-crate", + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +@pytest.mark.parametrize( + "main_entity", + [ + [], + [{"@id": "sort-and-change-case.ga"}, {"@id": "other-workflow.ga"}], + "sort-and-change-case.ga", + {"name": "sort-and-change-case.ga"}, + {"@id": 42}, + ], +) +def test_main_workflow_invalid_main_entity_is_reported(main_entity): + """Malformed mainEntity values fail validation without an unexpected exception.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["mainEntity"], + profile_identifier="workflow-ro-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"./": {"mainEntity": main_entity}}, + ) + + def test_workflow_diagram_file_existence(): """ Test a Workflow RO-Crate where the workflow diagram file is not in the diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index 39bf4099f..b9f8b1c2f 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -586,6 +586,64 @@ def _metadata_dict_with_id(entity_id: str) -> dict: } +def _metadata_dict_with_main_entity(main_entity: object) -> dict: + """Build a minimal RO-Crate metadata dict with a configurable mainEntity.""" + return { + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + }, + { + "@id": "./", + "@type": "Dataset", + "mainEntity": main_entity, + }, + { + "@id": "workflow.ga", + "@type": "File", + }, + ], + } + + +@pytest.mark.parametrize("main_entity", [{"@id": "workflow.ga"}, [{"@id": "workflow.ga"}]]) +def test_get_main_workflow_returns_entity_for_object_and_singleton_array(main_entity): + """A singular main workflow accessor accepts both JSON-LD representations.""" + crate = ROCrate.from_metadata_dict(_metadata_dict_with_main_entity(main_entity)) + root_data_entity = crate.metadata.get_root_data_entity() + + raw_main_entity = root_data_entity.get_property("mainEntity") + main_workflow = crate.metadata.get_main_workflow() + + if isinstance(main_entity, list): + assert isinstance(raw_main_entity, list), "Generic property access must preserve list values" + else: + assert isinstance(raw_main_entity, ROCrateEntity), "Object values must resolve to an entity" + assert isinstance(main_workflow, ROCrateEntity), "The singular accessor must return one entity" + assert main_workflow.id == "workflow.ga" + + +@pytest.mark.parametrize( + "main_entity", + [ + [], + [{"@id": "workflow.ga"}, {"@id": "other-workflow.ga"}], + "workflow.ga", + {"name": "workflow.ga"}, + {"@id": 42}, + ], +) +def test_get_main_workflow_rejects_invalid_values(main_entity): + """Invalid mainEntity cardinality and value shapes raise controlled errors.""" + crate = ROCrate.from_metadata_dict(_metadata_dict_with_main_entity(main_entity)) + + with pytest.raises((TypeError, ValueError), match="mainEntity"): + crate.metadata.get_main_workflow() + + @pytest.mark.parametrize( "entity_id", [ From 856546a49d61771c478b0a232bc12501313a46e0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 4 Sep 2026 09:38:12 +0200 Subject: [PATCH 015/271] =?UTF-8?q?fix(versioning):=20=F0=9F=90=9B=20resol?= =?UTF-8?q?ve=20package=20version=20outside=20Git=20checkout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/utils/versioning.py | 70 +++++++++++++++++++-------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/rocrate_validator/utils/versioning.py b/rocrate_validator/utils/versioning.py index 86e434e20..313835e06 100644 --- a/rocrate_validator/utils/versioning.py +++ b/rocrate_validator/utils/versioning.py @@ -15,69 +15,78 @@ import re import subprocess import sys +from importlib import metadata +from pathlib import Path from rocrate_validator.utils import log as logging -from rocrate_validator.utils.config import get_config +from rocrate_validator.utils.config import get_config, get_config_path # set up logging logger = logging.getLogger(__name__) +PACKAGE_NAME = "roc-validator" -def run_git_command(command: list[str]) -> str | None: +def run_git_command(command: list[str], cwd: Path | None = None) -> str | None: """ Run a git command and return the output :param command: The git command + :param cwd: The directory in which to run the command :return: The output of the command """ try: - return subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip() + return subprocess.check_output(command, cwd=cwd, stderr=subprocess.DEVNULL).decode().strip() except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.debug(e) return None -def get_git_commit() -> str: +def get_git_commit(cwd: Path | None = None) -> str: """ Get the git commit hash + :param cwd: The directory of the git repository :return: The git commit hash """ - return run_git_command(["git", "rev-parse", "--short", "HEAD"]) or "" + return run_git_command(["git", "rev-parse", "--short", "HEAD"], cwd=cwd) or "" -def is_release_tag(git_sha: str) -> bool: +def is_release_tag(git_sha: str, cwd: Path | None = None) -> bool: """ Check whether a git sha corresponds to a release tag :param git_sha: The git sha + :param cwd: The directory of the git repository :return: True if the sha corresponds to a release tag, False otherwise """ - tags = run_git_command(["git", "tag", "--points-at", git_sha]) + tags = run_git_command(["git", "tag", "--points-at", git_sha], cwd=cwd) return bool(tags) -def get_last_tag() -> str: +def get_last_tag(cwd: Path | None = None) -> str: """ Get the last tag in the git repository + :param cwd: The directory of the git repository :return: The last tag """ - return run_git_command(["git", "describe", "--tags", "--abbrev=0"]) or "" + return run_git_command(["git", "describe", "--tags", "--abbrev=0"], cwd=cwd) or "" -def get_commit_distance(tag: str | None = None) -> int: +def get_commit_distance(tag: str | None = None, cwd: Path | None = None) -> int: """ Get the distance in commits between the current commit and the last tag + :param tag: The tag from which to count + :param cwd: The directory of the git repository :return: The distance in commits """ if not tag: - tag = get_last_tag() + tag = get_last_tag(cwd=cwd) try: - count = run_git_command(["git", "rev-list", "--count", f"{tag}..HEAD"]) + count = run_git_command(["git", "rev-list", "--count", f"{tag}..HEAD"], cwd=cwd) return int(count) if count else 0 except Exception as e: if logger.isEnabledFor(logging.DEBUG): @@ -86,13 +95,23 @@ def get_commit_distance(tag: str | None = None) -> int: return 0 -def has_uncommitted_changes() -> bool: +def has_uncommitted_changes(cwd: Path | None = None) -> bool: """ Check whether there are any uncommitted changes in the repository + :param cwd: The directory of the git repository :return: True if there are uncommitted changes, False otherwise """ - return bool(run_git_command(["git", "status", "--porcelain"])) + return bool(run_git_command(["git", "status", "--porcelain"], cwd=cwd)) + + +def _is_source_checkout(package_root: Path) -> bool: + """Return whether the package files belong to their own Git checkout.""" + if not (package_root / ".git").exists(): + return False + + git_root = run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=package_root) + return bool(git_root) and Path(git_root).resolve() == package_root.resolve() def get_version() -> str: @@ -101,20 +120,31 @@ def get_version() -> str: :return: The version """ - version = None + package_root = get_config_path().parent + source_checkout = _is_source_checkout(package_root) + + if not source_checkout: + try: + return metadata.version(PACKAGE_NAME) + except metadata.PackageNotFoundError: + pass + config = get_config() declared_version = config["tool"]["poetry"]["version"] - commit_sha = get_git_commit() - is_release = is_release_tag(commit_sha) - latest_tag = get_last_tag() + if not source_checkout: + return declared_version + + commit_sha = get_git_commit(cwd=package_root) + is_release = is_release_tag(commit_sha, cwd=package_root) + latest_tag = get_last_tag(cwd=package_root) if is_release: if declared_version != latest_tag: logger.warning("The declared version %s is different from the last tag %s", declared_version, latest_tag) version = latest_tag else: - commit_distance = get_commit_distance(latest_tag) + commit_distance = get_commit_distance(latest_tag, cwd=package_root) version = f"{declared_version}_{commit_sha}+{commit_distance}" if commit_sha else declared_version - dirty = has_uncommitted_changes() + dirty = has_uncommitted_changes(cwd=package_root) return f"{version}-dirty" if dirty else version From 3bfe80a09514845772e2accd95a3449af58dd4bd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 4 Sep 2026 09:45:55 +0200 Subject: [PATCH 016/271] =?UTF-8?q?test(versioning):=20=E2=9C=85=20cover?= =?UTF-8?q?=20version=20resolution=20outside=20source=20checkout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_versioning.py | 125 ++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tests/unit/test_versioning.py diff --git a/tests/unit/test_versioning.py b/tests/unit/test_versioning.py new file mode 100644 index 000000000..017c2a12e --- /dev/null +++ b/tests/unit/test_versioning.py @@ -0,0 +1,125 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import shutil +import subprocess +import sys +from pathlib import Path +from unittest.mock import Mock + +import pytest + +from rocrate_validator.utils import versioning + +PACKAGE_VERSION = versioning.get_config()["tool"]["poetry"]["version"] + + +def _version_config(version: str = PACKAGE_VERSION) -> dict: + return {"tool": {"poetry": {"version": version}}} + + +def _run_git(cwd: Path, *args: str) -> None: + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True) + + +def test_get_version_uses_distribution_metadata_outside_source_checkout(monkeypatch, tmp_path: Path): + package_root = tmp_path / "site-packages" + package_root.mkdir() + monkeypatch.setattr(versioning, "get_config_path", lambda: package_root / "pyproject.toml") + monkeypatch.setattr(versioning, "_is_source_checkout", lambda _: False) + monkeypatch.setattr(versioning.metadata, "version", lambda name: PACKAGE_VERSION) + run_git_command = Mock(side_effect=AssertionError("Git must not be queried for an installed package")) + get_config = Mock(side_effect=AssertionError("pyproject.toml is not needed when metadata is available")) + monkeypatch.setattr(versioning, "run_git_command", run_git_command) + monkeypatch.setattr(versioning, "get_config", get_config) + + assert versioning.get_version() == PACKAGE_VERSION + run_git_command.assert_not_called() + get_config.assert_not_called() + + +def test_get_version_falls_back_to_declared_version_when_metadata_is_missing(monkeypatch, tmp_path: Path): + package_root = tmp_path / "source-distribution" + package_root.mkdir() + monkeypatch.setattr(versioning, "get_config_path", lambda: package_root / "pyproject.toml") + monkeypatch.setattr(versioning, "_is_source_checkout", lambda _: False) + + def missing_distribution(name: str) -> str: + raise versioning.metadata.PackageNotFoundError(name) + + monkeypatch.setattr(versioning.metadata, "version", missing_distribution) + monkeypatch.setattr(versioning, "get_config", _version_config) + + assert versioning.get_version() == PACKAGE_VERSION + + +def test_get_version_anchors_source_checkout_git_commands(monkeypatch, tmp_path: Path): + package_root = tmp_path / "rocrate-validator" + package_root.mkdir() + monkeypatch.setattr(versioning, "get_config_path", lambda: package_root / "pyproject.toml") + monkeypatch.setattr(versioning, "_is_source_checkout", lambda _: True) + monkeypatch.setattr(versioning.metadata, "version", lambda _: pytest.fail("Source checkouts must use Git")) + monkeypatch.setattr(versioning, "get_config", _version_config) + + outputs = iter(["abc1234", "", PACKAGE_VERSION, "7", ""]) + calls: list[tuple[list[str], Path | None]] = [] + + def run_git_command(command: list[str], cwd: Path | None = None) -> str: + calls.append((command, cwd)) + return next(outputs) + + monkeypatch.setattr(versioning, "run_git_command", run_git_command) + + assert versioning.get_version() == f"{PACKAGE_VERSION}_abc1234+7" + assert len(calls) == 5 + assert all(cwd == package_root for _, cwd in calls) + + +@pytest.mark.skipif(shutil.which("git") is None, reason="Git is required for the consumer-repository regression") +def test_source_import_ignores_consumer_repository_tag(tmp_path: Path): + consumer = tmp_path / "consumer" + consumer.mkdir() + _run_git(consumer, "init", "--quiet") + _run_git( + consumer, + "-c", + "user.name=Issue 198 Test", + "-c", + "user.email=issue-198@example.invalid", + "-c", + "commit.gpgSign=false", + "commit", + "--allow-empty", + "--quiet", + "-m", + "consumer release", + ) + _run_git(consumer, "tag", "0.3.1") + + environment = os.environ.copy() + environment["PYTHONPATH"] = str(Path(__file__).resolve().parents[2]) + result = subprocess.run( + [sys.executable, "-c", "import rocrate_validator; print(rocrate_validator.__version__)"], + cwd=consumer, + env=environment, + check=False, + capture_output=True, + text=True, + ) + + output = result.stdout + result.stderr + assert result.returncode == 0, output + assert "0.3.1" not in output + assert "different from the last tag" not in output From bb4d0317ef28c91470b9da46f5048d5d940f49aa Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 1 Sep 2026 15:50:29 +0200 Subject: [PATCH 017/271] =?UTF-8?q?perf(profiles):=20=E2=9A=A1=EF=B8=8F=20?= =?UTF-8?q?replace=20SPARQL=20targets=20with=20SHACL=20Core=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `sh:targetClass` and `sh:targetObjectsOf` where the selection can be expressed directly, removing expensive SPARQL queries. Drop the redundant root-computation subqueries from workflow and script shapes. --- .../ro-crate/1.1/must/6_contextual_entity.ttl | 11 +----- .../1.2/must/2_root_data_entity_haspart.ttl | 11 +----- .../ro-crate/1.2/must/8_workflow_metadata.ttl | 37 ++----------------- .../should/6_contextual_entity_metadata.ttl | 11 +----- .../1.2/should/8_workflow_metadata.ttl | 6 +-- 5 files changed, 7 insertions(+), 69 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl index 9ecf87a9d..7191408a0 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl @@ -25,16 +25,7 @@ ro-crate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; sh:name "Identify License Entity" ; sh:description """Mark a license entity any Data Entity referenced by the `schema:license` property.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?subject schema:license ?this . - } - """ - ] ; + sh:targetObjectsOf schema:license ; # Expand data graph with triples from the file data entity sh:rule [ diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl index 95bffb33a..44adba7a4 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl @@ -19,16 +19,7 @@ ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; sh:description """The Root Data Entity MUST directly or indirectly reference all Data Entities in the RO-Crate via hasPart (RO-Crate 1.2).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ro-crate:RootDataEntity . - } - """ - ] ; + sh:targetClass ro-crate:RootDataEntity ; sh:sparql [ a sh:SPARQLConstraint ; sh:prefixes ro-crate:sparqlPrefixes ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl index 9f771b58e..0c0741dcd 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl @@ -20,25 +20,11 @@ @prefix xsd: . @prefix validator: . -# Selects every SoftwareSourceCode entity (Scripts and Workflows). -ro-crate:SoftwareSourceCodeTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?this a schema:SoftwareSourceCode . - } - """ . - # --------------------------------------------------------------- # Identify Script entities # # A Script is an entity with schema:SoftwareSourceCode in @type -# that is NOT a Workflow (no bioschemas:ComputationalWorkflow). +# that is NOT a Workflow (`no bioschemas:ComputationalWorkflow). # --------------------------------------------------------------- ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Script entities" ; @@ -50,10 +36,6 @@ ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; sh:select """ SELECT ?this WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } ?this a schema:SoftwareSourceCode . FILTER NOT EXISTS { ?this a bioschemas:ComputationalWorkflow } } @@ -76,20 +58,7 @@ ro-crate:WorkflowDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Workflow entities" ; sh:description """Mark entities as Workflows if they have bioschemas:ComputationalWorkflow in @type.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?this a bioschemas:ComputationalWorkflow . - } - """ - ] ; + sh:targetClass bioschemas:ComputationalWorkflow ; sh:rule [ a sh:TripleRule ; sh:subject sh:this ; @@ -167,7 +136,7 @@ ro-crate:ScriptOrWorkflowRequiredName a sh:NodeShape ; sh:name "Script or Workflow: REQUIRED `name`" ; sh:description """Scripts and Workflows MUST have a human-readable `name` property (RO-Crate 1.2, Workflows and Scripts).""" ; - sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:targetClass schema:SoftwareSourceCode ; sh:property [ a sh:PropertyShape ; sh:name "Script or Workflow: REQUIRED `name` property" ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl index 3103fffc3..387d9fa19 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl @@ -162,16 +162,7 @@ ro-crate:PersonAffiliationRecommendedProperties a sh:NodeShape ; ro-crate:LicenseEntityRecommendedProperties a sh:NodeShape ; sh:name "License entity: RECOMMENDED properties" ; sh:description """License entities SHOULD have `name` and `description`.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?subject schema:license ?this . - } - """ - ] ; + sh:targetObjectsOf schema:license ; sh:property [ sh:path schema:name ; sh:minCount 1 ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl index bb87d0583..f7bc6a397 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl @@ -29,10 +29,6 @@ ro-crate:ScriptOrWorkflowImageTarget a sh:SPARQLTarget ; WHERE { ?sw a schema:SoftwareSourceCode . ?sw schema:image ?this . - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } } """ . @@ -45,7 +41,7 @@ ro-crate:ScriptOrWorkflowProgrammingLanguage a sh:NodeShape ; sh:description """Scripts and Workflows SHOULD have a `programmingLanguage` property referencing a contextual entity of type `ComputerLanguage` (RO-Crate 1.2, Workflows and Scripts).""" ; - sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:targetClass schema:SoftwareSourceCode ; sh:property [ a sh:PropertyShape ; sh:name "Script or Workflow: RECOMMENDED `programmingLanguage` property" ; From 14058e67fd96043ed5298c91c6396293e0a1d484 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 1 Sep 2026 20:01:05 +0200 Subject: [PATCH 018/271] =?UTF-8?q?docs(profiles):=20=F0=9F=93=9D=20add=20?= =?UTF-8?q?"Optimizing=20SHACL=20target=20selection"=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/0_toc.rst | 1 + docs/11_writing_a_profile.rst | 3 + docs/13_optimizing_shacl_target_selection.rst | 128 ++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 docs/13_optimizing_shacl_target_selection.rst diff --git a/docs/0_toc.rst b/docs/0_toc.rst index a1f299bb7..f827b61d1 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -29,6 +29,7 @@ 12_validation_profiles 11_writing_a_profile + 13_optimizing_shacl_target_selection 10_api genindex diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 86bdc8d63..3a226eb43 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -28,6 +28,9 @@ For complex validation, you may also need some knowledge of SPARQL, an RDF query language. You can learn about SPARQL in the tutorial `Using SPARQL to access Linked Open Data `_. +For guidance on SHACL Core targets, SPARQL target performance, inference and +rule ordering, see :ref:`efficient-shacl-targets`. + All these tools are best learned through practice and examples, so when building a profile, it's encouraged to use the `other profiles `_ diff --git a/docs/13_optimizing_shacl_target_selection.rst b/docs/13_optimizing_shacl_target_selection.rst new file mode 100644 index 000000000..8adb5fbc6 --- /dev/null +++ b/docs/13_optimizing_shacl_target_selection.rst @@ -0,0 +1,128 @@ +.. + Copyright (c) 2024-2026 CRS4 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +.. _efficient-shacl-targets: + +Optimizing SHACL target selection +================================= + +During rule expansion, pySHACL re-evaluates the targets of shapes that carry +rules as triples are added to the data graph (see the `SHACL-AF spec`_ for +details). Targets of validation-only shapes are evaluated afterwards, during +validation. A SPARQL target on a rule-bearing shape that is inexpensive on a +small test crate can therefore dominate runtime on a graph with thousands of +entities. + +.. _SHACL-AF spec: https://www.w3.org/TR/shacl-af/ + +Prefer SHACL Core targets such as ``sh:targetClass``, ``sh:targetNode``, +``sh:targetSubjectsOf`` and ``sh:targetObjectsOf`` when they express the +selection directly. An implicit class target is also available when a shape is +itself declared as an ``rdfs:Class``. Use ``sh:SPARQLTarget`` only when focus +nodes depend on a relationship or exclusion that Core targets cannot express. +Benchmark the complete profile either way: inference and iterative rules can +make a Core target process more focus nodes than an equivalent explicit query. + +Rules can classify entities for other shapes without a SPARQL target. All rules +are executed before validation begins, so a shape that merely validates the +inferred class needs no ``sh:order``: ordering matters only when one rule +depends on the output of another. Use ``sh:condition`` to restrict which +focus nodes a rule applies to. A validation-only consumer can then use +``sh:targetClass`` after rule expansion instead of paying for a SPARQL target +during the iterative rule phase. If the consuming shape also carries rules, +its target is still re-evaluated during expansion. + +Both shapes and rules have a default ``sh:order`` of zero, and rules sharing the +same order run in an unspecified sequence, so never rely on declaration order. +When a rule on shape A produces a type that shape B targets, assign shape A a +lower order than shape B so the type is inferred before B is evaluated. The +same principle applies to rules on the same shape: the producing rule must +precede the consuming one. Producers need a lower order than their consumers: + +.. code-block:: turtle + + ex:FindScripts a sh:NodeShape ; + sh:targetClass schema:SoftwareSourceCode ; + sh:order -1 ; + sh:rule [ + a sh:TripleRule ; + sh:condition [ + a sh:NodeShape ; + sh:not [ sh:class bioschemas:ComputationalWorkflow ] + ] ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ex:Script ; + ] . + +.. note:: + + The SPARQL snippets in this section omit prefix boilerplate. A complete + ``sh:SPARQLTarget`` must make prefixes such as ``rdf:``, ``rdfs:``, + ``schema:`` and ``ro:`` available either through inline ``PREFIX`` + declarations or through ``sh:prefixes`` and ``sh:declare``. The bundled + profiles use the latter form. + +.. warning:: + + A ``sh:condition`` built on ``sh:not`` makes the rule non-monotonic. If + ``bioschemas:ComputationalWorkflow`` is itself inferred by another rule, the + rule above runs first and wrongly classifies the workflow as a script. + Iterative execution does not repair this, because an inferred triple is never + retracted. Whenever a condition is negative, the rule producing the negated + type must have a lower order than the rule testing for its absence. + +When Core targets cannot express the selection - for example, when focus nodes +depend on a relationship that ``sh:targetObjectsOf`` cannot capture, or when +an exclusion filter is required - a ``sh:SPARQLTarget`` is justified. In that +case, pre-compute expensive subpatterns in a subquery to avoid large +intermediate cross-products. For instance, when a target needs the Root Data +Entity, compute the small set of roots before joining it to entity patterns: + +.. code-block:: sparql + + SELECT ?this + WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(STRENDS(STR(?metadatafile), "ro-crate-metadata.json")) + } } + ?this a schema:MediaObject . + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + } + +.. note:: + + Without the distinct-root subquery, the target would have the following + form, with the entity and descriptor patterns together in the outer query: + + .. code-block:: sparql + + SELECT ?this + WHERE { + ?this a schema:MediaObject . + ?metadatafile schema:about ?root . + FILTER(STRENDS(STR(?metadatafile), "ro-crate-metadata.json")) + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + } + + For example, 500 ``schema:MediaObject`` entities and 250 entities with a + ``schema:about`` property, only one of which is the metadata descriptor, + produce 500 x 250, or 125,000, intermediate combinations before the filter + is applied. By contrast, the optimized query filters and deduplicates the + roots first, so the outer query processes only 500 combinations. On a + synthetic 750-triple graph using RDFLib 7.6.0, a local benchmark reduced + runtime from 13.82-14.58 seconds to 0.08 seconds + (timings are machine-dependent). From d71d43b7edbb6edb8bc1625939929029b4a2962f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 1 Sep 2026 22:02:22 +0200 Subject: [PATCH 019/271] =?UTF-8?q?test(profiles):=20=E2=9C=85=20add=20reg?= =?UTF-8?q?ression=20tests=20for=20optimized=20SPARQL=20target=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_sparql_target_performance.py | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/integration/profiles/ro-crate/test_sparql_target_performance.py diff --git a/tests/integration/profiles/ro-crate/test_sparql_target_performance.py b/tests/integration/profiles/ro-crate/test_sparql_target_performance.py new file mode 100644 index 000000000..6e082b56f --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_sparql_target_performance.py @@ -0,0 +1,156 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from time import perf_counter +from typing import TYPE_CHECKING, cast + +from rdflib import Graph, Namespace, URIRef +from rdflib.namespace import RDF, SH + +if TYPE_CHECKING: + from rdflib.query import ResultRow + +SCHEMA = Namespace("http://schema.org/") +DCT = Namespace("http://purl.org/dc/terms/") +RO_CRATE_1_1 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate/") +RO_CRATE_1_2 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/") +PROFILE_1_1 = ( + Path(__file__).resolve().parents[4] + / "rocrate_validator" + / "profiles" + / "ro-crate" + / "1.1" + / "must" + / "4_data_entity_metadata.ttl" +) +PROFILE_1_2_SHOULD = ( + Path(__file__).resolve().parents[4] + / "rocrate_validator" + / "profiles" + / "ro-crate" + / "1.2" + / "should" + / "4_referenced_rocrate.ttl" +) + + +def _build_root_subquery_data_graph(entity_type, entity_count, about_count=249): + """Build a data graph with entities and a root subquery pattern.""" + data_graph = Graph() + root = URIRef("./") + expected = set() + for index in range(entity_count): + entity = URIRef(f"entity-{index}.dat") + expected.add(entity) + data_graph.add((entity, RDF.type, entity_type)) + data_graph.add((URIRef("ro-crate-metadata.json"), SCHEMA.about, root)) + for index in range(about_count): + about_entity = URIRef(f"about/entity-{index}") + data_graph.add((about_entity, SCHEMA.about, root)) + return data_graph, expected, root + + +def test_file_data_entity_target_scales_without_cross_product(): + """The production target must pre-compute roots before joining entities.""" + shapes_graph = Graph().parse(PROFILE_1_1, format="turtle") + target = shapes_graph.value(RO_CRATE_1_1.FileDataEntity, SH.target) + query = str(shapes_graph.value(target, SH.select)) + + # 500 entities + 250 about-entities + 1 descriptor = 501 triples. + # Without the root subquery, rdflib would build a cross-product of + # 500 × 250 = 125,000 intermediate rows before applying FILTERs. + # With the subquery, the root is pre-computed to a single binding, + # so the outer pattern processes only 500 combinations. + data_graph, expected, _ = _build_root_subquery_data_graph(SCHEMA.MediaObject, 500) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"FileDataEntity target took {duration:.2f}s; possible cross-product regression" + + +def test_directory_data_entity_target_scales_without_cross_product(): + """DirectoryDataEntity target must pre-compute roots before joining entities.""" + shapes_graph = Graph().parse(PROFILE_1_1, format="turtle") + target = shapes_graph.value(RO_CRATE_1_1.DirectoryDataEntity, SH.target) + query = str(shapes_graph.value(target, SH.select)) + + # Same structure as FileDataEntity: 500 Dataset entities + 250 about-entities. + # The root is excluded from results (FILTER ?this != ?root). + data_graph, expected, root = _build_root_subquery_data_graph(SCHEMA.Dataset, 500) + expected.discard(root) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"DirectoryDataEntity target took {duration:.2f}s; possible cross-product regression" + + +def test_referenced_rocrate_data_entity_target_scales_without_cross_product(): + """ReferencedROCrateDataEntityTarget must pre-compute roots before joining.""" + shapes_graph = Graph().parse(PROFILE_1_2_SHOULD, format="turtle") + query = str(shapes_graph.value(RO_CRATE_1_2.ReferencedROCrateDataEntityTarget, SH.select)) + + # 500 Dataset entities with dct:conformsTo + 250 about-entities + 1 descriptor. + # The target also filters by profile URI (STRSTARTS). + data_graph, expected, root = _build_root_subquery_data_graph(SCHEMA.Dataset, 500) + expected.discard(root) + profile_uri = URIRef("https://w3id.org/ro/crate/1.1") + for entity in expected: + data_graph.add((entity, DCT.conformsTo, profile_uri)) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA, "dct": DCT}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"ReferencedROCrateDataEntity target took {duration:.2f}s; possible cross-product regression" + + +def test_referenced_rocrate_metadata_descriptor_target_scales_without_cross_product(): + """ReferencedROCrateMetadataDescriptorTarget must pre-compute roots before joining.""" + shapes_graph = Graph().parse(PROFILE_1_2_SHOULD, format="turtle") + query = str(shapes_graph.value(RO_CRATE_1_2.ReferencedROCrateMetadataDescriptorTarget, SH.select)) + + # 1 crate with 500 subjectOf descriptors + 1 root descriptor. + # Without the root subquery, the cross-product would be 500 × 1 = 500 + # (small here, but scales with multiple crates in real graphs). + data_graph = Graph() + root = URIRef("./") + expected = set() + crate = URIRef("referenced-crate/") + profile_uri = URIRef("https://w3id.org/ro/crate/1.1") + data_graph.add((crate, RDF.type, SCHEMA.Dataset)) + data_graph.add((crate, DCT.conformsTo, profile_uri)) + for index in range(500): + descriptor = URIRef(f"descriptor-{index}") + expected.add(descriptor) + data_graph.add((crate, SCHEMA.subjectOf, descriptor)) + data_graph.add((URIRef("ro-crate-metadata.json"), SCHEMA.about, root)) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA, "dct": DCT}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"ReferencedROCrateMetadataDescriptor target took {duration:.2f}s; possible cross-product regression" From 928706c8941779ed970591f3b09574a57fa4d623 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 1 Sep 2026 22:40:11 +0200 Subject: [PATCH 020/271] =?UTF-8?q?chore(tests):=20=F0=9F=94=A7=20fix=20li?= =?UTF-8?q?nting=20warnings=20in=20SPARQL=20performance=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/ro-crate/test_sparql_target_performance.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration/profiles/ro-crate/test_sparql_target_performance.py b/tests/integration/profiles/ro-crate/test_sparql_target_performance.py index 6e082b56f..452b9a980 100644 --- a/tests/integration/profiles/ro-crate/test_sparql_target_performance.py +++ b/tests/integration/profiles/ro-crate/test_sparql_target_performance.py @@ -70,7 +70,7 @@ def test_file_data_entity_target_scales_without_cross_product(): # 500 entities + 250 about-entities + 1 descriptor = 501 triples. # Without the root subquery, rdflib would build a cross-product of - # 500 × 250 = 125,000 intermediate rows before applying FILTERs. + # 500 x 250 = 125,000 intermediate rows before applying FILTERs. # With the subquery, the root is pre-computed to a single binding, # so the outer pattern processes only 500 combinations. data_graph, expected, _ = _build_root_subquery_data_graph(SCHEMA.MediaObject, 500) @@ -132,7 +132,7 @@ def test_referenced_rocrate_metadata_descriptor_target_scales_without_cross_prod query = str(shapes_graph.value(RO_CRATE_1_2.ReferencedROCrateMetadataDescriptorTarget, SH.select)) # 1 crate with 500 subjectOf descriptors + 1 root descriptor. - # Without the root subquery, the cross-product would be 500 × 1 = 500 + # Without the root subquery, the cross-product would be 500 x 1 = 500 # (small here, but scales with multiple crates in real graphs). data_graph = Graph() root = URIRef("./") @@ -153,4 +153,6 @@ def test_referenced_rocrate_metadata_descriptor_target_scales_without_cross_prod duration = perf_counter() - started_at assert actual == expected - assert duration < 3.0, f"ReferencedROCrateMetadataDescriptor target took {duration:.2f}s; possible cross-product regression" + assert duration < 3.0, ( + f"ReferencedROCrateMetadataDescriptor target took {duration:.2f}s; possible cross-product regression" + ) From acb715bfc9c5ae3f27f399aae8e429660a311ebe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 7 Sep 2026 15:49:16 +0200 Subject: [PATCH 021/271] =?UTF-8?q?fix(profile):=20=F0=9F=90=9B=20skip=20e?= =?UTF-8?q?ncoding=20check=20in=20metadata-only=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid looking for ro-crate-metadata.json in the CWD when validation uses an in-memory metadata dictionary. --- .../profiles/ro-crate/1.2/must/0_file_descriptor_format.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 5c230f089..4f7a7abd9 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -101,6 +101,9 @@ def check(self, context: ValidationContext) -> bool: """ Check if the file descriptor is UTF-8 encoded """ + if context.settings.metadata_only: + logger.debug("Skipping file descriptor encoding check in metadata-only mode") + return True try: raw_data = context.ro_crate.get_file_content( Path(context.ro_crate.metadata_descriptor_id), binary_mode=True From 9411c86c195b6aad8bdf1b4bd13738ec9995f580 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 7 Sep 2026 15:52:38 +0200 Subject: [PATCH 022/271] =?UTF-8?q?fix(profile):=20=F0=9F=9B=A1=EF=B8=8F?= =?UTF-8?q?=20narrow=20descriptor=20encoding=20exception=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ttreat only UnicodeDecodeError as an encoding failure; I/O errors are no longer incorrectly reported as UTF-8 problems. --- .../profiles/ro-crate/1.2/must/0_file_descriptor_format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 4f7a7abd9..089f9d86f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -112,10 +112,10 @@ def check(self, context: ValidationContext) -> bool: return True raw_data.decode("utf-8") return True - except Exception: + except UnicodeDecodeError: context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") + logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) return False From c99772a5652cbf5881a104c9d07f30f02a18a650 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 7 Sep 2026 15:55:30 +0200 Subject: [PATCH 023/271] =?UTF-8?q?test(profile):=20=E2=9C=85=20cover=20me?= =?UTF-8?q?tadata-only=20descriptor=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate-1.2/test_metadata_document.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 4fe28c60d..2411cf820 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging from rocrate_validator import models +from rocrate_validator.rocrate.plain import ROCrateLocalFolder from tests.ro_crates_v1_2 import MetadataDocument, MetadataDocumentFormat from tests.shared import do_entity_test @@ -39,6 +41,33 @@ def test_not_utf8(): ) +def test_utf8_check_is_skipped_for_metadata_dict(monkeypatch, tmp_path): + """Metadata-only validation must not read a file descriptor from the CWD.""" + crate_path = __metadata_document_crates__.valid_context_reference + with (crate_path / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata_dict = json.load(stream) + + descriptor_reads = [] + original_get_file_content = ROCrateLocalFolder.get_file_content + + def track_descriptor_reads(crate, path, binary_mode=True): + if path.name == "ro-crate-metadata.json": + descriptor_reads.append((path, binary_mode)) + return original_get_file_content(crate, path, binary_mode) + + monkeypatch.setattr(ROCrateLocalFolder, "get_file_content", track_descriptor_reads) + monkeypatch.chdir(tmp_path) + do_entity_test( + crate_path, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + metadata_dict=metadata_dict, + metadata_only=True, + ) + assert descriptor_reads == [] + + def test_not_json(): """ Test that the metadata document is valid when it is not JSON-LD. From e1217fda31b7e8c806202350f8c17150735813d5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Sep 2026 10:40:17 +0200 Subject: [PATCH 024/271] =?UTF-8?q?fix(metadata):=20=F0=9F=9B=A1=EF=B8=8F?= =?UTF-8?q?=20wrap=20missing=20descriptor=20`FileNotFoundError`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise ROCrateMetadataNotFoundError when the metadata descriptor file is not found, preserving the original FileNotFoundError as the cause. --- rocrate_validator/rocrate/metadata.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index 5e21a9879..8eccd526e 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -20,6 +20,7 @@ from rdflib import Graph +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.utils import log as logging from .entity import ROCrateEntity @@ -139,11 +140,19 @@ def get_conforms_to(self) -> list[str] | None: logger.exception("Error getting entity identifiers by type") return None + def __get_file_content__(self, binary_mode: bool) -> str | bytes: + """Read the metadata descriptor and identify a missing descriptor explicitly.""" + try: + return self.ro_crate.get_file_content(Path(self.ro_crate.metadata_descriptor_id), binary_mode=binary_mode) + except FileNotFoundError as e: + raise ROCrateMetadataNotFoundError( + message=str(e), + path=self.ro_crate.metadata_descriptor_id, + ) from e + def as_json(self) -> str: if not self._json: - self._json = cast( - "str", self.ro_crate.get_file_content(Path(self.ro_crate.metadata_descriptor_id), binary_mode=False) - ) + self._json = cast("str", self.__get_file_content__(binary_mode=False)) return self._json def as_dict(self) -> dict[Any, Any]: From e18e24ef224493dbfe6acb73a3e494b3351bcf85 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Sep 2026 10:52:09 +0200 Subject: [PATCH 025/271] refactor(metadata): :sparkles: add raw byte access for metadata encoding checks --- .../profiles/ro-crate/1.2/must/0_file_descriptor_format.py | 5 +---- rocrate_validator/rocrate/metadata.py | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 089f9d86f..4507e28f3 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -15,7 +15,6 @@ import json import re from http import HTTPStatus -from pathlib import Path from typing import Any from urllib.parse import urljoin @@ -105,9 +104,7 @@ def check(self, context: ValidationContext) -> bool: logger.debug("Skipping file descriptor encoding check in metadata-only mode") return True try: - raw_data = context.ro_crate.get_file_content( - Path(context.ro_crate.metadata_descriptor_id), binary_mode=True - ) + raw_data = context.ro_crate.metadata.as_bytes() if isinstance(raw_data, str): return True raw_data.decode("utf-8") diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index 8eccd526e..3c30f7f87 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -150,6 +150,12 @@ def __get_file_content__(self, binary_mode: bool) -> str | bytes: path=self.ro_crate.metadata_descriptor_id, ) from e + def as_bytes(self) -> bytes: + """Return the raw metadata descriptor content.""" + if self._dict is not None: + return self.as_json().encode("utf-8") + return cast("bytes", self.__get_file_content__(binary_mode=True)) + def as_json(self) -> str: if not self._json: self._json = cast("str", self.__get_file_content__(binary_mode=False)) From b302b5cdd1dd25678e5bbc3d1a723f2ce87e7c8a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Sep 2026 11:00:15 +0200 Subject: [PATCH 026/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20handle=20metadata=20descriptor=20errors=20in=20chec?= =?UTF-8?q?ks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle missing, encoding, and other metadata descriptor errors in individual checks instead of handling them in the global requirement runner --- rocrate_validator/models/requirement.py | 6 -- .../1.1/must/0_file_descriptor_format.py | 28 ++++++-- .../1.1/must/4_data_entity_metadata.py | 8 ++- .../should/2_root_data_entity_relative_uri.py | 4 ++ .../1.1/should/4_data_entity_existence.py | 16 +++-- .../1.1/should/5_web_data_entity_metadata.py | 15 +++- .../1.2/must/0_file_descriptor_format.py | 40 +++++++++-- .../ro-crate/1.2/must/1_ro_crate_preview.py | 7 +- .../1.2/must/2_root_data_entity_cite_as.py | 4 ++ .../1.2/must/2_root_data_entity_identifier.py | 4 ++ .../1.2/must/2_root_identifier_property.py | 4 ++ .../1.2/must/4_data_entity_metadata.py | 68 ++++++++++++++++--- .../1.2/should/0_entity_identifier_format.py | 22 +++++- .../should/2_root_data_entity_identifier.py | 16 ++++- .../1.2/should/4_data_entity_license.py | 10 ++- .../1.2/should/4_data_entity_metadata.py | 33 +++++++-- .../1.2/should/4_missing_file_local_path.py | 15 +++- .../1.2/should/4_referenced_rocrate.py | 8 ++- .../1.2/should/4_web_dataset_distribution.py | 8 ++- .../workflow-ro-crate/may/1_main_workflow.py | 7 ++ .../workflow-ro-crate/must/0_main_workflow.py | 4 ++ .../requirements/shacl/checks.py | 2 +- .../requirements/shacl/requirements.py | 7 ++ 23 files changed, 279 insertions(+), 57 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 7ef85296b..01b34e66c 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -15,7 +15,6 @@ from __future__ import annotations import importlib -import json from abc import ABC, abstractmethod from dataclasses import dataclass from functools import total_ordering @@ -226,11 +225,6 @@ def _do_validate_(self, context: ValidationContext) -> bool: logger.debug("Skipping check '%s' because: %s", check.name, e) context.result._add_skipped_check(check) continue - except json.JSONDecodeError as e: - # The file descriptor is not valid JSON, so this check could not run. - # This is a malformed-input problem (reported as an ad-hoc issue by the - # dedicated "File Descriptor JSON format" check), not a validator bug. - logger.debug("Skipping check %s: file descriptor is not valid JSON: %s", check, e) except Exception as e: if context.maybe_warn_offline_cache_miss(e): logger.debug("Offline cache miss during check %s: %s", check, e) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index bcc7a512f..909bbb480 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -20,6 +20,7 @@ from urllib.parse import urljoin from rocrate_validator.constants import HTTP_STATUS_OK +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -55,11 +56,7 @@ def test_size(self, context: ValidationContext) -> bool: if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") return True - if not context.ro_crate.has_descriptor(): - message = f"file descriptor {context.rel_fd_path} is empty" - context.result.add_issue(message, self) - return False - if context.ro_crate.metadata.size == 0: + if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -89,6 +86,9 @@ def check(self, context: ValidationContext) -> bool: # The metadata cannot be parsed: abort to avoid false positives downstream. context.abort_validation(f"file descriptor is not valid JSON: {e}") return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + return True except Exception: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self @@ -214,7 +214,8 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) @check(name="File Descriptor @context property validation") def check_context(self, context: ValidationContext) -> bool: - """Check if the file descriptor contains + """ + Check if the file descriptor contains the @context property and it is a valid JSON-LD context """ try: @@ -227,6 +228,9 @@ def check_context(self, context: ValidationContext) -> bool: # Check if the context is valid return self.__check_contexts__(context, json_dict["@context"]) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error extracting @context from file descriptor") @@ -320,6 +324,9 @@ def _check_flattened_graph(self, context, is_flat): if fail_fast: return False return result + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error flattening JSON-LD file descriptor") @@ -340,6 +347,9 @@ def check_identifiers(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @id property of file descriptor entities") @@ -360,6 +370,9 @@ def check_types(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @type property of file descriptor entities") @@ -481,6 +494,9 @@ def check_compaction(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") return True except Exception as e: if logger.isEnabledFor(logging.DEBUG): diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py index 7aea2a2c6..0ee74daf2 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py @@ -14,6 +14,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -42,7 +43,12 @@ def check_availability(self, context: ValidationContext) -> bool: # Web-based Data Entities (absolute URIs with any scheme other than `file`, # e.g. http://, https://, ftp://, scp://, s3://, ...) are not required to # be part of the local payload per the RO-Crate specification. - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) try: diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py index a67458c34..46c3e634b 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py @@ -14,6 +14,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -36,6 +37,9 @@ def check_relative_uris(self, context: ValidationContext) -> bool: context.result.add_issue("Root Data Entity URI is not denoted by the string `./`", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity URI check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity URI: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py index c770521c3..2a82d87c3 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py @@ -14,6 +14,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -41,11 +42,16 @@ def check_availability(self, context: ValidationContext) -> bool: return True # Perform the check result = True - for entity in [ - _ - for _ in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) - if _.has_absolute_path() - ]: + try: + entities = [ + _ + for _ in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + if _.has_absolute_path() + ] + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" try: if not entity.is_available(): diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py index eb821df23..1ddcb39ed 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py @@ -14,6 +14,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -42,7 +43,12 @@ def check_availability(self, context: ValidationContext) -> bool: and logged as warnings, without invalidating the validation. """ result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" try: status = entity.check_availability() @@ -81,7 +87,12 @@ def check_content_size(self, context: ValidationContext) -> bool: and if it is set to actual size of the downloadable content """ result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip entities whose scheme the validator cannot natively fetch # (e.g. scp://, s3://): without retrieving the content there is diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 4507e28f3..8de3f28a6 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -18,6 +18,7 @@ from typing import Any from urllib.parse import urljoin +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -79,11 +80,7 @@ def test_size(self, context: ValidationContext) -> bool: if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") return True - if not context.ro_crate.has_descriptor(): - message = f"file descriptor {context.rel_fd_path} is empty" - context.result.add_issue(message, self) - return False - if context.ro_crate.metadata.size == 0: + if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -114,6 +111,9 @@ def check(self, context: ValidationContext) -> bool: if logger.isEnabledFor(logging.DEBUG): logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") + return True @requirement(name="File Descriptor JSON format") @@ -140,6 +140,9 @@ def check(self, context: ValidationContext) -> bool: # The metadata cannot be parsed: abort to avoid false positives downstream. context.abort_validation(f"file descriptor is not valid JSON: {e}") return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + return True except Exception: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self @@ -250,7 +253,8 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) @check(name="File Descriptor @context property validation") def check_context(self, context: ValidationContext) -> bool: - """Check if the file descriptor contains + """ + Check if the file descriptor contains the @context property and it is a valid JSON-LD context """ try: @@ -281,6 +285,9 @@ def has_expected_context(ctx: object) -> bool: # Check if the context is valid return self.__check_contexts__(context, json_dict["@context"]) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -380,6 +387,9 @@ def check_flattened(self, context: ValidationContext) -> bool: if fail_fast: return False return result + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -400,6 +410,9 @@ def check_identifiers(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -420,6 +433,9 @@ def check_types(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -437,6 +453,9 @@ def check_unique_identifiers(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping unique identifier check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -483,6 +502,9 @@ def check_entity_references(self, context: ValidationContext) -> bool: context.result.add_issue(message, self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping entity reference check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -502,6 +524,9 @@ def check_subject_keywords(self, context: ValidationContext) -> bool: context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'keywords'", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping subject and keyword check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -618,6 +643,9 @@ def check_compaction(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") return True except Exception as e: if logger.isEnabledFor(logging.DEBUG): diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index ed1b2e281..1c7c0b79f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -14,6 +14,7 @@ from pathlib import Path +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,7 +31,11 @@ class ROCrateWebsiteChecker(PyFunctionCheck): @check(name="RO-Crate Website HTML5 doctype") def check_preview_html(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached(): + try: + if context.ro_crate.is_detached(): + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") return True preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py index a5558f926..7cccb1c04 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -81,6 +82,9 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> bool: ) return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py index 5ac8766c1..cfc84fa3d 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py @@ -14,6 +14,7 @@ import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -42,6 +43,9 @@ def check_identifier(self, context: ValidationContext) -> bool: self, ) return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py index 1c1f17b9f..ace24b4d0 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -43,6 +44,9 @@ def check_identifier_values(self, context: ValidationContext) -> bool: context.result.add_issue("PropertyValue identifiers MUST include a `value`", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 780ab17f1..0482c93aa 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -15,6 +15,7 @@ import contextlib import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -31,11 +32,16 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> bool: # noqa: C901 """ Check the presence of the Data Entity in the RO-Crate """ - if context.ro_crate.is_detached(): + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + return True + if is_detached: logger.debug("Skipping data entity payload checks for detached RO-Crate") return True # Skip the check in metadata-only mode @@ -44,7 +50,12 @@ def check_availability(self, context: ValidationContext) -> bool: return True # Perform the check result = True - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) try: @@ -94,13 +105,23 @@ class DetachedDataEntityChecker(PyFunctionCheck): @check(name="Detached RO-Crate: data entities MUST be web-based") def check_detached_entities(self, context: ValidationContext) -> bool: - if not context.ro_crate.is_detached(): + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + return True + if not is_detached: return True result = True root_entity_id = None with contextlib.suppress(Exception): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + return True + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not entity.is_remote(): @@ -123,7 +144,7 @@ class DataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> bool: + def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 result = True root_entity_id = None root_entity_is_local = False @@ -135,7 +156,12 @@ def check_identifiers(self, context: ValidationContext) -> bool: root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False ) root_entity_absolute_path = root_data_entity.id_as_path if root_data_entity.has_absolute_path() else None - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") + return True + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not root_entity_is_local and not entity.is_remote(): @@ -172,10 +198,20 @@ def check_identifiers(self, context: ValidationContext) -> bool: @check(name="Data Entity: relative @id for payload files") def check_relative_paths(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached(): + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + return True + if is_detached: return True result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + return True + for entity in entities: if entity.has_local_identifier() or entity.is_remote(): continue if entity.has_absolute_path() and ( @@ -199,7 +235,12 @@ class DataEntityCitationChecker(PyFunctionCheck): @check(name="Data Entity: citation must include @id") def check_citation(self, context: ValidationContext) -> bool: result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + return True + for entity in entities: citations = entity.get_property("citation") if citations is None: continue @@ -256,7 +297,12 @@ def check_availability(self, context: ValidationContext) -> bool: ): return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip directory URIs: assumed available, not directly downloadable by spec if entity.id.endswith("/"): diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py index 6f9d8745b..6b79215c4 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py @@ -22,6 +22,7 @@ import contextlib import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -52,7 +53,12 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: (RO-Crate 1.2, JSON-LD appendix). """ result = True - for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + try: + entities = context.ro_crate.metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping parent traversal check: metadata descriptor is not available") + return True + for entity in entities: entity_id = entity.get("@id", "") if "../" in entity_id: context.result.add_issue( @@ -75,7 +81,12 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: UTF-8 rather than percent-encoded (RO-Crate 1.2, JSON-LD appendix). """ result = True - for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + try: + entities = context.ro_crate.metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") + return True + for entity in entities: entity_id = entity.get("@id", "") if _PCT_NON_ASCII_RE.search(entity_id): context.result.add_issue( @@ -110,7 +121,12 @@ def check_named_entity_id_format(self, context: ValidationContext) -> bool: with contextlib.suppress(Exception): non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) - for entity in ro_crate_metadata.as_dict().get("@graph", []): + try: + entities = ro_crate_metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping named entity identifier check: metadata descriptor is not available") + return True + for entity in entities: entity_id = entity.get("@id", "") if entity_id in non_contextual_ids: continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py index c957b475a..2b62c28d3 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,7 +29,7 @@ class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED identifier") - def check_identifier(self, context: ValidationContext) -> bool: + def check_identifier(self, context: ValidationContext) -> bool: # noqa: PLR0911 """ In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. """ @@ -50,6 +51,9 @@ def check_identifier(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) @@ -64,7 +68,7 @@ class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: use cite-as for resolvable identifiers") - def check_cite_as_reference(self, context: ValidationContext) -> bool: + def check_cite_as_reference(self, context: ValidationContext) -> bool: # noqa: PLR0911 """ If the Root Data Entity has a resolvable identifier, it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. @@ -95,6 +99,9 @@ def check_cite_as_reference(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) @@ -140,7 +147,7 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) - def check_identifier_resolvable(self, context: ValidationContext) -> bool: + def check_identifier_resolvable(self, context: ValidationContext) -> bool: # noqa: PLR0911 if context.settings.skip_availability_check: return True if context.settings.metadata_only: @@ -169,6 +176,9 @@ def check_identifier_resolvable(self, context: ValidationContext) -> bool: context.result.add_issue(msg, self) result = False return result + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py index 2ab81a7bc..e602a91b3 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py @@ -46,6 +46,7 @@ # check provides actionable feedback to authors without penalising # valid crates. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,7 +101,7 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> bool: + def check_license_divergence(self, context: ValidationContext) -> bool: # noqa: C901 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -117,7 +118,12 @@ def check_license_divergence(self, context: ValidationContext) -> bool: if not root_license_id: return True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + return True + for entity in entities: if entity.id == root_entity.id: continue entity_license_raw = entity.get_property("license") diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py index 713cf47fe..71f5a22d7 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py @@ -14,6 +14,7 @@ import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -32,7 +33,12 @@ class DataEntityCitationChecker(PyFunctionCheck): @check(name="Data Entity: citation must include @id") def check_citation(self, context: ValidationContext) -> bool: result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + return True + for entity in entities: citations = entity.get_property("citation") if citations is None: continue @@ -90,7 +96,12 @@ def check_availability_warning(self, context: ValidationContext) -> bool: ): return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.id.endswith("/"): continue @@ -107,11 +118,16 @@ def check_availability_warning(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) - def check_content_size(self, context: ValidationContext) -> bool: + def check_content_size(self, context: ValidationContext) -> bool: # noqa: C901 if context.settings.skip_availability_check: return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.is_available(): content_size = entity.get_property("contentSize") @@ -143,11 +159,16 @@ def check_content_size(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) - def check_content_url(self, context: ValidationContext) -> bool: + def check_content_url(self, context: ValidationContext) -> bool: # noqa: C901 if context.settings.skip_availability_check: return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") + return True + for entity in entities: content_url = entity.get_property("contentUrl") if not content_url: continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py index 08bbe75ce..e28156930 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py @@ -32,6 +32,7 @@ import contextlib +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,13 +101,23 @@ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) def check_missing_file_local_path(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached() or context.settings.metadata_only: + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping missing local file check: metadata descriptor is not available") + return True + if is_detached or context.settings.metadata_only: return True root_entity_id = None with contextlib.suppress(Exception): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id result = True - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping missing local file check: metadata descriptor is not available") + return True + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not entity.is_file(): diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py index 377d0fe0f..8a05b4ce4 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py @@ -23,6 +23,7 @@ This Python check performs the network-dependent refinement. """ +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -81,7 +82,12 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: except Exception: return True - for entity in context.ro_crate.metadata.get_dataset_entities(): + try: + entities = context.ro_crate.metadata.get_dataset_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + return True + for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): continue entity_id = entity.id diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py index 810b1646c..d3e3ac09f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -83,7 +84,12 @@ def check_distribution_downloadable(self, context: ValidationContext) -> bool: if context.settings.skip_availability_check or context.settings.metadata_only: return True result = True - for entity in context.ro_crate.metadata.get_dataset_entities(): + try: + entities = context.ro_crate.metadata.get_dataset_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") + return True + for entity in entities: distribution_raw = entity.get_property("distribution") if not distribution_raw: continue diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py index e88e19837..ce2970879 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py @@ -14,6 +14,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -44,6 +45,9 @@ def check_workflow_diagram(self, context: ValidationContext) -> bool: context.result.add_issue(f"Workflow diagram '{image.id}' not found in crate", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow diagram check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking main workflow image existence") @@ -65,6 +69,9 @@ def check_workflow_description(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow description check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking workflow description existence") diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py index 4e6beaf33..8d13b1acd 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py @@ -14,6 +14,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -38,6 +39,9 @@ def check_workflow(self, context: ValidationContext) -> bool: context.result.add_issue(f"Main Workflow {main_workflow.id} not found in crate", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping main workflow check: metadata descriptor is not available") + return True except ValueError: context.result.add_issue( "Unable to check the existence of the main workflow file " diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index de5475a6c..fe7734225 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -277,7 +277,7 @@ def execute_check(self, context: ValidationContext): "Unable to perform metadata validation due to missing metadata file: %s", e, ) - return False + return True def __do_execute_check__(self, shacl_context: SHACLValidationContext): # get the shapes registry diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index f2ff5ce10..72e0d8cb1 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -18,6 +18,7 @@ from rdflib import RDF from rocrate_validator.constants import VALIDATOR_NS +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ( Profile, Requirement, @@ -139,6 +140,12 @@ def finalize(cls, context: ValidationContext) -> None: shacl_context._current_validation_profile = target try: runner.__do_execute_check__(shacl_context) + except ROCrateMetadataNotFoundError as e: + logger.debug( + "Forced SHACL run for zero-shape target profile %s skipped: metadata descriptor is not available (%s)", + target.identifier, + e, + ) except Exception as e: if context.maybe_warn_offline_cache_miss(e): logger.debug( From 5a8fed3acfa7b6e33422a0bea8b8d76926360730 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Sep 2026 11:10:28 +0200 Subject: [PATCH 027/271] =?UTF-8?q?test:=20=E2=9C=85=20cover=20missing=20d?= =?UTF-8?q?escriptor=20FileNotFoundError=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify that missing metadata descriptor files use the metadata-specific exception and do not produce unexpected global warnings. --- .../ro-crate-1.2/test_metadata_document.py | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 2411cf820..403b2a4bb 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -14,9 +14,16 @@ import json import logging +from unittest.mock import MagicMock -from rocrate_validator import models +import pytest + +from rocrate_validator import models, services +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import requirement as requirement_module +from rocrate_validator.requirements.shacl import requirements as shacl_requirements_module from rocrate_validator.rocrate.plain import ROCrateLocalFolder +from rocrate_validator.utils.uri import URI from tests.ro_crates_v1_2 import MetadataDocument, MetadataDocumentFormat from tests.shared import do_entity_test @@ -68,6 +75,43 @@ def track_descriptor_reads(crate, path, binary_mode=True): assert descriptor_reads == [] +def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_path): + """Checks depending on metadata should defer to the descriptor checks.""" + requirement_logger = MagicMock() + shacl_logger = MagicMock() + monkeypatch.setattr(requirement_module, "logger", requirement_logger) + monkeypatch.setattr(shacl_requirements_module, "logger", shacl_logger) + + result = services.validate( + models.ValidationSettings( + rocrate_uri=URI(tmp_path), + profile_identifier="ro-crate-1.2", + requirement_severity=models.Severity.OPTIONAL, + ) + ) + + issues = [issue.message for issue in result.get_issues()] + assert result.passed() is False + assert any('file descriptor "ro-crate-metadata.json" is not present' in issue for issue in issues) + assert not any( + any(message in issue for message in ("Unexpected error", "Error checking", "not in the correct format")) + for issue in issues + ) + assert requirement_logger.warning.call_count == 0 + assert shacl_logger.warning.call_count == 0 + + +def test_missing_descriptor_raises_specific_metadata_error(tmp_path): + """Reading a missing descriptor must preserve its metadata-specific cause.""" + crate = ROCrateLocalFolder(tmp_path) + + with pytest.raises(ROCrateMetadataNotFoundError) as error: + crate.metadata.as_dict() + + assert error.value.path == "ro-crate-metadata.json" + assert isinstance(error.value.__cause__, FileNotFoundError) + + def test_not_json(): """ Test that the metadata document is valid when it is not JSON-LD. From 80bb62684e2c12077dcf110bbc5de9aa2be22406 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Sep 2026 11:14:42 +0200 Subject: [PATCH 028/271] fix(tests): :rotating_light: fix linter warnings --- .../ro-crate-1.2/test_metadata_document.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 403b2a4bb..d6eaff044 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -92,9 +92,12 @@ def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_p issues = [issue.message for issue in result.get_issues()] assert result.passed() is False - assert any('file descriptor "ro-crate-metadata.json" is not present' in issue for issue in issues) + assert any( + issue is not None and 'file descriptor "ro-crate-metadata.json" is not present' in issue for issue in issues + ) assert not any( - any(message in issue for message in ("Unexpected error", "Error checking", "not in the correct format")) + issue is not None + and any(message in issue for message in ("Unexpected error", "Error checking", "not in the correct format")) for issue in issues ) assert requirement_logger.warning.call_count == 0 @@ -167,8 +170,10 @@ def test_invalid_context_reference(): profile_identifier="ro-crate-1.2", expected_triggered_requirements=["File Descriptor JSON-LD format"], expected_triggered_issues=[ - 'RO-Crate file descriptor "ro-crate-metadata.json" ' - 'does not reference the required context "https://w3id.org/ro/crate/1.2/context"' + ( + 'RO-Crate file descriptor "ro-crate-metadata.json" ' + 'does not reference the required context "https://w3id.org/ro/crate/1.2/context"' + ) ], ) @@ -238,8 +243,10 @@ def test_not_described_contextual_entity(): profile_identifier="ro-crate-1.2", expected_triggered_requirements=["Contextual Entity RECOMMENDED description"], expected_triggered_issues=[ - "Contextual entities that are referenced by other entities SHOULD be " - "described in the same @graph, with at least an RDF type specified." + ( + "Contextual entities that are referenced by other entities SHOULD be " + "described in the same @graph, with at least an RDF type specified." + ) ], ) From 658b5963dab3d3c91b766a62f612b86adb47df48 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 8 Sep 2026 13:25:00 +0200 Subject: [PATCH 029/271] =?UTF-8?q?test(profiles):=20=E2=9C=85=20verify=20?= =?UTF-8?q?profile=20namespace=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/test_profile_namespaces.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/unit/requirements/test_profile_namespaces.py diff --git a/tests/unit/requirements/test_profile_namespaces.py b/tests/unit/requirements/test_profile_namespaces.py new file mode 100644 index 000000000..ead3d3af6 --- /dev/null +++ b/tests/unit/requirements/test_profile_namespaces.py @@ -0,0 +1,177 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest +from pyshacl import validate +from rdflib import RDF, Graph, Literal, Namespace, URIRef +from rdflib.namespace import OWL, SH + +PROFILES_PATH = Path(__file__).parents[3] / "rocrate_validator/profiles" +PROFILE_NAMESPACE_PREFIX = "https://github.com/crs4/rocrate-validator/profiles/" +SCHEMA = Namespace("http://schema.org/") +EXAMPLE = Namespace("https://example.org/") + + +def _parse_graph(path: Path) -> Graph: + return Graph().parse(path, format="turtle") + + +def _profile_paths_with(*relative_paths: str) -> list[Path]: + return [ + ontology_path.parent + for ontology_path in sorted(PROFILES_PATH.rglob("ontology.ttl")) + if all((ontology_path.parent / relative_path).is_file() for relative_path in relative_paths) + ] + + +def _profile_id(profile_path: Path) -> str: + return str(profile_path.relative_to(PROFILES_PATH)) + + +def _collect_profile_namespaces(graph: Graph) -> set[str]: + """ + Return declared validator namespaces that occur in actual graph terms. + + Ignoring unused prefix declarations keeps the comparison focused on the + vocabulary IRIs that the artifact really uses. + """ + declared_namespaces = { + str(namespace) for _, namespace in graph.namespaces() if str(namespace).startswith(PROFILE_NAMESPACE_PREFIX) + } + terms = [str(term) for triple in graph for term in triple if isinstance(term, URIRef)] + return {namespace for namespace in declared_namespaces if any(term.startswith(namespace) for term in terms)} + + +def _parse_profile_artifacts(profile_path: Path, *, exclude: set[Path] | None = None) -> Graph: + graph = Graph() + excluded_paths = exclude or set() + for artifact_path in sorted(profile_path.rglob("*.ttl")): + if artifact_path not in excluded_paths: + graph.parse(artifact_path, format="turtle") + return graph + + +@pytest.mark.parametrize("profile_path", _profile_paths_with("ontology.ttl"), ids=_profile_id) +def test_profile_ontology_uses_the_namespace_of_its_shapes(profile_path: Path): + """ + Every profile vocabulary namespace declared by an ontology must also occur + in that profile's SHACL artifacts. + + This is the general invariant behind issue 193: if the ontology and shapes + use different namespaces, a derived profile can target an ontology class + that the base shapes never emit or target. + """ + ontology_path = profile_path / "ontology.ttl" + ontology = _parse_graph(ontology_path) + artifacts = _parse_profile_artifacts(profile_path, exclude={ontology_path}) + + ontology_namespaces = _collect_profile_namespaces(ontology) + artifact_namespaces = _collect_profile_namespaces(artifacts) + + assert ontology_namespaces, f"{ontology_path} must use at least one profile namespace" + assert ontology_namespaces <= artifact_namespaces, ( + f"{ontology_path} uses namespaces not used by the profile's shapes: {ontology_namespaces - artifact_namespaces}" + ) + + +@pytest.mark.parametrize("profile_path", _profile_paths_with("ontology.ttl", "prefixes.ttl"), ids=_profile_id) +def test_sparql_prefixes_use_the_profile_ontology_namespace(profile_path: Path): + """ + The SPARQL prefix registry must use the ontology's vocabulary namespace. + + SPARQL constraints rely on this registry to expand prefixed class IRIs; a + different namespace would make those constraints refer to other resources. + """ + ontology = _parse_graph(profile_path / "ontology.ttl") + prefixes = _parse_graph(profile_path / "prefixes.ttl") + + ontology_namespaces = _collect_profile_namespaces(ontology) + prefix_namespaces = _collect_profile_namespaces(prefixes) + + assert prefix_namespaces == ontology_namespaces, ( + f"{profile_path} prefixes must use the ontology namespaces: " + f"expected {ontology_namespaces}, got {prefix_namespaces}" + ) + + +@pytest.mark.parametrize( + "profile_path", + _profile_paths_with("ontology.ttl", "must/2_root_data_entity_metadata.ttl"), + ids=_profile_id, +) +def test_root_data_entity_iri_matches_between_ontology_and_shacl(profile_path: Path): + """ + The root-identification rule must emit the exact class IRI declared by the ontology. + + The assertion compares complete URI terms, not just the local name + ``RootDataEntity``. This catches a namespace mismatch at the point where + the base profile assigns the class to the root node. + """ + ontology = _parse_graph(profile_path / "ontology.ttl") + root_shapes = _parse_graph(profile_path / "must/2_root_data_entity_metadata.ttl") + + ontology_class = next( + node + for node in ontology.subjects(RDF.type, OWL.Class) + if isinstance(node, URIRef) and str(node).endswith("/RootDataEntity") + ) + + assert (None, SH.object, ontology_class) in root_shapes, ( + "SHACL root-identification rule must reference the same RootDataEntity IRI as the ontology" + ) + + +@pytest.mark.parametrize( + "profile_path", + _profile_paths_with("ontology.ttl", "must/2_root_data_entity_metadata.ttl"), + ids=_profile_id, +) +def test_external_shape_receives_focus_node_not_vacuous_conformance(profile_path: Path): + """ + An external shape targeting the ontology class must receive a focus node. + + The test adds a root node with the ontology-declared class and applies an + external shape requiring a missing ``schema:name``. With matching + namespaces, validation finds the node and fails; with the original mismatch, + it finds no focus node and incorrectly passes vacuously. + """ + ontology = _parse_graph(profile_path / "ontology.ttl") + root_class = next( + node + for node in ontology.subjects(RDF.type, OWL.Class) + if isinstance(node, URIRef) and str(node).endswith("/RootDataEntity") + ) + + data = Graph() + data.add((EXAMPLE.root, RDF.type, root_class)) + + external_shapes = Graph() + external_shapes.add((EXAMPLE.RootShape, RDF.type, SH.NodeShape)) + external_shapes.add((EXAMPLE.RootShape, SH.targetClass, root_class)) + external_shapes.add((EXAMPLE.RootShape, SH.property, EXAMPLE.RequiredName)) + external_shapes.add((EXAMPLE.RequiredName, SH.path, SCHEMA.name)) + external_shapes.add((EXAMPLE.RequiredName, SH.minCount, Literal(1))) + + conforms, _, _ = validate( + data, + shacl_graph=external_shapes, + ont_graph=ontology, + inference="owlrl", + ) + + assert not conforms, ( + "external shape must fail validation (missing required property), not pass vacuously due to namespace mismatch" + ) From 5a98e4e02ce60bd5e61e3be943ceea8bba166e25 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 8 Sep 2026 16:33:32 +0200 Subject: [PATCH 030/271] initial copy of 1.2 profile and workflow-related tests --- .../1.3/may/4_data_entity_metadata.ttl | 89 +++ .../ro-crate/1.3/may/61_license_entity.ttl | 65 ++ .../1.3/may/6_contextual_entity_metadata.ttl | 82 +++ .../1.3/must/0_file_descriptor_format.py | 626 ++++++++++++++++++ .../1.3/must/1_file-descriptor_metadata.ttl | 88 +++ .../ro-crate/1.3/must/1_ro_crate_preview.py | 46 ++ .../1.3/must/2_root_data_entity_cite_as.py | 86 +++ .../1.3/must/2_root_data_entity_haspart.ttl | 49 ++ .../1.3/must/2_root_data_entity_identifier.py | 47 ++ .../1.3/must/2_root_data_entity_metadata.ttl | 170 +++++ .../1.3/must/2_root_identifier_property.py | 48 ++ .../1.3/must/4_data_entity_metadata.py | 275 ++++++++ .../1.3/must/4_data_entity_metadata.ttl | 221 +++++++ .../1.3/must/5_web_data_entity_metadata.ttl | 49 ++ .../1.3/must/6_contextual_entity_metadata.ttl | 213 ++++++ .../ro-crate/1.3/must/8_workflow_metadata.ttl | 175 +++++ .../profiles/ro-crate/1.3/ontology.ttl | 84 +++ .../profiles/ro-crate/1.3/prefixes.ttl | 59 ++ .../profiles/ro-crate/1.3/profile.ttl | 74 +++ .../1.3/should/0_entity_identifier_format.py | 134 ++++ .../ro-crate/1.3/should/0_entity_metadata.ttl | 75 +++ .../1.3/should/1_file-descriptor_metadata.ttl | 56 ++ .../1.3/should/1_file_descriptor_name.py | 58 ++ .../should/2_root_data_entity_conformsto.ttl | 47 ++ .../1.3/should/2_root_data_entity_funding.ttl | 94 +++ .../should/2_root_data_entity_identifier.py | 174 +++++ .../should/2_root_data_entity_identifier.ttl | 65 ++ .../should/2_root_data_entity_metadata.ttl | 30 + .../should/2_root_data_entity_publisher.ttl | 36 + .../should/3_ro_crate_preview_exclusion.ttl | 29 + .../1.3/should/4_data_entity_license.py | 142 ++++ .../1.3/should/4_data_entity_metadata.py | 177 +++++ .../1.3/should/4_data_entity_metadata.ttl | 209 ++++++ .../1.3/should/4_dataset_data_entity.ttl | 68 ++ .../1.3/should/4_missing_file_local_path.py | 118 ++++ .../1.3/should/4_referenced_rocrate.py | 113 ++++ .../1.3/should/4_referenced_rocrate.ttl | 199 ++++++ .../1.3/should/4_web_dataset_distribution.py | 96 +++ .../1.3/should/5_web_data_entity_metadata.ttl | 46 ++ .../should/6_contextual_entity_metadata.ttl | 277 ++++++++ .../1.3/should/6_organization_metadata.ttl | 117 ++++ .../1.3/should/8_workflow_metadata.ttl | 166 +++++ .../image_about/invalid/diagram.png | 0 .../invalid/ro-crate-metadata.json | 51 ++ .../image_about/invalid/workflow.ga | 0 .../image_about/valid/diagram.png | 0 .../image_about/valid/ro-crate-metadata.json | 124 ++++ .../image_about/valid/workflow.ga | 0 .../image_encoding_format/invalid/diagram.png | 0 .../invalid/ro-crate-metadata.json | 51 ++ .../image_encoding_format/invalid/workflow.ga | 0 .../image_encoding_format/valid/diagram.png | 0 .../valid/ro-crate-metadata.json | 124 ++++ .../image_encoding_format/valid/workflow.ga | 0 .../invalid/ro-crate-metadata.json | 32 + .../programming_language/invalid/script.sh | 0 .../valid/ro-crate-metadata.json | 88 +++ .../programming_language/valid/script.sh | 0 .../invalid/ro-crate-metadata.json | 31 + .../script_name/invalid/script.sh | 0 .../script_name/valid/ro-crate-metadata.json | 57 ++ .../script_name/valid/script.sh | 0 .../invalid/ro-crate-metadata.json | 30 + .../script_type/valid/ro-crate-metadata.json | 57 ++ .../script_type/valid/script.sh | 0 .../invalid/ro-crate-metadata.json | 39 ++ .../workflow_conformsTo/invalid/script.sh | 0 .../workflow_conformsTo/invalid/workflow.ga | 0 .../valid/ro-crate-metadata.json | 104 +++ .../workflow_conformsTo/valid/script.sh | 0 .../workflow_conformsTo/valid/workflow.ga | 0 .../invalid/ro-crate-metadata.json | 31 + .../workflow_name/invalid/script.sh | 0 .../workflow_name/invalid/workflow.ga | 0 .../valid/ro-crate-metadata.json | 61 ++ .../workflow_name/valid/script.sh | 0 .../workflow_name/valid/workflow.ga | 0 .../ro-crate-metadata.json | 30 + .../ro-crate-metadata.json | 30 + .../valid/ro-crate-metadata.json | 61 ++ .../workflow_type/valid/script.sh | 0 .../workflow_type/valid/workflow.ga | 0 .../ro-crate-1.3/test_workflows_scripts.py | 324 +++++++++ 83 files changed, 6397 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/ontology.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/profile.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/diagram.png create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/diagram.png create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/diagram.png create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/diagram.png create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/script.sh create mode 100644 tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/workflow.ga create mode 100644 tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl new file mode 100644 index 000000000..488dbfb16 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl @@ -0,0 +1,89 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix sh: . +@prefix xsd: . +@prefix owl: . +@prefix schema: . +@prefix validator: . + +ro-crate:FileDataEntityWebOptionalProperties a sh:NodeShape ; + sh:name "File Data Entity with web presence: OPTIONAL properties" ; + sh:description """A File Data Entity which have a corresponding web presence, + for instance a landing page that describes the file, including persistence identifiers (e.g. DOI), + resolving to an intermediate HTML page instead of the downloadable file directly. + These can included for File Data Entities as additional metadata by using the properties: + `identifier`, `url`, `subjectOf`and `mainEntityOfPage`""" ; + sh:targetClass ro-crate:File ; + # Check if the Web-based Data Entity has a contentSize property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "File Data Entity: optional formal `identifier` (e.g. DOI)" ; + sh:description """Check if the File Data Entity has a formal identifier string such as a DOI""" ; + sh:path schema:identifier ; + sh:datatype xsd:anyURI ; + sh:severity sh:Info ; + sh:message """The File Data Entity MAY have a formal identifier specified through an `identifier` property""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "File Data Entity: optional `url` property" ; + sh:description """Check if the File Data Entity has an optional `download` link""" ; + sh:path schema:url ; + sh:datatype xsd:anyURI ; + sh:severity sh:Info ; + sh:message """The File Data Entity MAY use a `url` property to denote a `download` link""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "File Data Entity: optional `subjectOf` property" ; + sh:description """Check if the File Data Entity includes a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; + sh:path schema:subjectOf ; + sh:class schema:WebPage, schema:CreativeWork ; + sh:severity sh:Info ; + sh:message """The File Data Entity MAY include a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "File Data Entity: optional `mainEntityOfPage` property" ; + sh:description """Check if the File Data Entity has a `mainEntityOfPage` property""" ; + sh:path schema:mainEntityOfPage ; + sh:class schema:WebPage, schema:CreativeWork ; + sh:severity sh:Info ; + sh:message """The File Data Entity MAY have a `mainEntityOfPage` property""" ; + ] . + + +ro-crate:DirectoryDataEntityWebOptionalDistribution a sh:NodeShape ; + sh:name "Directory Data Entity: OPTIONAL `distribution` property" ; + sh:description """A Directory Data Entity MAY have a `distribution` property to denote the distribution of the files within the directory""" ; + sh:targetClass ro-crate:Directory ; + # Check if the Web-based Data Entity has a contentSize property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Directory Data Entity: optional `distribution` property" ; + sh:description """Check if the Directory Data Entity has a `distribution` property""" ; + sh:path schema:distribution ; + sh:datatype xsd:anyURI ; + sh:severity sh:Info ; + sh:message """The Directory Data Entity MAY have a `distribution` property to denote the distribution of the files within the directory""" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl new file mode 100644 index 000000000..4679f9521 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl @@ -0,0 +1,65 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xsd: . + + +ro-crate:LicenseOptionalAllowedValues a sh:NodeShape ; + sh:name "Root Data Entity: optional properties" ; + sh:description """Define the optional properties for the Root Data Entity (e.g., license)""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "License" ; + sh:description """Check if the RO-Crate has a license property with a URI or a textual description""" ; + sh:message """MAY have a URI (eg for Creative Commons or Open Source licenses). + MAY, if necessary be a textual description of how the RO-Crate may be used.""" ; + sh:minCount 1 ; + sh:nodeKind sh:IRIOrLiteral ; + sh:path schema_org:license ; + sh:or ( + [ sh:dataType xsd:string ] + [ sh:dataType xsd:anyURI ] + ) ; + ]. + +ro-crate:LicenseDefinition a sh:NodeShape ; + sh:name "License definition" ; + sh:description """Contextual entity representing a license with a name and description."""; + sh:targetClass schema_org:license ; + sh:property [ + a sh:PropertyShape ; + sh:name "License name" ; + sh:description "The license MAY have a name" ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path schema_org:name ; + sh:message "Missing license name" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "License description" ; + sh:description """The license MAY have a description""" ; + sh:maxCount 1; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:path schema_org:description ; + sh:message "Missing license description" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl new file mode 100644 index 000000000..588e4207b --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl @@ -0,0 +1,82 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . + +# RO-Crate 1.2, Encoding Format Entity — MAY include `WebPageElement` in +# `@type` if the `@id` links to a section of a webpage (i.e. the URI carries +# a fragment identifier such as `https://example.org/page.html#section-foo`). +# This is purely optional; the shape emits an `sh:Info` suggestion — never a +# violation — so users can see the permissive recommendation without their +# crate being marked invalid. +ro-crate:EncodingFormatOptionalWebPageElement a sh:NodeShape ; + sh:name "Encoding format: OPTIONAL `WebPageElement` type for section references" ; + sh:description """An encoding format contextual entity whose `@id` links to + a section of a webpage (the URI contains a fragment identifier) MAY + include `WebPageElement` in its `@type` + (RO-Crate 1.2, Encoding Format Entity).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?s schema:encodingFormat ?this . + FILTER(isIRI(?this)) + FILTER(CONTAINS(STR(?this), "#")) + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Encoding format: OPTIONAL `WebPageElement` @type" ; + sh:description "Check if the encoding format entity includes `WebPageElement` in its `@type`." ; + sh:path rdf:type ; + sh:hasValue schema:WebPageElement ; + sh:severity sh:Info ; + sh:message "An encoding format entity whose `@id` is a section of a webpage MAY include `WebPageElement` in its `@type`" ; + ] . + +# RO-Crate 1.2, Software Applications / ComputerLanguage — `alternateName` +# MAY be present if there is a shorter colloquial name for the software or +# language (e.g. `"alternateName": "GNU Bourne-Again Shell"` for Bash). +# Emits an `sh:Info` suggestion when the property is missing. +ro-crate:SoftwareApplicationOrComputerLanguageOptionalAlternateName a sh:NodeShape ; + sh:name "SoftwareApplication or ComputerLanguage: OPTIONAL `alternateName`" ; + sh:description """A contextual entity representing a `SoftwareApplication` + or a `ComputerLanguage` MAY declare an `alternateName` property to + provide a shorter colloquial name for the software or language + (RO-Crate 1.2, Provenance — Software Applications).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a ?t . + FILTER(?t IN (schema:SoftwareApplication, schema:ComputerLanguage)) + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "SoftwareApplication/ComputerLanguage: OPTIONAL `alternateName`" ; + sh:description "Check if the entity declares an `alternateName` property." ; + sh:path schema:alternateName ; + sh:minCount 1 ; + sh:severity sh:Info ; + sh:message "A SoftwareApplication or ComputerLanguage MAY declare an `alternateName` property to provide a shorter colloquial name" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py new file mode 100644 index 000000000..5c230f089 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py @@ -0,0 +1,626 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re +from http import HTTPStatus +from pathlib import Path +from typing import Any +from urllib.parse import urljoin + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.http import HttpRequester + +# set up logging +logger = logging.getLogger(__name__) + +# Property keys whose string values are literals (e.g. names, dates, URLs) rather +# than references to other entities, so a matching @id must not be flagged. +_LITERAL_KEYS = frozenset( + { + "name", + "description", + "encodingFormat", + "contentSize", + "datePublished", + "keywords", + "creditText", + "contentUrl", + "copyrightNotice", + "version", + "softwareVersion", + "value", + "propertyID", + "actionStatus", + "error", + "startTime", + "endTime", + "url", + "identifier", + } +) + + +@requirement(name="File Descriptor existence") +class FileDescriptorExistence(PyFunctionCheck): + """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" + + @check(name="File Descriptor Existence") + def test_existence(self, context: ValidationContext) -> bool: + """ + Check if the file descriptor is present in the RO-Crate + """ + if context.settings.metadata_only: + logger.debug("Skipping file descriptor existence check in metadata-only mode") + return True + if not context.ro_crate.has_descriptor(): + message = f'file descriptor "{context.rel_fd_path}" is not present' + context.result.add_issue(message, self) + return False + return True + + @check(name="File Descriptor size check") + def test_size(self, context: ValidationContext) -> bool: + """ + Check if the file descriptor is not empty + """ + if context.settings.metadata_only: + logger.debug("Skipping file descriptor existence check in metadata-only mode") + return True + if not context.ro_crate.has_descriptor(): + message = f"file descriptor {context.rel_fd_path} is empty" + context.result.add_issue(message, self) + return False + if context.ro_crate.metadata.size == 0: + context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) + return False + return True + + +@requirement(name="File Descriptor UTF-8 encoding") +class FileDescriptorEncodingCheck(PyFunctionCheck): + """ + The file descriptor MUST be UTF-8 encoded + """ + + @check(name="File Descriptor UTF-8 encoding") + def check(self, context: ValidationContext) -> bool: + """ + Check if the file descriptor is UTF-8 encoded + """ + try: + raw_data = context.ro_crate.get_file_content( + Path(context.ro_crate.metadata_descriptor_id), binary_mode=True + ) + if isinstance(raw_data, str): + return True + raw_data.decode("utf-8") + return True + except Exception: + context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + +@requirement(name="File Descriptor JSON format") +class FileDescriptorJsonFormat(PyFunctionCheck): + """ + The file descriptor MUST be a valid JSON file + """ + + @check(name="File Descriptor JSON format") + def check(self, context: ValidationContext) -> bool: + """Check if the file descriptor is in the correct format""" + try: + logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) + context.ro_crate.metadata.as_dict() + return True + except json.JSONDecodeError as e: + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" is not valid JSON: ' + f"{e.msg} at line {e.lineno}, column {e.colno} (char {e.pos})", + self, + ) + if logger.isEnabledFor(logging.DEBUG): + logger.debug("RO-Crate file descriptor is not valid JSON", exc_info=True) + # The metadata cannot be parsed: abort to avoid false positives downstream. + context.abort_validation(f"file descriptor is not valid JSON: {e}") + return False + except Exception: + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self + ) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + +@requirement(name="File Descriptor JSON-LD format") +class FileDescriptorJsonLdFormat(PyFunctionCheck): + """ + The file descriptor MUST be a valid JSON-LD file + """ + + def __get_remote_context__(self, context_uri: str) -> object: + raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json, application/json"}) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'", self) + logger.debug(f"Retrieved context from {context_uri}") + + content_type = raw_data.headers.get("Content-Type", "") + is_valid_content_type = "application/ld+json" in content_type or "application/json" in content_type + if not is_valid_content_type: + logger.debug( + f"The retrieved context from {context_uri} " + f"does not have a Content-Type of application/ld+json or application/json: " + f"the actual Content-Type is {content_type}. " + ) + link_header = raw_data.headers.get("Link", "") + logger.debug(f"Checking Link header for alternate JSON-LD context: {link_header}") + has_alternate_link = 'rel="alternate"' in link_header and ( + 'type="application/ld+json"' in link_header or 'type="application/json"' in link_header + ) + + if has_alternate_link: + logger.debug(f"Found alternate link for JSON-LD context in Link header: {link_header}") + match = re.search(r'<([^>]+)>;\s*rel="alternate";\s*type="application/(ld\+json|json)"', link_header) + if match: + alternate_url = match.group(1) + if not alternate_url.startswith("http"): + alternate_url = urljoin(context_uri, alternate_url) + logger.debug(f"Trying to retrieve JSON-LD context from alternate URL: {alternate_url}") + raw_data = HttpRequester().get( + alternate_url, headers={"Accept": "application/ld+json, application/json"} + ) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError( + f"Unable to retrieve the JSON-LD context from alternate URL '{alternate_url}'", self + ) + logger.debug(f"Retrieved context from alternate URL {alternate_url}") + content_type = raw_data.headers.get("Content-Type", "") + if "application/ld+json" not in content_type and "application/json" not in content_type: + raise RuntimeError( + f"The retrieved context from alternate URL {alternate_url} " + "does not have a Content-Type of application/ld+json or application/json: " + f"the actual Content-Type is {content_type}. ", + self, + ) + else: + logger.debug(f"No valid alternate link found in Link header: {link_header}") + raise RuntimeError( + f"Unable to retrieve the JSON-LD context from {context_uri} and no valid " + f"alternate link found in Link header: {link_header}", + self, + ) + else: + logger.debug(f"No alternate link for JSON-LD context found in Link header: {link_header}") + raise RuntimeError( + f"Unable to retrieve the JSON-LD context from {context_uri} " + f"and no alternate link found in Link header: {link_header}", + self, + ) + + jsonLD = raw_data.json()["@context"] + assert isinstance(jsonLD, dict) + return jsonLD + + def __check_remote_context__(self, context_uri: str) -> bool: + try: + jsonLD = self.__get_remote_context__(context_uri) + assert isinstance(jsonLD, dict) + return True + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + def __check_contexts__(self, context: ValidationContext, jsonld_context: object) -> bool: + """Get the keys of the context URI""" + is_valid = True + # if the context is a string, check if it is a valid URI + if isinstance(jsonld_context, str) and not self.__check_remote_context__(jsonld_context): + context.result.add_issue(f'Unable to retrieve the JSON-LD context "{jsonld_context}"', self) + is_valid = False + + # if the context is a dictionary, get the keys of the dictionary + if isinstance(jsonld_context, dict): + logger.debug(f"Detected dictionary context: {jsonld_context}") + + # if the context is a list of contexts, get the keys of each context + if isinstance(jsonld_context, list): + for ctx in jsonld_context: + if not self.__check_contexts__(context, ctx): + is_valid = False + # return if the context is valid + return is_valid + + @check(name="File Descriptor @context property validation") + def check_context(self, context: ValidationContext) -> bool: + """Check if the file descriptor contains + the @context property and it is a valid JSON-LD context + """ + try: + json_dict = context.ro_crate.metadata.as_dict() + if "@context" not in json_dict: + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" does not contain a context', self + ) + return False + + expected_context = "https://w3id.org/ro/crate/1.2/context" + jsonld_context = json_dict.get("@context") + + def has_expected_context(ctx: object) -> bool: + if isinstance(ctx, str): + return ctx == expected_context + if isinstance(ctx, list): + return expected_context in ctx + return False + + if not has_expected_context(jsonld_context): + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" ' + f'does not reference the required context "{expected_context}"', + self, + ) + return False + + # Check if the context is valid + return self.__check_contexts__(context, json_dict["@context"]) + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + def _check_flat_value_object(self, context: ValidationContext, entity: dict, fail_fast: bool) -> bool: + """Validate the JSON-LD `@value` object constraints; returns False on violation.""" + result = True + has_language = "@language" in entity + has_type = "@type" in entity + if has_language and has_type: + context.result.add_issue( + f'entity "{entity.get("@id", entity)}" is not a valid value object: ' + "@language and @type cannot coexist.", + self, + ) + result = False + if fail_fast: + return False + if has_language and not isinstance(entity["@value"], str): + context.result.add_issue( + f'entity "{entity.get("@id", entity)}" is not a valid value object: ' + "if @language is present, @value must be a string.", + self, + ) + result = False + if fail_fast: + return False + return result + + def _check_flat_non_root_dict(self, context: ValidationContext, entity: dict, fail_fast: bool) -> bool: + """A non-root dict must be either an `@id` reference or a `@value` object.""" + result = True + if "@id" in entity and "@value" in entity: + context.result.add_issue( + f'entity "{entity.get("@id", entity)}" contains both @id and @value: ' + "an object with an @value represents a value object, which is a literal value such as " + "a string, number, date, or language-tagged string. This object is not an identifiable " + "resource, but a simple literal value.", + self, + ) + result = False + if fail_fast: + return False + + if "@value" in entity: + if not self._check_flat_value_object(context, entity, fail_fast): + result = False + if fail_fast: + return False + # Every remaining entity with more than just @id must be a node object + elif "@id" not in entity or len(entity) > 1: + context.result.add_issue( + f'entity "{entity.get("@id", entity)}" is not a valid node object reference: ' + "it MUST have only @id, but no other properties.", + self, + ) + result = False + if fail_fast: + return False + return result + + def _is_entity_flat(self, context: ValidationContext, entity: Any, is_first: bool, fail_fast: bool) -> bool: + """Recursively check that `entity` corresponds to a flattened JSON-LD object.""" + result = True + if isinstance(entity, dict): + if not is_first: + return self._check_flat_non_root_dict(context, entity, fail_fast) + for elem in entity.values(): + if not self._is_entity_flat(context, elem, False, fail_fast): + result = False + if fail_fast: + return False + elif isinstance(entity, list): + for element in entity: + if not self._is_entity_flat(context, element, False, fail_fast): + result = False + if fail_fast: + return False + return result + + @check(name="File Descriptor JSON-LD must be flattened") + def check_flattened(self, context: ValidationContext) -> bool: + """Check if the file descriptor is flattened""" + try: + fail_fast = bool(context.settings.abort_on_first) + json_dict = context.ro_crate.metadata.as_dict() + result = True + for entity in json_dict["@graph"]: + if not self._is_entity_flat(context, entity, True, fail_fast): + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" ' + f'is not fully flattened at entity "{entity.get("@id", entity)}"', + self, + ) + result = False + if fail_fast: + return False + return result + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + @check(name="Validation of the @id property of the file descriptor entities") + def check_identifiers(self, context: ValidationContext) -> bool: + """Check if the file descriptor entities have the @id property""" + try: + json_dict = context.ro_crate.metadata.as_dict() + for entity in json_dict["@graph"]: + if "@id" not in entity: + context.result.add_issue( + f'Entity "{entity.get("name", None) or entity}" ' + f'of RO-Crate "{context.rel_fd_path}" ' + "file descriptor does not contain the @id attribute", + self, + ) + return False + return True + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + @check(name="Validation of the @type property of the file descriptor entities") + def check_types(self, context: ValidationContext) -> bool: + """Check if the file descriptor entities have the @type property""" + try: + json_dict = context.ro_crate.metadata.as_dict() + for entity in json_dict["@graph"]: + if "@type" not in entity: + context.result.add_issue( + f'Entity "{entity.get("name", None) or entity}" ' + f'of RO-Crate "{context.rel_fd_path}" ' + "file descriptor does not contain the @type attribute", + self, + ) + return False + return True + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + @check(name="Validation of unique @id values") + def check_unique_identifiers(self, context: ValidationContext) -> bool: + try: + json_dict = context.ro_crate.metadata.as_dict() + identifiers = [entity.get("@id") for entity in json_dict.get("@graph", [])] + duplicates = {i for i in identifiers if i is not None and identifiers.count(i) > 1} + if duplicates: + context.result.add_issue( + f"Duplicate @id values detected in RO-Crate metadata: {sorted(duplicates)}", self + ) + return False + return True + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, key: str | None = None) -> str | None: + """ + Return an issue message if `value` references another entity by bare + string instead of an `{"@id": ...}` object, recursing into lists and + nested dictionaries; return None when no violation is found. + """ + if isinstance(value, str): + if key not in _LITERAL_KEYS and value in identifiers: + return f"Entity '{entity_id}' references '{value}' as a string; use {{\"@id\": \"{value}\"}}" + return None + if isinstance(value, list): + children: Any = value + elif isinstance(value, dict): + if "@value" in value or "@id" in value: + return None + children = value.values() + else: + return None + for child in children: + message = self._find_reference_issue(child, entity_id, identifiers, key) + if message: + return message + return None + + @check(name="Validation of entity references") + def check_entity_references(self, context: ValidationContext) -> bool: + try: + json_dict = context.ro_crate.metadata.as_dict() + graph = json_dict.get("@graph", []) + identifiers = {entity.get("@id") for entity in graph if entity.get("@id")} + + for entity in graph: + entity_id = entity.get("@id") + for key, value in entity.items(): + if key in ("@id", "@type", "@context"): + continue + message = self._find_reference_issue(value, entity_id, identifiers, key) + if message: + context.result.add_issue(message, self) + return False + return True + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + @check(name="Validation of subject and keyword properties") + def check_subject_keywords(self, context: ValidationContext) -> bool: + try: + json_dict = context.ro_crate.metadata.as_dict() + graph = json_dict.get("@graph", []) + for entity in graph: + entity_id = entity.get("@id") + if "subject" in entity or "dct:subject" in entity or "dcterms:subject" in entity: + context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'about' for subjects", self) + return False + if "keyword" in entity: + context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'keywords'", self) + return False + return True + except Exception: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + return False + + def __get_context_keys__(self, context: object) -> set: + """Get the keys of the context URI""" + if isinstance(context, str): + return self.__get_remote_context_keys__(context) + + # if the context is a dictionary, get the keys of the dictionary + if isinstance(context, dict): + return set(context.keys()) + + # if the context is a list of contexts, get the keys of each context + if isinstance(context, list): + keys = set() + for ctx in context: + keys.update(self.__get_context_keys__(ctx)) + return keys + return set() + + def __get_remote_context_keys__(self, context_uri: str) -> set: + """Get the keys of the context URI""" + + logger.debug(f"Retrieving context from {context_uri}...") + # Try to retrieve the context + raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json"}) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'") + + logger.debug(f"Retrieved context from {context_uri}") + + # Get the keys of the context + jsonLD = raw_data.json() + jsonLD_ctx = jsonLD["@context"] + if not isinstance(jsonLD_ctx, dict): + raise TypeError("The context is not a dictionary") + return set(jsonLD_ctx.keys()) + + def __check_entity_keys__( + self, entity: object, context_keys: set, unexpected_keys: dict[str, int] | None = None + ) -> dict[str, int]: + """Check if the entity is in the correct format""" + + def add_unexpected_key(k: str, u_keys: dict) -> None: + """Add a key to the unexpected keys dictionary""" + u_keys[k] = u_keys.get(k, 0) + 1 + + # Keys that should be skipped + SKIP_KEYS = {"@id", "@type", "@context", "@value", "@language"} + + # Ensure unexpected_keys is initialized + if unexpected_keys is None: + unexpected_keys = {} + + # If the entity is a dictionary, check each key + if isinstance(entity, dict): + for k, v in entity.items(): + if k not in context_keys and k not in SKIP_KEYS: + logger.debug(f"Key {k} not in context keys") + add_unexpected_key(k, unexpected_keys) + if isinstance(v, (dict, list)): + self.__check_entity_keys__(v, context_keys, unexpected_keys) + + # If the entity is a list, check each element + elif isinstance(entity, list): + for elem in entity: + self.__check_entity_keys__(elem, context_keys, unexpected_keys) + + return unexpected_keys + + @check(name="Validation of the compaction format of the file descriptor") + def check_compaction(self, context: ValidationContext) -> bool: + """Check if the file descriptor is in the **compacted** JSON-LD format""" + try: + logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) + json_dict = context.ro_crate.metadata.as_dict() + logger.debug(f"JSONLD keys:{json_dict.keys()}") + + jsonld_context = json_dict.get("@context", None) + logger.debug(f"Context: {jsonld_context}") + + try: + context_keys = self.__get_context_keys__(jsonld_context) + logger.debug(f"{context_keys}") + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + context.result.add_issue(str(e), self) + return False + + unexpected_keys = self.__check_entity_keys__(json_dict.get("@graph", []), context_keys) + logger.debug(f"Unexpected keys: {unexpected_keys}") + if len(unexpected_keys) > 0: + for k, v in unexpected_keys.items(): + logger.debug(f"Key {k} appears {v} times") + # Add the correct suffix to the message + suffix = "s" if v > 1 else "" + # Check if k is a term or a URI + if k.startswith("http"): + context.result.add_issue( + f'The The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "' + "because the compacted format requires simple terms as keys " + "(see https://www.w3.org/TR/json-ld-api/#compaction for more details).", + self, + ) + else: + context.result.add_issue( + f'The {v} occurrence{suffix} of the JSON-LD key "{k}" ' + f"{'is' if v == 1 else 'are'} not allowed in the compacted format " + "because it is not present in the @context of the document", + self, + ) + return False + + return True + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error during file descriptor validation") + context.result.add_issue(f"Unexpected error: {e}", self) + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl new file mode 100644 index 000000000..6c90fb03a --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl @@ -0,0 +1,88 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix validator: . + +ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape ; + sh:name "Identify the RO-Crate Metadata File Descriptor" ; + sh:description """The RO-Crate Metadata File Descriptor entity describes the RO-Crate itself, and it is named as `*-ro-crate-metadata.json`. + It can be identified by name according to the RO-Crate specification + available at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.2/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?crate a schema:Dataset . + ?this schema:about ?crate . + FILTER(contains(str(?this), "ro-crate-metadata.json")) + } + """ + ] ; + + # Expand data graph with triples from the file data entity + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:ROCrateMetadataFileDescriptor ; + ] . + +ro-crate:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor entity existence" ; + sh:description "The RO-Crate JSON-LD MUST contain a Metadata File Descriptor entity typed as `schema:CreativeWork`" ; + sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; + sh:property [ + a sh:PropertyShape ; + sh:name "RO-Crate Metadata File Descriptor entity existence" ; + sh:description """Check if the RO-Crate Metadata File Descriptor entity exists, + i.e., if there exists an entity with @id matching *-ro-crate-metadata.json and type `schema:CreativeWork`""" ; + sh:path rdf:type ; + sh:hasValue ro-crate:ROCrateMetadataFileDescriptor ; + sh:minCount 1 ; + sh:message "The root of the document MUST have a Metadata File Descriptor entity" ; + ] . + +ro-crate:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor REQUIRED properties" ; + sh:description """RO-Crate Metadata Descriptor MUST be defined + according with the requirements details defined in + [RO-Crate Metadata File Descriptor](https://www.researchobject.org/ro-crate/1.2/root-data-entity.html#ro-crate-metadata-file-descriptor)""" ; + sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor entity type" ; + sh:description "Check if the RO-Crate Metadata File Descriptor has `@type` CreativeWork, as per schema.org" ; + sh:minCount 1 ; + sh:nodeKind sh:IRI ; + sh:path rdf:type ; + sh:hasValue schema_org:CreativeWork ; + sh:message "The RO-Crate metadata file MUST be a CreativeWork, as per schema.org" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor entity: `about` property" ; + sh:description """Check if the RO-Crate Metadata File Descriptor has an `about` property referencing the Root Data Entity""" ; + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path schema_org:about ; + # sh:class ro-crate:RootDataEntity ; + sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py new file mode 100644 index 000000000..ed1b2e281 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="RO-Crate Website") +class ROCrateWebsiteChecker(PyFunctionCheck): + """ + If present, the RO-Crate Website MUST be a valid HTML5 document. + """ + + @check(name="RO-Crate Website HTML5 doctype") + def check_preview_html(self, context: ValidationContext) -> bool: + if context.ro_crate.is_detached(): + return True + preview_path = Path("ro-crate-preview.html") + if not context.ro_crate.has_file(preview_path): + return True + try: + content = context.ro_crate.get_file_content(preview_path, binary_mode=False) + if " str | None: + """ + Resolve the `cite-as` value to the http(s) URL that must be checked, + or None when there is nothing to verify (absent value, non-URL literal, + or a reference without an `id`). + """ + if not cite_as_raw: + return None + # cite-as can be a plain string literal or an entity reference {"@id": "..."} + if isinstance(cite_as_raw, str): + cite_as_url = cite_as_raw + elif hasattr(cite_as_raw, "id"): + cite_as_url = cite_as_raw.id + else: + return None + if not cite_as_url or not cite_as_url.startswith("http"): + return None + return cite_as_url + + @check(name="Root Data Entity: `cite-as` MUST reference a downloadable item") + def check_cite_as_downloadable(self, context: ValidationContext) -> bool: + if ( + context.settings.skip_availability_check + or not (context.settings.creation_time or context.settings.enforce_availability) + or context.settings.metadata_only + ): + return True + + try: + root_entity = context.ro_crate.metadata.get_root_data_entity() + cite_as_url = self._resolve_cite_as_url(root_entity.get_property("cite-as")) + if not cite_as_url: + return True + + result = check_downloadable(cite_as_url) + if not result.is_downloadable: + context.result.add_issue( + f"The `cite-as` value '{cite_as_url}' MUST ultimately provide " + f"the RO-Crate as a downloadable item" + (f": {result.reason}" if result.reason else ""), + self, + ) + return False + + logger.debug( + "cite-as '%s' is downloadable via %s (url: %s)", + cite_as_url, + result.via, + result.download_url, + ) + return True + + except Exception as e: + context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl new file mode 100644 index 000000000..95bffb33a --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix schema_org: . +@prefix sh: . + +ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; + sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; + sh:description """The Root Data Entity MUST directly or indirectly reference + all Data Entities in the RO-Crate via hasPart (RO-Crate 1.2).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a ro-crate:RootDataEntity . + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?unreferenced + WHERE { + $this a ro-crate:RootDataEntity . + ?unreferenced a ro-crate:DataEntity . + FILTER(?unreferenced != $this) + FILTER NOT EXISTS { + $this schema:hasPart+ ?unreferenced + } + } + """ ; + sh:severity sh:Violation ; + sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; + sh:message "The Root Data Entity MUST reference all Data Entities via hasPart (directly or indirectly)" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py new file mode 100644 index 000000000..5ac8766c1 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +logger = logging.getLogger(__name__) + + +@requirement(name="Root Data Entity identifier restriction") +class RootDataEntityIdentifierChecker(PyFunctionCheck): + """ + In an attached RO-Crate, the Root Data Entity @id MUST be ./ or an absolute URI. + """ + + @check(name="Root Data Entity: REQUIRED value") + def check_identifier(self, context: ValidationContext) -> bool: + try: + if context.ro_crate.is_detached(): + return True + root_entity = context.ro_crate.metadata.get_root_data_entity() + if root_entity.id == "./": + return True + if re.match(r"^[A-Za-z][A-Za-z0-9+\.-]*:", root_entity.id): + return True + context.result.add_issue( + "The Root Data Entity MUST be a `Dataset` (as per `schema.org`) and use an IRI or `./` as identifier", + self, + ) + return False + except Exception as e: + context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl new file mode 100644 index 000000000..68ce7aaf3 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl @@ -0,0 +1,170 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix prof: . +@prefix sh: . +@prefix validator: . +@prefix xsd: . +@prefix dct: . + +# Selects the Root Data Entity (the Dataset that the metadata file is `about`). +ro-crate:MetadataAboutTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?metadatafile schema:about ?this . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } + """ . + +ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify the Root Data Entity of the RO-Crate" ; + 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. + It is a schema:Dataset and is indirectly identified by the about property of the metadata descriptor in the RO-Crate + (see the definition at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.2/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores)). + """ ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Dataset . + ?metadatafile schema:about ?this . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } + """ + ] ; + + # Expand data graph with triples from the file data entity + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:RootDataEntity ; + ] . + +ro-crate:RootDataEntityType a sh:NodeShape ; + sh:name "RO-Crate Root Data Entity type" ; + sh:description "The Root Data Entity MUST be a `Dataset` (as per `schema.org`)" ; + sh:target ro-crate:MetadataAboutTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity type" ; + sh:description "Check if the Root Data Entity is a `Dataset` (as per `schema.org`)" ; + sh:path rdf:type ; + sh:hasValue schema_org:Dataset ; + sh:minCount 1 ; + sh:message """The Root Data Entity MUST be a `Dataset` (as per `schema.org`)""" ; + ] ; + # Validate that if the publisher is specified, it is an Organization or a Person + sh:property [ + sh:path schema_org:publisher ; + sh:severity sh:Violation ; + sh:name "Root Data Entity: `publisher` property" ; + sh:description """Check if the Root Data Entity has a `publisher` property of type `Organization` or `Person`.""" ; + sh:or ( + [ + sh:class schema_org:Organization + ] + [ + sh:class schema_org:Person + ] + ) ; + sh:message """The Root Data Entity MUST have a `publisher` property of type `Organization` or `Person`.""" ; + ] . + +ro-crate:RootDataEntityRequiredProperties a sh:NodeShape ; + sh:name "RO-Crate Root Data Entity REQUIRED properties" ; + sh:description "The Root Data Entity MUST have a `name`, `description`, `license` and `datePublished`" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: `name` property" ; + sh:description """Check if the Root Data Entity includes a `name` (as specified by schema.org) + to clearly identify the dataset and distinguish it from other datasets.""" ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:path schema_org:name ; + sh:message "The Root Data Entity MUST have a `name` property (as specified by schema.org)" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: `description` property" ; + sh:description """Check if the Root Data Entity includes a `description` (as specified by schema.org) + to provide a human-readable description of the dataset.""" ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:path schema_org:description ; + sh:message "The Root Data Entity MUST have a `description` property (as specified by schema.org)" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: `license` property" ; + sh:description """Check if the Root Data Entity includes a `license` property (as specified by schema.org) + to provide information about the license of the dataset.""" ; + sh:path schema_org:license ; + sh:minCount 1 ; + sh:nodeKind sh:IRIOrLiteral ; + sh:message """The Root Data Entity MUST have a `license` property (as specified by schema.org).""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: `datePublished` property" ; + sh:description """Check if the Root Data Entity includes a `datePublished` (as specified by schema.org) + to provide the date when the dataset was published. The datePublished MUST be a valid ISO 8601 date.""" ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:datatype xsd:string ; + sh:path schema_org:datePublished ; + sh:pattern "^([\\+-]?\\d{4})((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)?[0-5]\\d)?|24:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" ; + sh:message "The Root Data Entity MUST have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date" ; + ] . + +ro-crate:RootDataEntityOptionalConformsToProperty a sh:NodeShape ; + sh:name "RO-Crate Root Data Entity: optional `conformsTo` property value restriction" ; + sh:description """If the Root Data Entity includes a `conformsTo` property, its values MUST reference Profile entities.""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + sh:name "Root Data Entity: optional `conformsTo` property value restriction" ; + sh:description """Check if the Root Data Entity's `conformsTo` property values reference Profile entities.""" ; + sh:path dct:conformsTo ; + sh:class prof:Profile ; + sh:message """If the Root Data Entity includes a `conformsTo` property, its values MUST reference Profile entities.""" ; + ] . + +ro-crate:RootDataEntityVersionlessProfileProhibition a sh:NodeShape ; + sh:name "RO-Crate Root Data Entity: MUST NOT declare version-less conformsTo profile" ; + sh:description """The Root Data Entity MUST NOT declare the version-less generic RO-Crate profile URI + (https://w3id.org/ro/crate) in its conformsTo property. + Only versioned profile URIs (e.g., https://w3id.org/ro/crate/1.2) are permitted on the Root Data Entity. + The version-less URI is reserved for referenced RO-Crate data entities.""" ; + sh:target ro-crate:MetadataAboutTarget ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this dct:conformsTo . + } + """ ; + sh:name "Root Data Entity: MUST NOT declare version-less conformsTo" ; + sh:message "The Root Data Entity MUST NOT declare the version-less RO-Crate profile URI (https://w3id.org/ro/crate) in its conformsTo property. Use a versioned URI such as https://w3id.org/ro/crate/1.2 instead." ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py new file mode 100644 index 000000000..1c1f17b9f --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Root Data Entity: identifier PropertyValue") +class RootIdentifierPropertyChecker(PyFunctionCheck): + """ + If the Root Data Entity identifier references a PropertyValue, it MUST include value. + """ + + @check(name="Root Data Entity: identifier PropertyValue value") + def check_identifier_values(self, context: ValidationContext) -> bool: + try: + root = context.ro_crate.metadata.get_root_data_entity() + identifiers = root.get_property("identifier") + if identifiers is None: + return True + identifiers = identifiers if isinstance(identifiers, list) else [identifiers] + for identifier in identifiers: + if not hasattr(identifier, "has_type"): + continue + if not identifier.has_type("PropertyValue"): + continue + if not identifier.get_property("value"): + context.result.add_issue("PropertyValue identifiers MUST include a `value`", self) + return False + return True + except Exception as e: + context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py new file mode 100644 index 000000000..780ab17f1 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py @@ -0,0 +1,275 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import re + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.signposting import check_downloadable + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Data Entity: REQUIRED resource availability") +class DataEntityRequiredChecker(PyFunctionCheck): + """ + Resources corresponding to local Data Entities MUST be present in the RO-Crate payload + """ + + @check(name="Data Entity: REQUIRED resource availability") + def check_availability(self, context: ValidationContext) -> bool: + """ + Check the presence of the Data Entity in the RO-Crate + """ + if context.ro_crate.is_detached(): + logger.debug("Skipping data entity payload checks for detached RO-Crate") + return True + # Skip the check in metadata-only mode + if context.settings.metadata_only: + logger.debug("Skipping file descriptor existence check in metadata-only mode") + return True + # Perform the check + result = True + for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + assert entity.id is not None, "Entity has no @id" + logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) + try: + logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) + if entity.has_local_identifier(): + logger.debug( + "Ignoring the Data Entity '%s' as it is a local entity with a local identifier. " + "According to the RO-Crate specification, local entities with local identifiers " + "are not required to be included in the RO-Crate payload" + "(see https://github.com/ResearchObject/ro-crate/issues/400#issuecomment-2779152885 and " + "https://github.com/ResearchObject/ro-crate/pull/426 for more details)", + entity.id, + ) + continue + if not entity.has_relative_path(): + logger.debug( + "Ignoring the Data Entity '%s' as it is a local entity with an absolute path. " + "According to the RO-Crate specification, local entities with absolute paths " + "are not required to be included in the RO-Crate payload. " + "It is only recommended that they exist at the time of RO-Crate creation.", + entity.id, + ) + continue + if not entity.is_available(): + context.result.add_issue( + f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self + ) + result = False + except Exception as e: + context.result.add_issue( + f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self + ) + if logger.isEnabledFor(logging.DEBUG): + logger.debug(e, exc_info=True) + result = False + if not result and context.fail_fast: + return result + return result + + +@requirement(name="Detached RO-Crate: data entities MUST be web-based") +class DetachedDataEntityChecker(PyFunctionCheck): + """ + In a detached RO-Crate, all Data Entities MUST be web-based + resources (i.e., have an absolute URL as @id). + """ + + @check(name="Detached RO-Crate: data entities MUST be web-based") + def check_detached_entities(self, context: ValidationContext) -> bool: + if not context.ro_crate.is_detached(): + return True + result = True + root_entity_id = None + with contextlib.suppress(Exception): + root_entity_id = context.ro_crate.metadata.get_root_data_entity().id + for entity in context.ro_crate.metadata.get_data_entities(): + if root_entity_id and entity.id == root_entity_id: + continue + if not entity.is_remote(): + context.result.add_issue( + f"Data Entity '{entity.id}' is not web-based, " + f"but in a detached RO-Crate all Data Entities " + f"MUST have an absolute URL as @id", + self, + ) + result = False + if context.fail_fast: + return False + return result + + +@requirement(name="Data Entity: identifier requirements") +class DataEntityIdentifierChecker(PyFunctionCheck): + """ + Data Entity identifiers must be valid URI references and use relative paths for payload files. + """ + + @check(name="Data Entity: @id value requirements") + def check_identifiers(self, context: ValidationContext) -> bool: + result = True + root_entity_id = None + root_entity_is_local = False + root_entity_absolute_path = None + with contextlib.suppress(Exception): + root_data_entity = context.ro_crate.metadata.get_root_data_entity() + root_entity_id = root_data_entity.id + root_entity_is_local = ( + root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False + ) + root_entity_absolute_path = root_data_entity.id_as_path if root_data_entity.has_absolute_path() else None + for entity in context.ro_crate.metadata.get_data_entities(): + if root_entity_id and entity.id == root_entity_id: + continue + if not root_entity_is_local and not entity.is_remote(): + context.result.add_issue( + f"Data Entity '{entity.id}' has a local identifier but the Root Data Entity " + "does not have a local identifier", + self, + ) + result = False + if context.fail_fast: + return False + if entity.has_local_identifier(): + continue + if "\\" in entity.id or " " in entity.id: + context.result.add_issue( + f"Data Entity '{entity.id}' has an invalid @id; use URI-compatible paths", self + ) + result = False + if context.fail_fast: + return False + if ( + root_entity_is_local + and not str(entity.id_as_path).startswith(str(root_entity_absolute_path)) + and not str(entity.id).startswith("./") + and (str(entity.id).startswith(("/", "file://"))) + ): + context.result.add_issue( + f"Data Entity '{entity.id}' MUST use a relative @id within the RO-Crate root", self + ) + result = False + if context.fail_fast: + return False + return result + + @check(name="Data Entity: relative @id for payload files") + def check_relative_paths(self, context: ValidationContext) -> bool: + if context.ro_crate.is_detached(): + return True + result = True + for entity in context.ro_crate.metadata.get_data_entities(): + if entity.has_local_identifier() or entity.is_remote(): + continue + if entity.has_absolute_path() and ( + context.ro_crate.has_file(entity.id_as_path) or context.ro_crate.has_directory(entity.id_as_path) + ): + context.result.add_issue( + f"Data Entity '{entity.id}' should use a relative @id within the RO-Crate root", self + ) + result = False + if context.fail_fast: + return False + return result + + +@requirement(name="Data Entity: citation references") +class DataEntityCitationChecker(PyFunctionCheck): + """ + Citation references must include an absolute URI. + """ + + @check(name="Data Entity: citation must include @id") + def check_citation(self, context: ValidationContext) -> bool: + result = True + for entity in context.ro_crate.metadata.get_data_entities(): + citations = entity.get_property("citation") + if citations is None: + continue + citation_list = citations if isinstance(citations, list) else [citations] + for citation in citation_list: + if isinstance(citation, str): + citation_id = citation + elif hasattr(citation, "id"): + citation_id = citation.id + else: + context.result.add_issue( + f"Citation for Data Entity '{entity.id}' must reference a publication @id", self + ) + result = False + if context.fail_fast: + return False + continue + if not re.match(r"^[A-Za-z][A-Za-z0-9+\.-]*:", citation_id): + context.result.add_issue(f"Citation for Data Entity '{entity.id}' must be an absolute URI", self) + result = False + if context.fail_fast: + return False + return result + + +@requirement(name="Web-based Data Entity: REQUIRED availability") +class WebDataEntityRequiredChecker(PyFunctionCheck): + """ + Web-based Data Entities MUST be directly downloadable at the time of creation + (RO-Crate 1.2). Downloadability is verified via Signposting (rel=item, + rel=describedby), direct Content-Type inspection, and content negotiation. + """ + + @staticmethod + def _not_downloadable_message(entity_id: str, dl) -> str: + """Build the issue message for a web-based Data Entity that is not directly downloadable.""" + if dl.reason and "HTML" in dl.reason: + return ( + f"Web-based Data Entity '{entity_id}' references an HTML page " + f"(possible splash page or viewer application); " + f"it MUST be directly downloadable" + ) + msg = f"Web-based Data Entity '{entity_id}' is not directly downloadable" + if dl.reason: + msg += f": {dl.reason}" + return msg + + @check(name="Web-based Data Entity: REQUIRED resource availability") + def check_availability(self, context: ValidationContext) -> bool: + if ( + context.settings.skip_availability_check + or not (context.settings.creation_time or context.settings.enforce_availability) + or context.settings.metadata_only + ): + return True + result = True + for entity in context.ro_crate.metadata.get_web_data_entities(): + assert entity.id is not None, "Entity has no @id" + # Skip directory URIs: assumed available, not directly downloadable by spec + if entity.id.endswith("/"): + logger.debug("Skipping downloadability check for directory entity '%s'", entity.id) + continue + try: + dl = check_downloadable(entity.id) + if not dl.is_downloadable: + context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) + result = False + except Exception as e: + context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) + result = False + if not result and context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl new file mode 100644 index 000000000..ada5c742c --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl @@ -0,0 +1,221 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix owl: . +@prefix validator: . + +ro-crate:DataEntityRequiredProperties a sh:NodeShape ; + sh:name "Data Entity: REQUIRED properties" ; + sh:description """A Data Entity MUST be a `URI Path` relative to the ROCrate root, + or an absolute URI""" ; + sh:targetClass ro-crate:DataEntity ; + sh:property [ + sh:name "Data Entity: @id value restriction" ; + sh:description """Check if the Data Entity has an absolute or relative URI as `@id`""" ; + sh:path [ + sh:inversePath rdf:type + ] ; sh:nodeKind sh:IRI ; sh:severity sh:Violation ; sh:message """Data Entities MUST have an absolute or relative URI as @id.""" ; + ] . + +ro-crate:FileDataEntity a sh:NodeShape ; + sh:name "File Data Entity: REQUIRED properties" ; + sh:description """A File Data Entity MUST be a `File`. + `File` is an RO-Crate alias for the schema.org `MediaObject`. + The term `File` here is liberal, and includes "downloadable" resources where `@id` is an absolute URI. + """ ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:MediaObject . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + } + """ + ] ; + + # Irrelevant for validation, but useful for debugging + # sh:property [ + # sh:name "File Data Entity: REQUIRED type" ; + # sh:description """Check if the File Data Entity has `File` as `@type`. + # `File` is an RO-Crate alias for the schema.org `MediaObject`. + # """ ; + # sh:path rdf:type ; + # sh:or ( + # [ sh:hasValue ro-crate:File ] + # [ sh:hasValue schema_org:MediaObject ] + # ) ; + # sh:severity sh:Violation ; + # sh:message """File Data Entities MUST have "File" as a value for @type.""" ; + # ] ; + + # Expand data graph with triples to mark the matching entities as File instances + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:File ; + ] ; + # Expand data graph with triples from the file data entity + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:DataEntity ; + ] . + +ro-crate:DirectoryDataEntity a sh:NodeShape ; + sh:name "Directory Data Entity: REQUIRED properties" ; + sh:description """A Directory Data Entity MUST be of @type `Dataset`. + The term `directory` here includes HTTP file listings where `@id` is an absolute URI. + """ ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Dataset . + ?metadatafile schema:about ?root . + # Exclude all dataset entities that ends with `./#` + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?root) + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + } + """ + ] ; + + # Uncomment for debugging + # sh:property [ + # sh:name "Test Directory" ; + # sh:description """Data Entities representing directories MUST have "Directory" as a value for @type.""" ; + # sh:path rdf:type ; + # sh:hasValue ro-crate:File ; + # sh:severity sh:Violation ; + # ] ; + + # Expand data graph with triples to mark the matching entities as Directory instances + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:Directory ; + ] ; + + # Expand data graph with triples from the directory data entity + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:DataEntity ; + ] ; + + # Ensure that the directory data entity is a dataset + sh:property [ + sh:name "Directory Data Entity: REQUIRED type" ; + sh:description """Check if the Directory Data Entity has `Dataset` as `@type`.""" ; + sh:path rdf:type ; + sh:hasValue schema_org:Dataset ; + sh:severity sh:Violation ; + ] . + +ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; + sh:name "Data Entity: REQUIRED properties" ; + sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; + sh:targetClass ro-crate:DataEntity ; + sh:property + [ + a sh:PropertyShape ; + sh:path [ sh:oneOrMorePath [ sh:inversePath schema_org:hasPart ] ] ; + sh:qualifiedValueShape [ sh:class ro-crate:RootDataEntity ] ; + sh:qualifiedMinCount 1 ; + sh:name "Data Entity MUST be directly referenced" ; + sh:description """Check if the Data Entity is linked, either directly or indirectly, to the `Root Data Entity` using the `hasPart` (as defined in `schema.org`) property" """ ; + sh:message "A Data Entity MUST be directly or indirectly linked to the `Root Data Entity` through the `hasPart` property" ; + ] . + +ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; + sh:name "Generic Data Entity: REQUIRED properties" ; + sh:description """A Data Entity other than a File or a Directory MUST be a `DataEntity`""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root schema:hasPart ?this . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?root) + FILTER(?this != ?metadatafile) + FILTER NOT EXISTS { + ?this a schema:MediaObject . + ?this a schema:Dataset . + } + } + """ + ] ; + + # Expand data graph with triples to mark the matching entities as GenericDataEntity instances + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:GenericDataEntity ; + ] ; + + # Expand data graph with triples to mark the matching entities as DataEntity instances + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:DataEntity ; + ] . + +# Uncomment for debugging +# ro-crate:TestGenericDataEntity a sh:NodeShape ; +# sh:disabled true ; +# sh:targetClass ro-crate:GenericDataEntity ; +# sh:name "Generic Data Entity: test invalid property"; +# sh:description """Check if the GenericDataEntity has the invalidProperty property""" ; +# sh:property [ +# sh:minCount 1 ; +# sh:maxCount 1 ; +# sh:path ro-crate:invalidProperty ; +# sh:severity sh:Violation ; +# sh:message "Testing the generic data entity"; +# sh:datatype xsd:string ; +# sh:message "Testing for the invalidProperty of the generic data entity"; +# ] . + +# Uncomment for debugging +# ro:testDirectory a sh:NodeShape ; +# sh:name "Definition of Test Directory" ; +# sh:description """A Test Directory is a digital object that is stored in a file format""" ; +# sh:targetClass ro-crate:Directory ; + +# sh:property [ +# sh:name "Test Directory instance" ; +# sh:description """Check if the Directory DataEntity instance has the fake property ro-crate:foo""" ; +# sh:path rdf:type ; +# sh:hasValue ro-crate:foo ; +# sh:severity sh:Violation ; +# ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl new file mode 100644 index 000000000..db63676ad --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix dct: . +@prefix schema_org: . +@prefix sh: . +@prefix owl: . +@prefix xsd: . +@prefix validator: . + + +ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; + sh:name "Web-based Data Entity: REQUIRED properties" ; + sh:description """A Web-based Data Entity is a `File` identified by an absolute URL""" ; + + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:MediaObject . + FILTER(!contains(str(?this), "ro-crate-metadata.json")) + FILTER regex(str(?this), "^(https?|ftps?)://", "i") + } + """ + ] ; + + # Expand data graph with triples which identify the web-based data entity + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:WebDataEntity ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl new file mode 100644 index 000000000..80168a0b1 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl @@ -0,0 +1,213 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix owl: . +@prefix rdf: . +@prefix ro: <./> . +@prefix ro-crate: . +@prefix schema: . +@prefix schema_org: . +@prefix sh: . +@prefix validator: . +@prefix xsd: . + +ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Contextual Entities" ; + sh:description """Mark entities as Contextual Entities if they are in the RO-Crate + metadata but are not Data Entities, not the Root Data Entity, + and not the Metadata File Descriptor. The query uses `?this ?p ?o` + (subject of any triple) — SHACL rules from earlier shapes (e.g. + CreativeWorkAuthorDefinition) tag referenced-but-undescribed entities + with rdf:type, so they appear as subjects and are therefore captured. + Vocabulary namespaces loaded from ontologies (RDF/RDFS/OWL/XSD, + schema.org, bioschemas, w3id.org/ro, …) are excluded at the query + level; domain-specific legacy filters that wrongly excluded legitimate + crate identifiers (iana, urn, foaf, …) have been removed.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + # Structural criteria: IRI or blank node, subject of some triple. + # SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) + # tag even referenced-but-undescribed entities so they become + # subjects and are correctly classified as Contextual Entities. + ?this ?p ?o . + FILTER(isIRI(?this) || isBlank(?this)) + + # Structural exclusions — Metadata Descriptor, Root Data Entity, + # Data Entities (File/Dataset or ro-crate:DataEntity), Ontology. + FILTER NOT EXISTS { ?this schema:about ?anyRoot } + FILTER NOT EXISTS { ?anyMF schema:about ?this } + FILTER NOT EXISTS { ?this a ro-crate:DataEntity } + FILTER NOT EXISTS { ?this a schema:MediaObject } + FILTER NOT EXISTS { ?this a schema:Dataset } + FILTER NOT EXISTS { ?this a owl:Ontology } + + # Vocabulary / profile namespace exclusions. Required because + # the RO-Crate 1.2 profile loads ontology.ttl into the same data + # graph as the metadata file; terms from those vocabularies + # appear as subjects with rdf:type + rdfs:label and would + # otherwise be misclassified. + FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) + FILTER(!STRSTARTS(STR(?this), "https://www.w3.org/")) + FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) + FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) + FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) + FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) + FILTER(!STRSTARTS(STR(?this), "http://www.opengis.net/")) + FILTER(!STRSTARTS(STR(?this), "https://w3id.org/")) + FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) + FILTER(!STRSTARTS(STR(?this), "https://codemeta.github.io/")) + FILTER(!STRSTARTS(STR(?this), "http://pcdm.org/")) + FILTER(!STRSTARTS(STR(?this), "http://xmlns.com/foaf/")) + FILTER(!STRSTARTS(STR(?this), "http://creativecommons.org/ns")) + FILTER(!STRSTARTS(STR(?this), "http://www.iana.org/")) + FILTER(!STRSTARTS(STR(?this), "https://www.iana.org/")) + FILTER(!STRSTARTS(STR(?this), "https://nationaalarchief.nl/archieven/archief/")) + FILTER(!STRSTARTS(STR(?this), "https://www.nationalarchives.gov.uk")) + + # Narrow urn: exclusion — only the internal ontology versionIRI. + # Unlike the legacy blanket `urn:` filter, this keeps legitimate + # `urn:uuid:*` contextual entities (e.g. DigitalDocument) in play. + FILTER(!STRSTARTS(STR(?this), "urn:absolute:")) + } + """ + ] ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:ContextualEntity ; + ] . + +ro-crate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify License Entity" ; + sh:description """Mark a license entity any Data Entity referenced by the `schema:license` property that is defined in the RO-Crate metadata.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?subject schema:license ?this . + FILTER(isIRI(?this)) + # FILTER EXISTS { ?this ?p ?o . FILTER(?p NOT IN (owl:sameAs, rdf:type)) } + } + """ + ] ; + + # Expand data graph with triples to mark the matching entities as License instances + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:License ; + ] ; + + # Expand data graph with triples from the file data entity + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:ContextualEntity ; + ] . + +ro-crate:WebSiteRecommendedProperties a sh:NodeShape ; + sh:name "WebSite RECOMMENDED Properties" ; + sh:description """A `WebSite` MUST be identified by a valid IRI and MUST have a `name` property.""" ; + sh:targetClass schema:WebSite ; + sh:property [ + sh:path [ + sh:inversePath rdf:type + ] ; sh:datType sh:IRI ; sh:name "WebSite: value restriction of its identifier" ; sh:description "Check if the WebSite has a valid IRI" ; sh:message "A WebSite MUST have a valid IRI" ; + ] ; sh:property [ + sh:path schema:name ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:name "WebSite: REQUIRED `name` property" ; + sh:description "Check if the WebSite has a `name` property" ; + sh:message "A WebSite MUST have a `name` property" ; + ] . + +ro-crate:CreativeWorkAuthorDefinition a sh:NodeShape, validator:HiddenShape ; + sh:name "CreativeWork Author Definition" ; + sh:description """Define the `CreativeWorkAuthor` as the `Person` object of the `schema:author` predicate.""" ; + sh:targetObjectsOf schema:author ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:CreativeWorkAuthor ; + sh:condition [ + sh:property [ + sh:path rdf:type ; sh:hasValue schema:Person ; sh:minCount 1 + ] ;] ;] . + +ro-crate:ThumbnailReferencesFile a sh:NodeShape ; + sh:name "Thumbnail reference" ; + sh:description """If `thumbnail` is present, it MUST reference a File data entity.""" ; + sh:targetSubjectsOf schema:thumbnail ; + sh:property [ + sh:path schema:thumbnail ; + sh:class schema:MediaObject ; + sh:message "If present, `thumbnail` MUST reference a File data entity" ; + ] . + +# A contextual entity representing a `ComputerLanguage` and/or `SoftwareApplication` +# MUST have `name`, `url`, and `version`. +ro-crate:SoftwareApplicationOrComputerLanguageRequiredProperties a sh:NodeShape ; + sh:name "SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`" ; + sh:description """A contextual entity representing a `SoftwareApplication` + or a `ComputerLanguage` MUST declare `name`, `url`, and `version` + properties (RO-Crate 1.2, Provenance — Software Applications; + Workflows — programmingLanguage).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a ?t . + FILTER(?t IN (schema:SoftwareApplication, schema:ComputerLanguage)) + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "SoftwareApplication/ComputerLanguage: REQUIRED `name`" ; + sh:description "Check that the entity declares a `name` property." ; + sh:path schema:name ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A SoftwareApplication or ComputerLanguage MUST have a `name` property" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "SoftwareApplication/ComputerLanguage: REQUIRED `url`" ; + sh:description "Check that the entity declares a `url` property." ; + sh:path schema:url ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A SoftwareApplication or ComputerLanguage MUST have a `url` property" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "SoftwareApplication/ComputerLanguage: REQUIRED `version`" ; + sh:description "Check that the entity declares a `version` property indicating a known release." ; + sh:path schema:version ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A SoftwareApplication or ComputerLanguage MUST have a `version` property (SHOULD indicate a known version that linked workflows/scripts were developed/tested with)" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl new file mode 100644 index 000000000..3e1aa66e1 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl @@ -0,0 +1,175 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix xsd: . +@prefix validator: . + +# Selects every SoftwareSourceCode entity (Scripts and Workflows). +ro-crate:SoftwareSourceCodeTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:SoftwareSourceCode . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } + """ . + +# --------------------------------------------------------------- +# Identify Script entities +# +# A Script is an entity with schema:SoftwareSourceCode in @type +# that is NOT a Workflow (no bioschemas:ComputationalWorkflow). +# --------------------------------------------------------------- +ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Script entities" ; + sh:description """Mark entities as Scripts if they have schema:SoftwareSourceCode + in @type but NOT bioschemas:ComputationalWorkflow.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:SoftwareSourceCode . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER NOT EXISTS { ?this a bioschemas:ComputationalWorkflow } + } + """ + ] ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:Script ; + ] . + +# --------------------------------------------------------------- +# Identify Workflow entities +# +# A Workflow is an entity with bioschemas:ComputationalWorkflow +# in @type. +# --------------------------------------------------------------- +ro-crate:WorkflowDefinition a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Workflow entities" ; + sh:description """Mark entities as Workflows if they have + bioschemas:ComputationalWorkflow in @type.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a bioschemas:ComputationalWorkflow . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } + """ + ] ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:Workflow ; + ] . + +# --------------------------------------------------------------- +# Script: MUST include `File` (schema:MediaObject) in @type +# --------------------------------------------------------------- +ro-crate:ScriptRequiredFileType a sh:NodeShape ; + sh:name "Script: REQUIRED `File` type" ; + sh:description """A Script entity MUST include `File` (schema:MediaObject) + in its @type alongside `SoftwareSourceCode` + (RO-Crate 1.2, Workflows and Scripts — Script).""" ; + sh:targetClass ro-crate:Script ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script: REQUIRED `File` type" ; + sh:description """Check that the Script entity includes `File` + (schema:MediaObject) in its @type.""" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A Script MUST include `File` in its `@type` (schema:MediaObject / schema:SoftwareSourceCode)" ; + ] . + +# --------------------------------------------------------------- +# Workflow: MUST include `File` (schema:MediaObject) in @type +# --------------------------------------------------------------- +ro-crate:WorkflowRequiredFileType a sh:NodeShape ; + sh:name "Workflow: REQUIRED `File` type" ; + sh:description """A Workflow entity MUST include `File` (schema:MediaObject) + in its @type alongside `SoftwareSourceCode` and `ComputationalWorkflow` + (RO-Crate 1.2, Workflows and Scripts — Workflow).""" ; + sh:targetClass ro-crate:Workflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Workflow: REQUIRED `File` type" ; + sh:description """Check that the Workflow entity includes `File` + (schema:MediaObject) in its @type.""" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A Workflow MUST include `File` in its `@type` (schema:MediaObject / schema:SoftwareSourceCode / bioschemas:ComputationalWorkflow)" ; + ] . + +# --------------------------------------------------------------- +# Workflow: MUST include `SoftwareSourceCode` in @type +# --------------------------------------------------------------- +ro-crate:WorkflowRequiredSoftwareSourceCodeType a sh:NodeShape ; + sh:name "Workflow: REQUIRED `SoftwareSourceCode` type" ; + sh:description """A Workflow entity MUST include `SoftwareSourceCode` + in its @type alongside `File` and `ComputationalWorkflow` + (RO-Crate 1.2, Workflows and Scripts — Workflow).""" ; + sh:targetClass ro-crate:Workflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Workflow: REQUIRED `SoftwareSourceCode` type" ; + sh:description """Check that the Workflow entity includes + `SoftwareSourceCode` in its @type.""" ; + sh:path rdf:type ; + sh:hasValue schema:SoftwareSourceCode ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A Workflow MUST include `SoftwareSourceCode` in its `@type` (schema:MediaObject / schema:SoftwareSourceCode / bioschemas:ComputationalWorkflow)" ; + ] . + +# --------------------------------------------------------------- +# Script or Workflow: MUST have a `name` property +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowRequiredName a sh:NodeShape ; + sh:name "Script or Workflow: REQUIRED `name`" ; + sh:description """Scripts and Workflows MUST have a human-readable `name` + property (RO-Crate 1.2, Workflows and Scripts).""" ; + sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script or Workflow: REQUIRED `name` property" ; + sh:description """Check that the Script or Workflow entity has a + human-readable `name` property.""" ; + sh:path schema:name ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:severity sh:Violation ; + sh:message "Scripts and Workflows MUST have a human-readable `name` property" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl new file mode 100644 index 000000000..84331ccc7 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl @@ -0,0 +1,84 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@prefix schema: . +@prefix ro-crate: . +@prefix bioschemas: . +# @base <./.> . + + rdf:type owl:Ontology ; + owl:versionIRI . + +# # ################################################################# +# # # Classes +# # ################################################################# + +# Declare the RootDataEntity class +ro-crate:RootDataEntity rdf:type owl:Class ; + rdfs:subClassOf schema:Dataset ; + rdfs:label "RootDataEntity"@en . + +### http://schema.org/CreativeWork +schema:CreativeWork rdf:type owl:Class ; + rdfs:label "CreativeWork"@en . + +### http://schema.org/MediaObject +schema:MediaObject rdf:type owl:Class ; + owl:equivalentClass ro-crate:File ; + rdfs:label "MediaObject"@en . + + +### http://schema.org/SoftwareSourceCode +schema:SoftwareSourceCode rdf:type owl:Class ; + rdfs:subClassOf schema:CreativeWork . + + +### https://bioschemas.org/ComputationalWorkflow +bioschemas:ComputationalWorkflow rdf:type owl:Class . + + +### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Script +ro-crate:Script rdf:type owl:Class ; + rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject ; + rdfs:label "Script"@en . + + +### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Workflow +ro-crate:Workflow rdf:type owl:Class ; + rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject, bioschemas:ComputationalWorkflow ; + rdfs:label "Workflow"@en . + + +### https://w3id.org/ro/crate/1.2/DataEntity +ro-crate:DataEntity rdf:type owl:Class ; + rdfs:subClassOf schema:CreativeWork ; + rdfs:label "DataEntity"@en . + + +# # ### https://w3id.org/ro/crate/1.2/Directory +ro-crate:Directory rdf:type owl:Class ; + rdfs:subClassOf schema:Dataset ; + rdfs:label "Directory"@en . + + +### https://w3id.org/ro/crate/1.2/ContextualEntity +ro-crate:ContextualEntity rdf:type owl:Class ; + rdfs:subClassOf schema:Thing ; + rdfs:label "ContextualEntity"@en . diff --git a/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl b/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl new file mode 100644 index 000000000..8cad6b4dd --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl @@ -0,0 +1,59 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix sh: . +@prefix xsd: . +@prefix ro-crate: . + +# Define the prefixes used in the SPARQL queries +ro-crate:sparqlPrefixes sh:declare [ + sh:prefix "schema" ; + sh:namespace "http://schema.org/"^^xsd:anyURI ; +] ; + sh:declare [ + sh:prefix "bioschemas" ; + sh:namespace "https://bioschemas.org/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "bioschemas-cw" ; + sh:namespace "https://bioschemas.org/ComputationalWorkflow#"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "rocrate" ; + sh:namespace "https://w3id.org/ro/crate/1.2/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "wfrun" ; + sh:namespace "https://w3id.org/ro/terms/workflow-run#"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "codemeta" ; + sh:namespace "https://codemeta.github.io/terms/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "iana-relation" ; + sh:namespace "http://www.iana.org/assignments/relation/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "ro-crate" ; + sh:namespace "https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "dct" ; + sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "ro" ; + sh:namespace "./"^^xsd:anyURI ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/profile.ttl b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl new file mode 100644 index 000000000..b87a59bfe --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl @@ -0,0 +1,74 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + # a Profile; it's identifying URI + a prof:Profile ; + + # common metadata for the Profile + + # the Profile's label + rdfs:label "RO-Crate Metadata Specification 1.2" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """RO-Crate Metadata Specification."""@en ; + + # regular metadata, URI of publisher + dct:publisher ; + + # this profile has a JSON-LD context resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in JSON-LD format + dct:format ; + + # it conforms to JSON-LD, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Vocabulary" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Vocabulary ; + + # this profile resource's actual file + prof:hasArtifact ; + ] ; + + # this profile has a human-readable documentation resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in HTML format + dct:format ; + + # it conforms to HTML, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Specification" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Specification ; + + # this profile resource's actual file + prof:hasArtifact ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "ro-crate" ; +. diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py new file mode 100644 index 000000000..6f9d8745b --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py @@ -0,0 +1,134 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +RECOMMENDED checks on entity @id values: + - @id SHOULD NOT use ../ to climb out of the RO-Crate Root + - International characters SHOULD be native UTF-8, not percent-encoded + - Contextual entities SHOULD use absolute URI (permalink) or #-prefixed @id +""" + +import contextlib +import re + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +logger = logging.getLogger(__name__) + +# Matches any %XX sequence where XX decodes to a non-ASCII byte (0x80-0xFF). +# These are UTF-8 continuation / leading bytes for multi-byte code-points. +_PCT_NON_ASCII_RE = re.compile(r"%[89A-Fa-f][0-9A-Fa-f]") + +# RFC 3986 absolute URI scheme prefix: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) +# e.g. http:, https:, mailto:, urn:, doi:, ftp:, file: +_ABSOLUTE_URI_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+\-.]*:") + + +@requirement(name="Entity identifier: format recommendations") +class EntityIdentifierFormatChecker(PyFunctionCheck): + """ + Checks that entity @id values follow RO-Crate 1.2 RECOMMENDED conventions: + no parent-directory traversal, native UTF-8 for international characters, + and '#'-prefixed identifiers for named local entities. + """ + + @check(name="Entity identifiers SHOULD NOT use ../", severity=Severity.RECOMMENDED) + def check_no_parent_traversal(self, context: ValidationContext) -> bool: + """ + @id paths SHOULD NOT use `../` to climb out of the RO-Crate Root + (RO-Crate 1.2, JSON-LD appendix). + """ + result = True + for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + entity_id = entity.get("@id", "") + if "../" in entity_id: + context.result.add_issue( + f"Entity @id '{entity_id}' uses '../' to traverse above the " + f"RO-Crate Root; @id paths SHOULD NOT contain '../'", + self, + ) + result = False + if context.fail_fast: + return result + return result + + @check( + name="Entity identifiers SHOULD use native UTF-8, not percent-encoding", + severity=Severity.RECOMMENDED, + ) + def check_utf8_identifiers(self, context: ValidationContext) -> bool: + """ + International characters in @id values SHOULD be written in native + UTF-8 rather than percent-encoded (RO-Crate 1.2, JSON-LD appendix). + """ + result = True + for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + entity_id = entity.get("@id", "") + if _PCT_NON_ASCII_RE.search(entity_id): + context.result.add_issue( + f"Entity @id '{entity_id}' contains percent-encoded non-ASCII " + f"characters; international characters SHOULD be written in " + f"native UTF-8 rather than percent-encoded", + self, + ) + result = False + if context.fail_fast: + return result + return result + + @check( + name="Contextual entity @id SHOULD be absolute URI or '#'-prefixed", + severity=Severity.RECOMMENDED, + ) + def check_named_entity_id_format(self, context: ValidationContext) -> bool: + """ + Any Contextual Entity (Person, Organization, ContactPoint, PropertyValue, + Place, etc.) SHOULD use an @id that is an absolute URI (permalink), + a '#'-prefixed local identifier, or a blank node — not a bare relative + path (RO-Crate 1.2, 5.1 Any Contextual Entity). + """ + result = True + ro_crate_metadata = context.ro_crate.metadata + non_contextual_ids = set() + with contextlib.suppress(Exception): + non_contextual_ids.add(ro_crate_metadata.get_file_descriptor_entity().id) + with contextlib.suppress(Exception): + non_contextual_ids.add(ro_crate_metadata.get_root_data_entity().id) + with contextlib.suppress(Exception): + non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) + + for entity in ro_crate_metadata.as_dict().get("@graph", []): + entity_id = entity.get("@id", "") + if entity_id in non_contextual_ids: + continue + raw_type = entity.get("@type") + if not raw_type: + # Reference-only stub (no @type) — out of scope for this check + continue + # Absolute URIs (any scheme), fragment-prefixed IDs, and blank nodes + # are all valid; only flag bare relative paths + if entity_id.startswith(("#", "_:")) or _ABSOLUTE_URI_RE.match(entity_id): + continue + context.result.add_issue( + f"Entity @id '{entity_id}' of type '{raw_type}' is a local identifier " + f"that does not start with '#'; named local entities SHOULD use a " + f"'#'-prefixed @id (e.g. '#alice') or an absolute URI (permalink)", + self, + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl new file mode 100644 index 000000000..007c0f5cb --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl @@ -0,0 +1,75 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix sh: . +@prefix schema: . +@prefix xsd: . + +ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; + sh:name "RO-Crate Metadata Entity: RECOMMENDED properties" ; + sh:description """Check if the RO-Crate Metadata Entity includes at least one Schema.org type and a human-readable name""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a ?type . + + # Exclude Root and RO-Crate Metadata File entities + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?metadatafile) + FILTER (?this != ?root) + # Exclude entities with non-IRI identifiers or those from specific namespaces + FILTER (isIRI(?this)) + FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) + FILTER(!STRSTARTS(STR(?this), "https://w3id.org/ro/crate/")) + FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) + FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) + FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) + FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) + FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) + FILTER(!STRSTARTS(STR(?this), "urn:")) + } + """ ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:name "RO-Crate Metadata Entity: RECOMMENDED type" ; + sh:description """Check if the RO-Crate Metadata Entity includes at least one Schema.org type""" ; + sh:message "RO-Crate Metadata Entity SHOULD include at least one Schema.org type" ; + sh:select """ + SELECT ?this + WHERE { + FILTER NOT EXISTS { + $this a ?schemaType . + FILTER ( + STRSTARTS(STR(?schemaType), "http://schema.org/") || + STRSTARTS(STR(?schemaType), "https://schema.org/") + ) + } + } + """ ; + ] ; + sh:property [ + sh:name "RO-Crate Metadata Entity: name" ; + sh:description """Check if the RO-Crate Metadata Entity includes a human-readable name""" ; + sh:path schema:name ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:severity sh:Warning ; + sh:message "Entities SHOULD have a human-readable name" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl new file mode 100644 index 000000000..d6d7e42bf --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl @@ -0,0 +1,56 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix dct: . +@prefix sh: . + +ro-crate:ROCrateMetadataFileDescriptorConformsTo + a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor RECOMMENDED conformsTo" ; + sh:description """The RO-Crate Metadata Descriptor SHOULD indicate the versioned RO-Crate specification URI. + In RO-Crate 1.2, conformsTo SHOULD have a single value which is a versioned permalink URI + of the RO-Crate specification that the RO-Crate JSON-LD conforms to. + The URI SHOULD start with https://w3id.org/ro/crate/.""" ; + sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Metadata File Descriptor: RECOMMENDED `conformsTo` value" ; + sh:description """Check if the RO-Crate Metadata File Descriptor has a versioned RO-Crate specification URI""" ; + sh:severity sh:Warning ; + sh:nodeKind sh:IRI ; + sh:path dct:conformsTo ; + sh:hasValue ; + sh:message "The RO-Crate metadata file descriptor SHOULD have a `conformsTo` property with the RO-Crate specification version" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor: RECOMMENDED single `conformsTo` value" ; + sh:description """Check if the RO-Crate Metadata File Descriptor has a single `conformsTo` value""" ; + sh:severity sh:Warning ; + sh:path dct:conformsTo ; + sh:maxCount 1 ; + sh:message "The RO-Crate metadata file descriptor SHOULD have a single `conformsTo` value" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor: RECOMMENDED `conformsTo` URI prefix" ; + sh:description """Check if the `conformsTo` URI starts with https://w3id.org/ro/crate/""" ; + sh:severity sh:Warning ; + sh:path dct:conformsTo ; + sh:pattern "^https://w3id\\.org/ro/crate/" ; + sh:message "The RO-Crate metadata file descriptor `conformsTo` URI SHOULD start with https://w3id.org/ro/crate/" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py new file mode 100644 index 000000000..e7034f3d1 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="File Descriptor naming convention") +class FileDescriptorExistence(PyFunctionCheck): + """ + If stored in a file, SHOULD be named ${prefix}-ro-crate-metadata.json, where the variable ${prefix} + is a human readable version of the dataset's ID or name. + """ + + @check(name="Detached RO-Crate file descriptor RECOMMENDED naming convention") + def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: + """ + Check if the file descriptor of a Detached RO-Crate exists and is named according to the convention. + In a Detached RO-Crate, the file descriptor SHOULD be named `{prefix}-ro-crate-metadata.json`, + where `{prefix}` is a human readable version of the dataset's ID or name. + """ + if context.settings.metadata_only: + logger.debug("Skipping file descriptor existence check in metadata-only mode") + return True + if not context.ro_crate.has_descriptor(): + message = f'file descriptor "{context.rel_fd_path}" is not present' + context.result.add_issue(message, self) + return False + if context.ro_crate.is_detached(): + # Check if the filename follows the convention + fd_filename = context.ro_crate.get_descriptor_path() + if fd_filename and not ( + fd_filename.name.endswith("-ro-crate-metadata.json") + or fd_filename.name.endswith("-ro-crate-metadata.yaml") + ): + context.result.add_issue( + "In a detached RO-Crate, the metadata descriptor filename " + "SHOULD be named according to the convention `{prefix}-ro-crate-metadata.json` ", + self, + ) + return False + return True diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl new file mode 100644 index 000000000..344bafc5b --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl @@ -0,0 +1,47 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix schema_org: . +@prefix dct: . +@prefix prof: . +@prefix sh: . + +# Root Data Entity: conformsTo SHOULD be present if profiles exist in the crate +ro-crate:RootDataEntityConformsToIfProfiles a sh:NodeShape ; + sh:name "Root Data Entity: conformsTo SHOULD be present if profiles exist" ; + sh:description """If the RO-Crate conforms to additional profiles, + the Root Data Entity SHOULD have a conformsTo property referencing + Profile entities (RO-Crate 1.2).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a ro-crate:RootDataEntity . + FILTER EXISTS { + ?profile a prof:Profile . + FILTER(?profile != ) + } + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: RECOMMENDED conformsTo if profiles exist" ; + sh:path dct:conformsTo ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "The Root Data Entity SHOULD have a `conformsTo` property if the RO-Crate conforms to profiles" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl new file mode 100644 index 000000000..25d708a93 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl @@ -0,0 +1,94 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . + +ro-crate:RootDataEntityRecommendedFunder a sh:NodeShape ; + sh:name "Root Data Entity: recommended funder" ; + sh:description """If the RO-Crate describes a research project, grant, or + monetary grant (via a `schema:Project`, `schema:Grant`, or + `schema:MonetaryGrant` entity in the @graph), the Root Data Entity + SHOULD reference the funding source via the `funder` property + (RO-Crate 1.2, Funding and Grants).""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Root Data Entity: RECOMMENDED `funder` property (conditional)" ; + sh:description """Fire a warning only if the crate contains project or + grant entities AND the Root Data Entity has no `funder` property.""" ; + sh:select """ + SELECT $this + WHERE { + # Trigger: at least one project/grant entity is present. + ?project a ?projectType . + FILTER(?projectType IN (schema:Project, schema:Grant, schema:MonetaryGrant)) + # Violation condition: the Root has no `funder` property. + FILTER NOT EXISTS { $this schema:funder ?f } + } + """ ; + sh:severity sh:Warning ; + sh:message "The Root Data Entity SHOULD reference funders directly via the `funder` property when the crate describes a research project, grant, or monetary grant" ; + ] . + +# funder values SHOULD be Organization entities; +# Organization funder entities SHOULD themselves reference an external funder. +ro-crate:FunderEntityRecommendedType a sh:NodeShape ; + sh:name "Funder entity: recommended Organization type" ; + sh:description """Entities referenced via `funder` from the Root Data Entity SHOULD be of + type Organization (RO-Crate 1.2, Funding and Grants — K1/K2).""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Funder entity: RECOMMENDED `Organization` type" ; + sh:description """Check if funder entities referenced from the Root Data Entity + are of type Organization.""" ; + sh:path schema_org:funder ; + sh:class schema_org:Organization ; + sh:severity sh:Warning ; + sh:message "Funder entities referenced from the Root Data Entity SHOULD be of type `Organization`" ; + ] . + +# An Organization that is referenced as funder from Root SHOULD itself +# carry a `funder` property pointing to an external funding body. +ro-crate:ProjectOrganizationRecommendedFunder a sh:NodeShape ; + sh:name "Project Organization: recommended funder reference" ; + sh:description """An Organization entity referenced via `funder` from the Root Data Entity + SHOULD itself reference the external funding body via its own `funder` property + (RO-Crate 1.2, Funding and Grants — K2).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root a ro-crate:RootDataEntity . + ?root schema:funder ?this . + ?this a schema:Organization . + FILTER(!regex(str(?this), "^https?://", "i")) + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Project Organization: RECOMMENDED `funder` property" ; + sh:description """Check if an Organization referenced as funder from the Root Data Entity + itself references an external funding body via `funder`.""" ; + sh:path schema_org:funder ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "An Organization referenced as `funder` from the Root Data Entity SHOULD itself reference an external `funder`" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py new file mode 100644 index 000000000..c957b475a --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py @@ -0,0 +1,174 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.signposting import check_downloadable +from rocrate_validator.utils.uri import URI + +logger = logging.getLogger(__name__) + + +@requirement(name="Root Data Entity: RECOMMENDED identifier") +class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): + """ + In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. + """ + + @check(name="Root Data Entity: RECOMMENDED identifier") + def check_identifier(self, context: ValidationContext) -> bool: + """ + In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. + """ + try: + if not context.ro_crate.is_detached(): + return True + root_entity = context.ro_crate.metadata.get_root_data_entity() + if not root_entity.is_remote(): + return True + + if root_entity.id == "./": + context.result.add_issue( + "In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL, not `./`", self + ) + return False + if not URI(root_entity.id).is_remote_resource(): + context.result.add_issue( + "In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL", self + ) + return False + + return True + except Exception as e: + context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) + return False + + +@requirement(name="Root Data Entity: use cite-as for resolvable identifiers") +class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): + """ + If the Root Data Entity has a resolvable identifier, + it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. + """ + + @check(name="Root Data Entity: use cite-as for resolvable identifiers") + def check_cite_as_reference(self, context: ValidationContext) -> bool: + """ + If the Root Data Entity has a resolvable identifier, + it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. + """ + try: + if not context.ro_crate.is_detached(): + return True + root_entity = context.ro_crate.metadata.get_root_data_entity() + if not root_entity.is_remote(): + return True + + # Check if the `cite-as` property is present and references the Root Data Entity + cite_as = root_entity.get_property("cite-as") + if root_entity.id_as_uri.is_remote_resource() and not cite_as: + context.result.add_issue( + "If the Root Data Entity has a resolvable identifier, " + "it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity.", + self, + ) + return False + + # If the `cite-as` property is present, check that it references the Root Data Entity + if cite_as.id != root_entity.id: + context.result.add_issue( + "If the Root Data Entity has a resolvable identifier, " + "it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity.", + self, + ) + return False + + return True + except Exception as e: + context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) + return False + + +def _extract_identifier_urls(identifier_raw) -> list[str]: + """ + Extract HTTP URL(s) from a raw ``identifier`` property value. + + The identifier can be: + - A plain string URL + - A ``schema:PropertyValue`` entity with a ``url`` or ``value`` sub-property + - A list of any of the above + """ + urls = [] + items = identifier_raw if isinstance(identifier_raw, list) else [identifier_raw] + for item in items: + if isinstance(item, str): + if item.startswith("http"): + urls.append(item) + elif hasattr(item, "get_property"): + # PropertyValue: try url first, then value + for prop in ("url", "value"): + val = item.get_property(prop) + if val: + url = val if isinstance(val, str) else val.id if hasattr(val, "id") else None + if url and url.startswith("http"): + urls.append(url) + break + elif hasattr(item, "id") and item.id and item.id.startswith("http"): + urls.append(item.id) + return urls + + +@requirement(name="Root Data Entity: persistent identifier resolution") +class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): + """ + If the Root Data Entity has an ``identifier`` property with a resolvable + HTTP URL, resolving that URL SHOULD ultimately provide the RO-Crate Metadata + Document or an archive, accessible via Signposting or content negotiation + (RO-Crate 1.2, RECOMMENDED). + """ + + @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) + def check_identifier_resolvable(self, context: ValidationContext) -> bool: + if context.settings.skip_availability_check: + return True + if context.settings.metadata_only: + return True + try: + root_entity = context.ro_crate.metadata.get_root_data_entity() + identifier_raw = root_entity.get_property("identifier") + if not identifier_raw: + return True + + urls = _extract_identifier_urls(identifier_raw) + if not urls: + return True + + result = True + for url in urls: + dl = check_downloadable(url) + if not dl.is_downloadable: + msg = ( + f"The Root Data Entity identifier '{url}' SHOULD resolve to the " + f"RO-Crate Metadata Document or an archive via Signposting or " + f"content negotiation" + ) + if dl.reason: + msg += f": {dl.reason}" + context.result.add_issue(msg, self) + result = False + return result + except Exception as e: + context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl new file mode 100644 index 000000000..115aa861d --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl @@ -0,0 +1,65 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix schema_org: . +@prefix sh: . +@prefix xsd: . + +# Root Data Entity: identifier SHOULD be present if its @id is an absolute URI +ro-crate:RootDataEntityIdentifierIfPid a sh:NodeShape ; + sh:name "Root Data Entity: identifier SHOULD be present if PID exists" ; + sh:description """If the Root Data Entity has a persistent identifier + (e.g., its @id is an absolute URI), it SHOULD include an identifier property.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a ro-crate:RootDataEntity . + FILTER(regex(str(?this), "^https?://", "i")) + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: RECOMMENDED identifier if persistent ID exists" ; + sh:path schema_org:identifier ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "The Root Data Entity SHOULD have an `identifier` property if it has a persistent identifier" ; + ] . + +# Root Data Entity: identifier SHOULD use PropertyValue approach (Science On Schema.org) +# This shape only fires when an identifier IS present but is not a PropertyValue entity. +ro-crate:RootDataEntityIdentifierPropertyValueApproach a sh:NodeShape ; + sh:name "Root Data Entity: identifier SHOULD use PropertyValue approach" ; + sh:description """If the Root Data Entity has an identifier, it SHOULD use + the PropertyValue approach as per Science On Schema.org guides.""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Root Data Entity: RECOMMENDED PropertyValue approach for identifier" ; + sh:description """Check that if an identifier is present it is a PropertyValue entity.""" ; + sh:select """ + SELECT ?this + WHERE { + ?this schema:identifier ?id . + FILTER NOT EXISTS { ?id a schema:PropertyValue } + } + """ ; + sh:severity sh:Warning ; + sh:message "The Root Data Entity SHOULD use PropertyValue entities for identifiers (Science On Schema.org approach)" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl new file mode 100644 index 000000000..986d71d47 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl @@ -0,0 +1,30 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix schema_org: . +@prefix sh: . + +ro-crate:RootDataEntityDatePublishedDayPrecision a sh:NodeShape ; + sh:name "Root Data Entity: datePublished SHOULD specify at least day precision" ; + sh:description """The Root Data Entity SHOULD specify datePublished to at least + the precision of a day (YYYY-MM-DD) per RO-Crate 1.2.""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: RECOMMENDED datePublished day precision" ; + sh:path schema_org:datePublished ; + sh:pattern "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])" ; + sh:severity sh:Warning ; + sh:message "The Root Data Entity SHOULD specify datePublished to at least the precision of a day (YYYY-MM-DD)" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl new file mode 100644 index 000000000..baa88dbc9 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl @@ -0,0 +1,36 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . + +ro-crate:RootDataEntityRecommendedPublisher a sh:NodeShape ; + sh:name "Root Data Entity: recommended publisher" ; + sh:description """The Root Data Entity SHOULD have a `publisher` property + referencing an Organization or Person (as per schema.org).""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: RECOMMENDED `publisher` property" ; + sh:description """Check if the Root Data Entity has a `publisher` property. + The RO-Crate specification recommends that the Root Data Entity includes + a publisher (Organization or Person) to describe who is responsible for + making the RO-Crate available.""" ; + sh:path schema_org:publisher ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "The Root Data Entity SHOULD have a `publisher` property (Organization or Person)" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl new file mode 100644 index 000000000..37a94d76c --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix schema_org: . +@prefix sh: . + +ro-crate:PreviewFilesNotInHasPart a sh:NodeShape ; + sh:name "Preview file descriptor should not be included in `hasPart`" ; + sh:description """`ro-crate-preview.html` and `ro-crate-preview_files/` SHOULD NOT be included in `hasPart`.""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + sh:path schema_org:hasPart ; + sh:nodeKind sh:IRI ; + sh:description "Check that `ro-crate-preview.html` and `ro-crate-preview_files/` are not included in `hasPart`" ; + sh:message "RO-Crate Website files SHOULD NOT be included in `hasPart`" ; + sh:pattern "^(?!.*(ro-crate-preview\\.html|ro-crate-preview_files/)).*$" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py new file mode 100644 index 000000000..2ab81a7bc --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py @@ -0,0 +1,142 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ================================================================== +# NOTE +# ================================================================== +# +# The RO-Crate 1.2 specification (section on Licensing) states: +# +# "Data Entities with different license SHOULD have own license property" +# (requirement L4 / P2.2) +# +# This requirement is inherently non-actionable in its strict formulation +# because there is no way for a validator to determine whether a Data +# Entity's content *should* be under a different license than the Root +# Data Entity's. The validator can only observe three states: +# +# 1. A Data Entity declares a `license` identical to the Root's. +# This is redundant (the entity already inherits the Root license) +# but not incorrect. +# +# 2. A Data Entity declares a `license` different from the Root's. +# This is the case the spec explicitly encourages. +# +# 3. A Data Entity does not declare `license`. It inherits the Root +# license, which is the default and perfectly acceptable. +# +# While this cross-entity comparison *could* be expressed as a SHACL +# shape using a SPARQL constraint, doing so would produce a validation +# failure that affects the overall validation result. The intent here +# is purely advisory: the redundant declaration is not an error, it is +# a style improvement suggestion that should not cause the crate to +# fail validation. By implementing this as a Python PyFunctionCheck +# that only logs a warning (without adding a validation issue), the +# check provides actionable feedback to authors without penalising +# valid crates. + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +logger = logging.getLogger(__name__) + + +def _resolve_license_id(license_value) -> str: + """ + Resolve a license property value to its @id string. + + The `license` property may be: + - a string (e.g. "https://creativecommons.org/licenses/by/4.0/") + - a dict with @id (e.g. {"@id": "https://creativecommons.org/licenses/by/4.0/"}) + - an ROCrateEntity object (which has an .id attribute) + - a list containing any of the above + - None + + Returns the @id string, or the string value, or empty string if + unresolvable. + """ + if license_value is None: + return "" + if isinstance(license_value, list): + items = [_resolve_license_id(item) for item in license_value] + return ",".join(item for item in items if item) + if isinstance(license_value, str): + return license_value + if hasattr(license_value, "id"): + return license_value.id + if isinstance(license_value, dict): + return license_value.get("@id", "") + return str(license_value) + + +@requirement(name="Data Entity: SHOULD NOT redundantly declare the Root license") +class DataEntityLicenseDivergenceChecker(PyFunctionCheck): + """ + Data Entities that declare a ``license`` property identical to the + Root Data Entity's license are redundantly overriding the inherited + license. The declaration is not incorrect, but it is unnecessary: + by default, all Data Entities inherit the Root Data Entity's license. + + This check logs a warning for each such redundant declaration, + suggesting that the property can be removed. It does **not** add + a validation issue, so the crate still passes validation. + + Data Entities with a *different* license from the Root are explicitly + encouraged by the spec and produce no warning. Data Entities with + no ``license`` property also produce no warning — they correctly + inherit the Root license. + """ + + @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) + def check_license_divergence(self, context: ValidationContext) -> bool: + root_entity = None + try: + root_entity = context.ro_crate.metadata.get_root_data_entity() + except Exception: + return True + if root_entity is None: + return True + + root_license_raw = root_entity.get_property("license") + if root_license_raw is None: + return True + + root_license_id = _resolve_license_id(root_license_raw) + if not root_license_id: + return True + + for entity in context.ro_crate.metadata.get_data_entities(): + if entity.id == root_entity.id: + continue + entity_license_raw = entity.get_property("license") + if entity_license_raw is None: + continue + + entity_license_id = _resolve_license_id(entity_license_raw) + if not entity_license_id: + continue + + if entity_license_id == root_license_id: + logger.warning( + "Data Entity '%s' declares a `license` property that is " + "identical to the Root Data Entity's license ('%s'); " + "this is redundant because Data Entities inherit the Root " + "license by default. Remove the `license` property or " + "change it to a different license if the content requires " + "one.", + entity.id, + root_license_id, + ) + return True diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py new file mode 100644 index 000000000..713cf47fe --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py @@ -0,0 +1,177 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.signposting import check_downloadable + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Data Entity: citation references") +class DataEntityCitationChecker(PyFunctionCheck): + """ + Citation references must include an absolute URI. + """ + + @check(name="Data Entity: citation must include @id") + def check_citation(self, context: ValidationContext) -> bool: + result = True + for entity in context.ro_crate.metadata.get_data_entities(): + citations = entity.get_property("citation") + if citations is None: + continue + citation_list = citations if isinstance(citations, list) else [citations] + for citation in citation_list: + if isinstance(citation, str): + citation_id = citation + elif hasattr(citation, "id"): + citation_id = citation.id + else: + context.result.add_issue( + f"Citation for Data Entity '{entity.id}' must reference a publication @id", self + ) + result = False + if context.fail_fast: + return False + continue + if not re.match(r"^[A-Za-z][A-Za-z0-9+\.-]*:", citation_id): + context.result.add_issue(f"Citation for Data Entity '{entity.id}' must be an absolute URI", self) + result = False + if context.fail_fast: + return False + return result + + +@requirement(name="Web-based Data Entity: REQUIRED availability") +class WebDataEntityRequiredChecker(PyFunctionCheck): + """ + Web-based Data Entities SHOULD be directly downloadable (RO-Crate 1.2). + Downloadability is checked via Signposting, Content-Type, and content negotiation. + Entities returning an HTML page (splash page / viewer) are also flagged. + """ + + @staticmethod + def _not_downloadable_message(entity_id: str, dl) -> str: + """Build the warning message for a web-based Data Entity that is not directly downloadable.""" + if dl.reason and "HTML" in dl.reason: + return ( + f"Web-based Data Entity '{entity_id}' references an HTML page " + f"(possible splash page or viewer application) and is not " + f"directly downloadable" + ) + msg = f"Web-based Data Entity '{entity_id}' is not directly downloadable" + if dl.reason: + msg += f": {dl.reason}" + return msg + + @check(name="Web-based Data Entity: RECOMMENDED resource availability", severity=Severity.RECOMMENDED) + def check_availability_warning(self, context: ValidationContext) -> bool: + if ( + context.settings.skip_availability_check + or context.settings.creation_time + or context.settings.enforce_availability + or context.settings.metadata_only + ): + return True + result = True + for entity in context.ro_crate.metadata.get_web_data_entities(): + assert entity.id is not None, "Entity has no @id" + if entity.id.endswith("/"): + continue + try: + dl = check_downloadable(entity.id) + if not dl.is_downloadable: + context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) + result = False + except Exception as e: + context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) + result = False + if not result and context.fail_fast: + return result + return result + + @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) + def check_content_size(self, context: ValidationContext) -> bool: + if context.settings.skip_availability_check: + return True + result = True + for entity in context.ro_crate.metadata.get_web_data_entities(): + assert entity.id is not None, "Entity has no @id" + if entity.is_available(): + content_size = entity.get_property("contentSize") + if content_size: + if isinstance(content_size, str): + content_value = content_size + elif hasattr(content_size, "id"): + content_value = content_size.id + else: + content_value = str(content_size) + try: + content_int = int(str(content_value)) + except Exception: + content_int = None + external_size = context.ro_crate.get_external_file_size(entity.id) + if external_size is not None and content_int is not None and content_int != external_size: + context.result.add_issue( + f"The property contentSize={content_size} of the Web-based Data Entity " + f"{entity.id} does not match the actual size of " + f"the downloadable content, i.e., {external_size} (bytes)", + self, + violatingEntity=entity.id, + violatingProperty="contentSize", + violatingPropertyValue=str(content_value), + ) + result = False + if not result and context.fail_fast: + return result + return result + + @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) + def check_content_url(self, context: ValidationContext) -> bool: + if context.settings.skip_availability_check: + return True + result = True + for entity in context.ro_crate.metadata.get_web_data_entities(): + content_url = entity.get_property("contentUrl") + if not content_url: + continue + urls = content_url if isinstance(content_url, list) else [content_url] + for url in urls: + url_value = url if isinstance(url, str) else url.id if hasattr(url, "id") else None + if not url_value or not url_value.startswith("http"): + continue + try: + dl = check_downloadable(url_value) + if not dl.is_downloadable: + entity_ref = f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}'" + msg = f"{entity_ref} is not directly downloadable" + if dl.reason: + msg += f": {dl.reason}" + context.result.add_issue(msg, self) + result = False + except Exception as e: + context.result.add_issue( + f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}' " + f"availability check failed: {e}", + self, + ) + result = False + if not result and context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl new file mode 100644 index 000000000..bcb836835 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl @@ -0,0 +1,209 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix dct: . +@prefix prof: . +@prefix sh: . +@prefix xsd: . + +ro-crate:FileRecommendedProperties a sh:NodeShape ; + sh:name "File Data Entity: RECOMMENDED `encodingFormat` property" ; + sh:description """A `File` Data Entity SHOULD have detailed descriptions of encodings through the `encodingFormat` property.""" ; + sh:targetClass ro-crate:File ; + sh:property [ + sh:minCount 1 ; + sh:maxCount 2 ; + sh:path schema_org:encodingFormat ; + sh:severity sh:Warning ; + sh:name "File Data Entity: RECOMMENDED `encodingFormat` property" ; + sh:description """Check if the File Data Entity has a detailed description of encodings through the `encodingFormat` property. + The `encodingFormat` property SHOULD be a PRONOM identifier (e.g., application/pdf) or, + to add more detail, SHOULD be linked using a `PRONOM` identifier to a `Contextual Entity` of type `WebPage`.""" ; + sh:message "Missing or invalid `encodingFormat` linked to the `File Data Entity`" ; + sh:or ( + [ + sh:datatype xsd:string ; + sh:pattern "^(\\w*)\\/(\\w[\\w\\.-]*)(?:\\+(\\w[\\w\\.-]*))?(?:;(\\w+=[^;]+))*$" ; + ] + [ + sh:nodeKind sh:IRI ; + ] + )] . + +ro-crate:FileContentSizeRecommendedProperties a sh:NodeShape ; + sh:name "File Data Entity: RECOMMENDED contentSize" ; + sh:description """A `File` Data Entity SHOULD have `contentSize` set to the size in bytes.""" ; + sh:targetClass ro-crate:File ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "File Data Entity: RECOMMENDED `contentSize` property" ; + sh:path schema_org:contentSize ; + sh:datatype xsd:string ; + sh:severity sh:Warning ; + sh:message "File Data Entities SHOULD have a `contentSize` property" ; + ] . + +ro-crate:DataEntityRecommendedProperties a sh:NodeShape ; + sh:name "Data Entity: RECOMMENDED properties" ; + sh:description """Data Entities SHOULD have `name`, `description`, `license`, `contentLocation`, `spatialCoverage`, and `citation` properties.""" ; + sh:targetClass ro-crate:DataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Data Entity: RECOMMENDED `name` property" ; + sh:path schema_org:name ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:severity sh:Warning ; + sh:message "Data Entities SHOULD have a `name` property" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Data Entity: RECOMMENDED `description` property" ; + sh:path schema_org:description ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:severity sh:Warning ; + sh:message "Data Entities SHOULD have a `description` property" ; + ] ; + sh:property [ + sh:name "Data Entity: RECOMMENDED `license` reference" ; + sh:description """If present, `license` SHOULD reference a CreativeWork entity.""" ; + sh:path schema_org:license ; + sh:class schema_org:CreativeWork ; + sh:severity sh:Warning ; + sh:message "Data Entity `license` SHOULD reference a CreativeWork entity" ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this (schema:contentLocation|schema:spatialCoverage) ?location . + FILTER NOT EXISTS { + ?location a schema:Place . + } + } + """ ; + sh:name "Data Entity: RECOMMENDED `contentLocation`/`spatialCoverage` reference" ; + sh:description """If present, `contentLocation` or `spatialCoverage` SHOULD reference a Place.""" ; + sh:message "If present, `contentLocation` or `spatialCoverage` SHOULD reference a Place" ; + sh:severity sh:Warning ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this schema:citation ?citation . + FILTER NOT EXISTS { + ?citation a ?type . + FILTER (?type IN (schema:ScholarlyArticle, schema:CreativeWork)) + } + } + """ ; + sh:name "Data Entity: RECOMMENDED `citation` reference" ; + sh:description """If present, `citation` SHOULD reference a ScholarlyArticle or CreativeWork.""" ; + sh:message "Data Entity `citation` SHOULD reference a ScholarlyArticle or CreativeWork" ; + sh:severity sh:Warning ; + ] ; + sh:property [ + sh:name "Data Entity: RECOMMENDED `author` reference" ; + sh:description """If present, `author` SHOULD reference Person entities.""" ; + sh:path schema_org:author ; + sh:class schema_org:Person ; + sh:severity sh:Warning ; + sh:message "Data Entity `author` SHOULD reference a Person entity" ; + ] . + +# ro-crate:DirectoryDataEntityRecommendedValueRestriction a sh:NodeShape ; +# sh:name "Directory Data Entity: RECOMMENDED value restriction" ; +# sh:description """A Directory Data Entity SHOULD end with `/`""" ; +# sh:targetClass ro-crate:Directory ; +# sh:property [ +# a sh:PropertyShape ; +# sh:name "Directory Data Entity: RECOMMENDED value restriction" ; +# sh:description """Check if the Directory Data Entity ends with `/`""" ; +# sh:path [ +# sh:inversePath rdf:type +# ] ; sh:message """Every Data Entity Directory URI SHOULD end with `/`""" ; sh:pattern "/$" ; +# ] . + +# ro-crate:DatasetIdShouldEndWithSlash a sh:NodeShape ; +# sh:name "Dataset: RECOMMENDED trailing slash" ; +# sh:description "Dataset IRI SHOULD end with `/`." ; +# sh:targetClass schema_org:Dataset ; +# sh:nodeKind sh:IRI ; +# sh:pattern "/$" ; +# sh:message "Dataset IRI SHOULD end with '/'" . + +ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; + sh:name "Web-based Directory Data Entity: recommended distribution" ; + sh:description """If a non-root Dataset Data Entity has a web URI (absolute https?://), + it SHOULD include a `distribution` property pointing to a downloadable archive + (RO-Crate 1.2, Data Entities — Directories on the web).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?root) + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + FILTER regex(str(?this), "^https?://", "i") + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Web-based Directory Data Entity: RECOMMENDED `distribution` property" ; + sh:description """Check if the web-based Directory Data Entity has a `distribution` + property pointing to a downloadable archive.""" ; + sh:path schema_org:distribution ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Web-based Directory Data Entities SHOULD include a `distribution` property pointing to a downloadable archive" ; + ] . + +ro-crate:FileConformsToProfile a sh:NodeShape ; + sh:name "File: RECOMMENDED `conformsTo` profile" ; + sh:description """If present, `conformsTo` SHOULD reference a Profile or CreativeWork entity.""" ; + sh:targetClass ro-crate:File ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this dct:conformsTo ?target . + FILTER NOT EXISTS { + ?target a schema:CreativeWork . + } + FILTER NOT EXISTS { + ?target a . + } + } + """ ; + sh:name "File: RECOMMENDED `conformsTo` profile" ; + sh:message "File `conformsTo` SHOULD reference a Profile or CreativeWork entity" ; + sh:severity sh:Warning ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl new file mode 100644 index 000000000..d46263091 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl @@ -0,0 +1,68 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro-crate: . +@prefix schema_org: . +@prefix sh: . + +# Selects local (relative-URI) non-root Dataset entities (directory Data Entities). +ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?root) + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + FILTER(!regex(str(?this), "^https?://", "i")) + } + """ . + +# SHOULD: relative @id SHOULD end with '/' +ro-crate:DirectoryDataEntityTrailingSlash a sh:NodeShape ; + sh:name "Dataset Data Entity: SHOULD end with trailing slash" ; + sh:description """A Dataset Data Entity whose @id is a relative URI SHOULD end with '/' + (RO-Crate 1.2, Dataset Data Entities).""" ; + sh:target ro-crate:LocalDatasetTarget ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + FILTER(!regex(str(?this), "/$")) + } + """ ; + sh:name "Dataset Data Entity: RECOMMENDED trailing slash" ; + sh:description "Check that a local Dataset Data Entity @id ends with '/'." ; + sh:severity sh:Warning ; + sh:message "The @id of a local Dataset Data Entity SHOULD end with '/'" ; + ] . + +# SHOULD: hasPart SHOULD list contained Data Entities +ro-crate:DirectoryDataEntityHasPart a sh:NodeShape ; + sh:name "Dataset Data Entity: SHOULD have hasPart" ; + sh:description """A local Dataset Data Entity SHOULD use `hasPart` to list + the Data Entities it contains (RO-Crate 1.2, Dataset Data Entities).""" ; + sh:target ro-crate:LocalDatasetTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Dataset Data Entity: RECOMMENDED `hasPart` property" ; + sh:description "Check that a local Dataset Data Entity lists its contents via `hasPart`." ; + sh:path schema_org:hasPart ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Local Dataset Data Entities SHOULD list their contents via `hasPart`" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py new file mode 100644 index 000000000..08bbe75ce --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py @@ -0,0 +1,118 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# NOTE: This check is implemented as a Python PyFunctionCheck rather than a +# SHACL shape because the core condition — "the file is not present in the +# RO-Crate payload" — requires filesystem access (i.e. checking whether a +# referenced local file actually exists on disk or inside a ZIP archive). +# SHACL cannot express conditions that depend on external state such as +# file availability. A SHACL shape could only verify that `localPath` is +# present as a property, which would produce false positives on every local +# file that *is* present and therefore has no need for `localPath`. By +# using a Python check we can combine graph inspection with filesystem +# availability checks and emit a warning only when a local file is missing +# *and* no `localPath` property is provided. +# +# Additionally, the spec states that a File entity MAY use a local identifier +# starting with "#" for files that are deliberately not present in the +# payload. In that case `localPath` SHOULD be used to indicate where the +# file can be found. This requires distinguishing "#" identifiers from +# regular relative paths, which SHACL cannot do. + +import contextlib + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +logger = logging.getLogger(__name__) + + +@requirement(name="Data Entity: missing file SHOULD use localPath") +class MissingFileLocalPathChecker(PyFunctionCheck): + """ + In an attached RO-Crate, when a File Data Entity references a local file + that is not present in the RO-Crate payload, the entity SHOULD declare a + ``localPath`` property to indicate where the file can be found locally. + + Additionally, a File Data Entity whose ``@id`` begins with ``#`` denotes a + deliberately absent file; in that case ``localPath`` SHOULD also be + declared to indicate where the file can be found when it exists. + (RO-Crate 1.2 specification, section on File Data Entities.) + """ + + def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: + """ + Validate a single File Data Entity. Returns False (and records an issue) + when the entity refers to an absent local file without declaring a + `localPath` property; returns True otherwise. + """ + if entity.has_local_identifier(): + local_path = entity.get_property("localPath") + if not local_path: + context.result.add_issue( + f"File Data Entity '{entity.id}' uses a local " + f"identifier (#) for a deliberately absent file, " + f"but does not declare a `localPath` property; " + f"consider adding `localPath` to indicate where " + f"the file can be found locally", + self, + ) + return False + local_path_value = ( + local_path + if isinstance(local_path, str) + else local_path.id + if hasattr(local_path, "id") + else str(local_path) + ) + logger.warning( + "File Data Entity '%s' declares localPath='%s' for a " + "deliberately absent file; the availability of this " + "path cannot be verified by the validator", + entity.id, + local_path_value, + ) + return True + if not entity.has_relative_path() or entity.is_available(): + return True + if not entity.get_property("localPath"): + context.result.add_issue( + f"File Data Entity '{entity.id}' is not present in the " + f"RO-Crate payload and does not declare a `localPath` " + f"property; consider adding `localPath` to indicate where " + f"the file can be found locally", + self, + ) + return False + return True + + @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) + def check_missing_file_local_path(self, context: ValidationContext) -> bool: + if context.ro_crate.is_detached() or context.settings.metadata_only: + return True + root_entity_id = None + with contextlib.suppress(Exception): + root_entity_id = context.ro_crate.metadata.get_root_data_entity().id + result = True + for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + if root_entity_id and entity.id == root_entity_id: + continue + if not entity.is_file(): + continue + if not self._check_entity_local_path(context, entity): + result = False + if context.fail_fast: + return False + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py new file mode 100644 index 000000000..377d0fe0f --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py @@ -0,0 +1,113 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Network-aware refinement for Referenced RO-Crate `sdDatePublished` check. + +The structural SHACL shape `ReferencedROCrateSdDatePublishedRecommended` +already warns when a referenced RO-Crate data entity with an absolute URI +@id omits both `identifier` and `sdDatePublished`. RO-Crate 1.2 § 4.5 +relaxes that obligation when the URI declares Signposting +`Link: rel="cite-as"` (which supplies a persistent citation surrogate). +This Python check performs the network-dependent refinement. +""" + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.signposting import has_signposting_cite_as + +logger = logging.getLogger(__name__) + + +@requirement(name="Referenced RO-Crate: Signposting cite-as refinement for sdDatePublished") +class ReferencedROCrateSignpostingCiteAsChecker(PyFunctionCheck): + """ + Network-aware refinement of the `sdDatePublished` requirement for + referenced RO-Crate data entities (RO-Crate 1.2, § 4.5). + """ + + @staticmethod + def _needs_sddatepublished_check(entity, root_id) -> bool: + """ + Return True when `entity` is a referenced RO-Crate (absolute-URI @id + conforming to RO-Crate) that declares neither `identifier` nor + `sdDatePublished`, and therefore requires the Signposting refinement. + """ + if entity.id == root_id: + return False + if not entity.id.startswith(("http://", "https://")): + return False + conforms_to = entity.get_property("conformsTo", []) + if not isinstance(conforms_to, list): + conforms_to = [conforms_to] + conforms_to_ids = [c.id if hasattr(c, "id") else str(c) for c in conforms_to] + if not any(c and c.startswith("https://w3id.org/ro/crate") for c in conforms_to_ids): + return False + return not (entity.get_property("identifier") or entity.get_property("sdDatePublished")) + + @check( + name="Referenced RO-Crate: `sdDatePublished` SHOULD be present when Signposting cite-as is not declared", + severity=Severity.RECOMMENDED, + ) + def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: + """ + For each referenced RO-Crate data entity whose @id is an absolute URI + with no declared `identifier` and no `sdDatePublished`, verify whether + the URI declares Signposting `Link: rel="cite-as"`. If cite-as is + absent the entity SHOULD include `sdDatePublished`. + """ + if ( + context.settings.skip_availability_check + or not (context.settings.creation_time or context.settings.enforce_availability) + or context.settings.metadata_only + ): + return True + + result = True + try: + root = context.ro_crate.metadata.get_root_data_entity() + except Exception: + return True + + for entity in context.ro_crate.metadata.get_dataset_entities(): + if not self._needs_sddatepublished_check(entity, root.id): + continue + entity_id = entity.id + + cite_as_present = has_signposting_cite_as(entity_id) + if cite_as_present is None: + logger.debug( + "Signposting probe for referenced RO-Crate '%s' failed; skipping cite-as refinement.", + entity_id, + ) + continue + if cite_as_present: + logger.debug( + "Referenced RO-Crate '%s' declares Signposting rel='cite-as'; `sdDatePublished` not required.", + entity_id, + ) + continue + + context.result.add_issue( + f"Referenced RO-Crate '{entity_id}' has no Signposting " + f'`Link: rel="cite-as"` declared; `sdDatePublished` SHOULD be ' + f"present to indicate when the absolute URI was accessed", + self, + ) + result = False + if context.fail_fast: + return result + + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl new file mode 100644 index 000000000..26a96b3a2 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl @@ -0,0 +1,199 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix dct: . +@prefix prof: . +@prefix sh: . +@prefix xsd: . + +# --------------------------------------------------------------- +# Shared SPARQL target: Referenced RO-Crate Data Entity +# +# Selects Dataset entities that declare conformsTo pointing to an +# RO-Crate specification URI (starting with https://w3id.org/ro/crate) +# and are not the Root Data Entity. +# --------------------------------------------------------------- +ro-crate:ReferencedROCrateDataEntityTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Dataset ; + dct:conformsTo ?profile . + FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?root) + } + """ . + +# --------------------------------------------------------------- +# Shared SPARQL target: Referenced RO-Crate Metadata Descriptor +# +# Selects entities referenced by subjectOf of a Referenced RO-Crate +# Data Entity. +# --------------------------------------------------------------- +ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?crate a schema:Dataset ; + dct:conformsTo ?profile ; + schema:subjectOf ?this . + FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?crate != ?root) + } + """ . + +# These shapes validate data entities that represent referenced RO-Crates. +# A "referenced RO-Crate" is a Dataset entity whose conformsTo property +# includes an RO-Crate specification URI (starting with +# https://w3id.org/ro/crate). The RO-Crate 1.2 specification requires +# that such entities follow specific conventions regarding conformsTo, +# subjectOf, and the structure of their associated metadata descriptor. + +# --------------------------------------------------------------- +# Referenced RO-Crate data entity SHOULD include the +# version-less RO-Crate base profile URI in conformsTo +# --------------------------------------------------------------- +ro-crate:ReferencedROCrateConformsToVersionlessProfile a sh:NodeShape ; + sh:name "Referenced RO-Crate: SHOULD include version-less profile in conformsTo" ; + sh:description """A data entity that represents a referenced RO-Crate + SHOULD include the version-less RO-Crate base profile URI + (https://w3id.org/ro/crate) in its conformsTo property, to indicate + that it is an RO-Crate. Specific profile versions are also permitted + in addition to the version-less URI.""" ; + sh:target ro-crate:ReferencedROCrateDataEntityTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Referenced RO-Crate: SHOULD include version-less conformsTo" ; + sh:description """A referenced RO-Crate data entity SHOULD include the + version-less RO-Crate base profile URI + (https://w3id.org/ro/crate) in its conformsTo property.""" ; + sh:path dct:conformsTo ; + sh:hasValue ; + sh:severity sh:Warning ; + sh:message "Referenced RO-Crate data entity SHOULD include the version-less RO-Crate base profile URI (https://w3id.org/ro/crate) in its conformsTo property" ; + ] . + +# --------------------------------------------------------------- +# Referenced RO-Crate data entity SHOULD have subjectOf +# pointing to a metadata descriptor contextual entity +# --------------------------------------------------------------- +ro-crate:ReferencedROCrateSubjectOfRecommended a sh:NodeShape ; + sh:name "Referenced RO-Crate: SHOULD have subjectOf" ; + sh:description """A data entity that represents a referenced RO-Crate + SHOULD have a subjectOf property linking to a contextual entity + representing the metadata descriptor of the referenced RO-Crate.""" ; + sh:target ro-crate:ReferencedROCrateDataEntityTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Referenced RO-Crate: SHOULD have subjectOf" ; + sh:description """A referenced RO-Crate data entity SHOULD have a + subjectOf property linking to a contextual entity representing + the metadata descriptor of the referenced RO-Crate.""" ; + sh:path schema_org:subjectOf ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Referenced RO-Crate data entity SHOULD have a subjectOf property linking to the referenced crate metadata descriptor" ; + ] . + +# --------------------------------------------------------------- +# Referenced RO-Crate data entity whose @id is an absolute URI +# without a declared persistent identifier SHOULD include +# `sdDatePublished` to indicate when the absolute URI was +# accessed (unless Signposting rel=cite-as is declared — that +# conditional refinement is verified separately by the Python +# check `ReferencedROCrateSignpostingCiteAsChecker`). +# (RO-Crate 1.2, § 4.5 — Determining entity identifier for a +# referenced RO-Crate, step #3.) +# --------------------------------------------------------------- +ro-crate:ReferencedROCrateSdDatePublishedRecommended a sh:NodeShape ; + sh:name "Referenced RO-Crate: SHOULD have sdDatePublished when @id is an absolute URI without persistent identifier" ; + sh:description """A data entity that represents a referenced RO-Crate + whose @id is an absolute HTTP(S) URI — and that does not declare a + persistent identifier via `identifier` — SHOULD include the + `sdDatePublished` timestamp to indicate when the URI was accessed + (RO-Crate 1.2, § 4.5). This structural check does not inspect + Signposting `Link: rel=cite-as` headers; that refinement is performed + by a separate network-aware Python check.""" ; + sh:target ro-crate:ReferencedROCrateDataEntityTarget ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this + WHERE { + FILTER(STRSTARTS(STR($this), "http://") || STRSTARTS(STR($this), "https://")) + FILTER NOT EXISTS { $this schema:identifier ?id } + FILTER NOT EXISTS { $this schema:sdDatePublished ?date } + } + """ ; + sh:severity sh:Warning ; + sh:message "A Referenced RO-Crate data entity with an absolute URI @id and no declared persistent identifier SHOULD include `sdDatePublished` to indicate when the URI was accessed (unless Signposting `Link: rel=cite-as` is declared on that URI)" ; + ] . + +# --------------------------------------------------------------- +# Metadata descriptor of a referenced RO-Crate +# +# All three constraints (encodingFormat, no conformsTo, no about) +# share the same SPARQL target. +# --------------------------------------------------------------- +ro-crate:ReferencedROCrateMetadataDescriptorProperties a sh:NodeShape ; + sh:name "Referenced RO-Crate metadata descriptor: recommended properties" ; + sh:description """The contextual entity representing the metadata descriptor + of a referenced RO-Crate SHOULD have encodingFormat set to + 'application/ld+json' and SHOULD NOT have conformsTo or about + properties. Profile declarations belong on the data entity, and + the about property is reserved for the crate's own metadata + descriptor.""" ; + sh:target ro-crate:ReferencedROCrateMetadataDescriptorTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Referenced RO-Crate metadata descriptor: SHOULD have encodingFormat application/ld+json" ; + sh:description """The metadata descriptor of a referenced RO-Crate + SHOULD declare encodingFormat as 'application/ld+json'.""" ; + sh:path schema_org:encodingFormat ; + sh:hasValue "application/ld+json" ; + sh:severity sh:Warning ; + sh:message "Referenced RO-Crate metadata descriptor SHOULD have encodingFormat 'application/ld+json'" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Referenced RO-Crate metadata descriptor: SHOULD NOT have conformsTo" ; + sh:description """The metadata descriptor of a referenced RO-Crate + SHOULD NOT have a conformsTo property. Profile declarations belong + on the data entity.""" ; + sh:path dct:conformsTo ; + sh:maxCount 0 ; + sh:severity sh:Warning ; + sh:message "Referenced RO-Crate metadata descriptor SHOULD NOT have a conformsTo property" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Referenced RO-Crate metadata descriptor: SHOULD NOT have about" ; + sh:description """The metadata descriptor of a referenced RO-Crate + SHOULD NOT have an about property. The about property belongs + on the crate's own metadata descriptor.""" ; + sh:path schema_org:about ; + sh:maxCount 0 ; + sh:severity sh:Warning ; + sh:message "Referenced RO-Crate metadata descriptor SHOULD NOT have an about property" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py new file mode 100644 index 000000000..810b1646c --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py @@ -0,0 +1,96 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.signposting import check_downloadable + +logger = logging.getLogger(__name__) + + +def _resolve_distribution_url(dist) -> str | None: + """ + Extract the download URL from a distribution value. + + The distribution property can be: + - A plain string URL + - A DataDownload entity reference (ROCrateEntity) with a ``contentUrl`` + or an ``@id`` that is itself a download URL + """ + if isinstance(dist, str): + return dist if dist.startswith("http") else None + if hasattr(dist, "get_property"): + # Prefer contentUrl, fall back to @id + content_url_raw = dist.get_property("contentUrl") + if content_url_raw: + url = ( + content_url_raw + if isinstance(content_url_raw, str) + else content_url_raw.id + if hasattr(content_url_raw, "id") + else None + ) + if url and url.startswith("http"): + return url + if hasattr(dist, "id") and dist.id and dist.id.startswith("http"): + return dist.id + return None + + +@requirement(name="Dataset: distribution downloadability") +class DatasetDistributionChecker(PyFunctionCheck): + """ + If a Dataset declares a ``distribution`` property pointing to a DataDownload + entity, the referenced archive SHOULD be directly downloadable (RO-Crate 1.2, + RECOMMENDED). Downloadability is verified via Signposting, Content-Type, and + content negotiation. + """ + + def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bool: + """Verify a single distribution value; return False (and record an issue) if not downloadable.""" + url = _resolve_distribution_url(dist) + if not url: + return True + try: + dl = check_downloadable(url) + if not dl.is_downloadable: + msg = f"The distribution '{url}' of Dataset '{entity_id}' SHOULD be downloadable" + if dl.reason: + msg += f": {dl.reason}" + context.result.add_issue(msg, self) + return False + except Exception as e: + context.result.add_issue( + f"Error checking downloadability of distribution '{url}' for Dataset '{entity_id}': {e}", self + ) + return False + return True + + @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) + def check_distribution_downloadable(self, context: ValidationContext) -> bool: + if context.settings.skip_availability_check or context.settings.metadata_only: + return True + result = True + for entity in context.ro_crate.metadata.get_dataset_entities(): + distribution_raw = entity.get_property("distribution") + if not distribution_raw: + continue + distributions = distribution_raw if isinstance(distribution_raw, list) else [distribution_raw] + for dist in distributions: + if not self._check_distribution(context, entity.id, dist): + result = False + if not result and context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl new file mode 100644 index 000000000..26720849f --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xsd: . + +ro-crate:WebBasedDataEntityRecommendedProperties a sh:NodeShape ; + sh:name "Web-based Data Entity: RECOMMENDED properties" ; + sh:description """A Web-based Data Entity SHOULD have `contentSize` and `sdDatePublished` properties.""" ; + sh:targetClass ro-crate:WebDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Web-based Data Entity: RECOMMENDED `contentSize` property" ; + sh:description """Check if the Web-based Data Entity has a `contentSize` property""" ; + sh:path schema_org:contentSize ; + sh:datatype xsd:string ; + sh:severity sh:Warning ; + sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:name "Web-based Data Entity: RECOMMENDED `sdDatePublished` property" ; + sh:description """Check if the Web-based Data Entity has a `sdDatePublished` property to indicate when the absolute URL was accessed""" ; + sh:path schema_org:sdDatePublished ; + sh:datatype xsd:string ; + sh:pattern "^([\\+-]?\\d{4})((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))|W([0-4]\\d|5[0-2])(-?[1-7])|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)?[0-5]\\d)?|24:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)$" ; + sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property to indicate when the absolute URL was accessed""" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl new file mode 100644 index 000000000..3103fffc3 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl @@ -0,0 +1,277 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xsd: . +@prefix dct: . + +ro-crate:ContextualEntityReferences a sh:NodeShape ; + sh:name "Contextual Entity RECOMMENDED references" ; + sh:description "Contextual entities SHOULD be referenced by other entities and described in the same graph." ; + sh:targetClass ro-crate:ContextualEntity ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:name "Contextual Entity references" ; + sh:description "Check that Contextual Entities are referenced by other entities." ; + sh:message "Contextual entities SHOULD be referenced by other entities." ; + sh:select """ + SELECT ?this + WHERE { + FILTER NOT EXISTS { + ?other ?pr ?this . + FILTER (?other != ?this) + } + } + """ ; + ] . + +ro-crate:ContextualEntityProperties a sh:NodeShape ; + sh:name "Contextual Entity Properties" ; + sh:description """Contextual Entities MUST have a `name` property and SHOULD have a `description` property.""" ; + sh:targetClass ro-crate:ContextualEntity ; + sh:property [ + sh:path schema:name ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:name "Contextual Entity: REQUIRED `name` property" ; + sh:description "Check if the Contextual Entity has a `name` property" ; + sh:message "A Contextual Entity MUST have a `name` property" ; + ] ; + sh:property [ + sh:path schema:description ; + sh:dataType xsd:string ; + sh:name "Contextual Entity: OPTIONAL `description` property" ; + sh:description "Check if the Contextual Entity has a `description` property" ; + sh:message "A Contextual Entity SHOULD have a `description` property" ; + ] . + +ro-crate:ReferencedContextualEntitiesShouldBeDescribed a sh:NodeShape ; + sh:name "Contextual Entity RECOMMENDED description" ; + sh:description """Contextual entities referenced by other entities SHOULD be described in the same graph.""" ; + sh:targetClass ro-crate:ContextualEntity ; + sh:property [ + sh:name "Contextual Entity: REQUIRED RDF type" ; + sh:description "Check if the Contextual Entity is described in the same graph with at least an RDF type specified." ; + sh:message "Contextual entities that are referenced by other entities SHOULD be described in the same @graph, with at least an RDF type specified." ; + # One rdf:type triple is automatically added to the data graph for each Contextual Entity + # by the `ContextualEntityDefinition` shape, so this constraint checks + # that at least that triple is present in the graph for the Contextual Entity. + sh:path rdf:type ; + sh:minCount 2 ; + ] . + +# ro-crate:EntityNameRecommendedProperties a sh:NodeShape ; +# sh:name "Entity: RECOMMENDED name" ; +# sh:description """Entities SHOULD have a human-readable `name`, in particular if its `@id` does not go to a human-readable Web page. +# This follows the RO-Crate 1.2 Common Principles for RO-Crate entities.""" ; +# sh:targetClass ro-crate:ContextualEntity ; +# sh:property [ +# sh:path schema:name ; +# sh:minCount 1 ; +# sh:datatype xsd:string ; +# sh:severity sh:Warning ; +# sh:name "Contextual Entity: RECOMMENDED `name` property" ; +# sh:description "Check if the Contextual Entity has a `name` property" ; +# sh:message "A Contextual Entity SHOULD have a `name` property" ; +# ] . + +ro-crate:CreativeWorkAuthorMinimumRecommendedProperties a sh:NodeShape ; + sh:name "CreativeWork Author: minimum RECOMMENDED properties" ; + sh:description """The minimum recommended properties for a `CreativeWork Author` are `name` and `affiliation`.""" ; + sh:targetClass ro-crate:CreativeWorkAuthor ; + sh:property [ + sh:path schema:name ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:name "CreativeWork Author: RECOMMENDED name property" ; + sh:description "Check if the author has a name." ; + sh:message "The author SHOULD have a name." ; + ] ; + sh:property [ + sh:path schema:affiliation ; + sh:minCount 1 ; + sh:or ( + [ + sh:dataType xsd:string ; + ] + [ + sh:class schema:Organization ; + ] + ) ; + sh:severity sh:Warning ; + sh:name "CreativeWork Author: RECOMMENDED affiliation property" ; + sh:description "Check if the author has an organizational affiliation." ; + sh:message "The author SHOULD have an organizational affiliation." ; + ] ; + sh:property [ + sh:path schema:affiliation ; + sh:minCount 1 ; + sh:class schema:Organization ; + sh:severity sh:Warning ; + sh:name "CreativeWork Author: RECOMMENDED Contextual Entity linked for the organizational `affiliation` property" ; + sh:description "Check if the author has a Contextual Entity for the organizational `affiliation` property." ; + sh:message "The author SHOULD have a Contextual Entity which specifies the organizational `affiliation`." ; + ] . + +ro-crate:OrganizationRecommendedProperties a sh:NodeShape ; + sh:name "Organization: RECOMMENDED properties" ; + sh:description """The recommended properties for an `Organization` are `name` and `url`.""" ; + sh:targetClass schema:Organization ; + sh:property [ + sh:path schema:name ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:name "Organization: RECOMMENDED name property" ; + sh:description "Check if the `organization` has a name." ; + sh:message "The organization SHOULD have a name." ; + ] ; + sh:property [ + sh:path schema:url ; + sh:minCount 1 ; + sh:dataType xsd:anyURI ; + sh:name "Organization: RECOMMENDED url property" ; + sh:description "Check if the `organization` has a URL." ; + sh:message "The organization SHOULD have a URL." ; + ] . + +ro-crate:PersonAffiliationRecommendedProperties a sh:NodeShape ; + sh:name "Person: RECOMMENDED affiliation" ; + sh:description """Persons SHOULD reference an Organization for affiliation.""" ; + sh:targetClass schema:Person ; + sh:property [ + sh:path schema:affiliation ; + sh:class schema:Organization ; + sh:severity sh:Warning ; + sh:message "Persons SHOULD reference an Organization for affiliation" ; + ] . + +ro-crate:LicenseEntityRecommendedProperties a sh:NodeShape ; + sh:name "License entity: RECOMMENDED properties" ; + sh:description """License entities SHOULD have `name` and `description`.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?subject schema:license ?this . + } + """ + ] ; + sh:property [ + sh:path schema:name ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:severity sh:Warning ; + sh:message "License entities SHOULD have a name" ; + ] ; + sh:property [ + sh:path schema:description ; + sh:minCount 1 ; + sh:dataType xsd:string ; + sh:severity sh:Warning ; + sh:message "License entities SHOULD have a description" ; + ] . + +ro-crate:PlaceGeoRecommendedProperties a sh:NodeShape ; + sh:name "Place: RECOMMENDED geo" ; + sh:description """Places SHOULD reference Geometry via `geo` when geometry is provided.""" ; + sh:targetClass schema:Place ; + sh:property [ + sh:path schema:geo ; + sh:class ; + sh:severity sh:Warning ; + sh:message "Places SHOULD reference a Geometry entity via `geo`" ; + ] . + +ro-crate:GeometryWktRecommendedProperties a sh:NodeShape ; + sh:name "Geometry: RECOMMENDED asWKT" ; + sh:description """Geometry entities SHOULD provide `asWKT`.""" ; + sh:targetClass ; + sh:property [ + sh:path ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Geometry entities SHOULD provide `asWKT`" ; + ] . + +ro-crate:LicenseEntityAbsoluteUrl a sh:NodeShape ; + sh:name "License entity: SHOULD have absolute URL @id" ; + sh:description """A License entity SHOULD be identified by an absolute HTTP(S) URL, + for example an SPDX license URI (https://spdx.org/licenses/). + (RO-Crate 1.2, Contextual Entities — License)""" ; + sh:targetClass ro-crate:License ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + FILTER(!regex(str(?this), "^https?://", "i")) + } + """ ; + sh:severity sh:Warning ; + sh:message "A License entity SHOULD have an absolute HTTP(S) URL as its @id (e.g., an SPDX license URI such as https://spdx.org/licenses/MIT.html)" ; + ] . + +ro-crate:LicenseEntityCreativeWorkType a sh:NodeShape ; + sh:name "License entity: SHOULD be typed as CreativeWork" ; + sh:description """A License entity SHOULD include `schema:CreativeWork` in its `@type`. + (RO-Crate 1.2, Contextual Entities — License)""" ; + sh:targetClass ro-crate:License ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + FILTER NOT EXISTS { + ?this a schema:CreativeWork . + } + } + """ ; + sh:severity sh:Warning ; + sh:message "A License entity SHOULD have `CreativeWork` in its `@type`" ; + ] . + +ro-crate:EncodingFormatRecommendedTypes a sh:NodeShape ; + sh:name "Encoding format: RECOMMENDED types" ; + sh:description """Encoding format contextual entities SHOULD include WebPage and/or Standard types.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?s schema:encodingFormat ?this . + FILTER(isIRI(?this)) + } + """ + ] ; + sh:property [ + sh:path rdf:type ; + sh:or ( + [ + sh:hasValue schema:WebPage + ] + [ + sh:hasValue dct:Standard + ] + ) ; + sh:severity sh:Warning ; + sh:message "Encoding format entities SHOULD include WebPage and/or Standard types" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl new file mode 100644 index 000000000..48e338c73 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl @@ -0,0 +1,117 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xsd: . + +ro-crate:OrganizationRorIdentifier a sh:NodeShape ; + sh:name "Organization: SHOULD have ROR identifier" ; + sh:description """An Organization entity SHOULD use a ROR (Research Organization Registry) identifier + as its @id, if possible. + (RO-Crate 1.2, Contextual Entities — Organizations as values; Metadata — Recommended Identifiers)""" ; + sh:targetClass schema:Organization ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + FILTER(!STRSTARTS(STR(?this), "https://ror.org/")) + } + """ ; + sh:severity sh:Warning ; + sh:message "An Organization entity SHOULD have a ROR identifier as its @id (e.g., https://ror.org/03f0f6041)" ; + ] . + +ro-crate:OrganizationContactPoint a sh:NodeShape ; + sh:name "Organization: SHOULD have contactPoint referencing ContactPoint" ; + sh:description """An Organization SHOULD have a contactPoint property referencing a ContactPoint contextual entity. + (RO-Crate 1.2, Contextual Entities — Contact Information)""" ; + sh:targetClass schema:Organization ; + sh:property [ + sh:path schema:contactPoint ; + sh:minCount 1 ; + sh:class schema:ContactPoint ; + sh:severity sh:Warning ; + sh:name "Organization: RECOMMENDED contactPoint references ContactPoint entity" ; + sh:description "Check if the Organization has a contactPoint referencing a ContactPoint entity." ; + sh:message "An Organization's contactPoint SHOULD reference a ContactPoint contextual entity" ; + ] . + +ro-crate:PersonContactPoint a sh:NodeShape ; + sh:name "Person: SHOULD have contactPoint referencing ContactPoint" ; + sh:description """A Person's contactPoint property SHOULD reference a ContactPoint contextual entity. + (RO-Crate 1.2, Contextual Entities — Contact Information)""" ; + sh:targetClass schema:Person ; + sh:property [ + sh:path schema:contactPoint ; + sh:class schema:ContactPoint ; + sh:severity sh:Warning ; + sh:name "Person: RECOMMENDED contactPoint references ContactPoint entity" ; + sh:description "Check if the Person's contactPoint references a ContactPoint entity." ; + sh:message "A Person's contactPoint SHOULD reference a ContactPoint contextual entity" ; + ] . + +ro-crate:AuthorPublisherContactPoint a sh:NodeShape ; + sh:name "Root Data Entity: at least one author/publisher SHOULD have contactPoint" ; + sh:description """At least one of the Person or Organization entities referenced as author or publisher + from the Root Data Entity SHOULD have a contactPoint property. + (RO-Crate 1.2, Contextual Entities — Contact Information)""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + { + ?this schema:author ?author . + } UNION { + ?this schema:publisher ?publisher . + BIND(?publisher AS ?author) + } + FILTER NOT EXISTS { + { + ?this schema:author ?a . + } UNION { + ?this schema:publisher ?a . + } + ?a schema:contactPoint ?cp . + } + } + """ ; + sh:severity sh:Warning ; + sh:message "At least one author or publisher Person/Organization SHOULD have a contactPoint property" ; + ] . + +ro-crate:PersonOrcidIdentifier a sh:NodeShape ; + sh:name "Person: SHOULD have ORCID identifier" ; + sh:description """A Person entity SHOULD use an ORCID identifier as its @id. + (RO-Crate 1.2, Contextual Entities — People)""" ; + sh:targetClass schema:Person ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + FILTER(!STRSTARTS(STR(?this), "https://orcid.org/")) + } + """ ; + sh:severity sh:Warning ; + sh:message "A Person entity SHOULD have an ORCID identifier as its @id (e.g., https://orcid.org/0000-0001-2345-6789)" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl new file mode 100644 index 000000000..c4a9aeebd --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl @@ -0,0 +1,166 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix dct: . +@prefix sh: . +@prefix xsd: . +@prefix validator: . + +# Selects ImageObject entities referenced via `image` from a Script or Workflow. +ro-crate:ScriptOrWorkflowImageTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?sw a schema:SoftwareSourceCode . + ?sw schema:image ?this . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } + """ . + +# --------------------------------------------------------------- +# Script or Workflow: programmingLanguage SHOULD reference a +# ComputerLanguage entity +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowProgrammingLanguage a sh:NodeShape ; + sh:name "Script or Workflow: RECOMMENDED `programmingLanguage`" ; + sh:description """Scripts and Workflows SHOULD have a `programmingLanguage` + property referencing a contextual entity of type `ComputerLanguage` + (RO-Crate 1.2, Workflows and Scripts).""" ; + sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script or Workflow: RECOMMENDED `programmingLanguage` property" ; + sh:description """Check that the Script or Workflow has a `programmingLanguage` + property.""" ; + sh:path schema:programmingLanguage ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Scripts and Workflows SHOULD have a `programmingLanguage` property referencing a `ComputerLanguage` entity" ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Script or Workflow: `programmingLanguage` SHOULD reference ComputerLanguage" ; + sh:description """Check that the `programmingLanguage` property references + an entity of type `ComputerLanguage`.""" ; + sh:select """ + SELECT ?this + WHERE { + ?this schema:programmingLanguage ?lang . + FILTER NOT EXISTS { ?lang a schema:ComputerLanguage } + } + """ ; + sh:severity sh:Warning ; + sh:message "The `programmingLanguage` of a Script or Workflow SHOULD reference a `ComputerLanguage` entity" ; + ] . + +# --------------------------------------------------------------- +# Workflow: conformsTo SHOULD include a versioned Bioschemas +# ComputationalWorkflow profile URI +# --------------------------------------------------------------- +ro-crate:WorkflowRecommendedConformsTo a sh:NodeShape ; + sh:name "Workflow: RECOMMENDED Bioschemas `conformsTo`" ; + sh:description """A Workflow SHOULD declare `conformsTo` pointing to a + versioned Bioschemas ComputationalWorkflow profile URI + (https://bioschemas.org/profiles/ComputationalWorkflow/) + (RO-Crate 1.2, Workflows and Scripts).""" ; + sh:targetClass ro-crate:Workflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Workflow: RECOMMENDED `conformsTo` Bioschemas profile" ; + sh:description """Check that the Workflow has a `conformsTo` pointing + to a Bioschemas ComputationalWorkflow profile URI.""" ; + sh:path dct:conformsTo ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Workflows SHOULD declare `conformsTo` referencing a versioned Bioschemas ComputationalWorkflow profile URI (https://bioschemas.org/profiles/ComputationalWorkflow/)" ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Workflow `conformsTo` SHOULD reference Bioschemas profile" ; + sh:description """If `conformsTo` is present, at least one value SHOULD + start with the Bioschemas ComputationalWorkflow profile URI.""" ; + sh:select """ + SELECT ?this + WHERE { + ?this dct:conformsTo ?profile . + FILTER NOT EXISTS { + ?this dct:conformsTo ?bioprofile . + FILTER(STRSTARTS(STR(?bioprofile), "https://bioschemas.org/profiles/ComputationalWorkflow/")) + } + } + """ ; + sh:severity sh:Warning ; + sh:message "Workflow `conformsTo` SHOULD include a versioned Bioschemas ComputationalWorkflow profile URI (https://bioschemas.org/profiles/ComputationalWorkflow/)" ; + ] . + +# --------------------------------------------------------------- +# ImageObject linked from Script/Workflow: encodingFormat SHOULD +# be present +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowImageEncodingFormat a sh:NodeShape ; + sh:name "Script/Workflow ImageObject: RECOMMENDED `encodingFormat`" ; + sh:description """ImageObject entities referenced via `image` from a + Script or Workflow SHOULD have an `encodingFormat` property with an + IANA media type string and/or a Pronom identifier reference + (RO-Crate 1.2, Workflows and Scripts — related ImageObject).""" ; + sh:target ro-crate:ScriptOrWorkflowImageTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script/Workflow ImageObject: RECOMMENDED `encodingFormat` property" ; + sh:description """Check that the ImageObject has an `encodingFormat` + property.""" ; + sh:path schema:encodingFormat ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `encodingFormat` property (IANA media type and/or Pronom identifier)" ; + ] . + +# --------------------------------------------------------------- +# ImageObject linked from Script/Workflow: `about` SHOULD +# reference the script/workflow +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowImageAbout a sh:NodeShape ; + sh:name "Script/Workflow ImageObject: RECOMMENDED `about` reference" ; + sh:description """ImageObject entities referenced via `image` from a + Script or Workflow SHOULD have an `about` property that references + the script or workflow entity + (RO-Crate 1.2, Workflows and Scripts — related ImageObject).""" ; + sh:target ro-crate:ScriptOrWorkflowImageTarget ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Script/Workflow ImageObject: `about` SHOULD reference the script/workflow" ; + sh:description """Check that the ImageObject's `about` property references + the script or workflow that includes it via `image`.""" ; + sh:select """ + SELECT ?this + WHERE { + ?sw a schema:SoftwareSourceCode . + ?sw schema:image ?this . + FILTER NOT EXISTS { + ?this schema:about ?sw . + } + } + """ ; + sh:severity sh:Warning ; + sh:message "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `about` property referencing the script or workflow" ; + ] . diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..92ec33c49 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image about — invalid (missing about)", + "description": "RO-Crate with a Workflow whose image ImageObject is missing the recommended about property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [ + { "@id": "workflow.ga" }, + { "@id": "diagram.png" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "programmingLanguage": { "@id": "#galaxy" }, + "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, + "image": { "@id": "diagram.png" } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { "@id": "https://galaxyproject.org/" } + }, + { + "@id": "diagram.png", + "@type": ["File", "ImageObject"], + "name": "Workflow diagram", + "description": "A diagram missing the recommended about property.", + "encodingFormat": "image/png" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json new file mode 100644 index 000000000..016778ae5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image about \u2014 valid", + "description": "RO-Crate with a Workflow whose image ImageObject has the recommended about property referencing the workflow.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "hasPart": [ + { + "@id": "workflow.ga" + }, + { + "@id": "diagram.png" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "contentSize": "2048", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "image": { + "@id": "diagram.png" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": "https://galaxyproject.org/", + "version": "1.0" + }, + { + "@id": "diagram.png", + "@type": [ + "File", + "ImageObject" + ], + "name": "Workflow diagram", + "description": "A diagram with about referencing the workflow.", + "encodingFormat": "image/png", + "contentSize": "10240", + "about": { + "@id": "workflow.ga" + } + }, + { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Bioschemas ComputationalWorkflow Profile 1.0-RELEASE", + "description": "Bioschemas profile for computational workflows." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..0a98560be --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image encodingFormat — invalid (missing encodingFormat)", + "description": "RO-Crate with a Workflow whose image ImageObject is missing the recommended encodingFormat property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [ + { "@id": "workflow.ga" }, + { "@id": "diagram.png" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "programmingLanguage": { "@id": "#galaxy" }, + "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, + "image": { "@id": "diagram.png" } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { "@id": "https://galaxyproject.org/" } + }, + { + "@id": "diagram.png", + "@type": ["File", "ImageObject"], + "name": "Workflow diagram", + "description": "A diagram missing the required encodingFormat.", + "about": { "@id": "workflow.ga" } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..651d9abb6 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image encodingFormat \u2014 valid", + "description": "RO-Crate with a Workflow whose image ImageObject has the recommended encodingFormat property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "hasPart": [ + { + "@id": "workflow.ga" + }, + { + "@id": "diagram.png" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "contentSize": "2048", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "image": { + "@id": "diagram.png" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": "https://galaxyproject.org/", + "version": "1.0" + }, + { + "@id": "diagram.png", + "@type": [ + "File", + "ImageObject" + ], + "name": "Workflow diagram", + "description": "A diagram illustrating the workflow steps.", + "encodingFormat": "image/png", + "contentSize": "10240", + "about": { + "@id": "workflow.ga" + } + }, + { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Bioschemas ComputationalWorkflow Profile 1.0-RELEASE", + "description": "Bioschemas profile for computational workflows." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..9b21b0539 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Programming language — invalid (missing programmingLanguage)", + "description": "RO-Crate with a Script that is missing the recommended programmingLanguage property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "script.sh" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": ["File", "SoftwareSourceCode"], + "name": "My Script", + "description": "A script without a programmingLanguage property.", + "encodingFormat": "application/x-sh" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json new file mode 100644 index 000000000..1ffe2309e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json @@ -0,0 +1,88 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Programming language \u2014 valid", + "description": "RO-Crate with a Script that declares programmingLanguage referencing a ComputerLanguage entity.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "script.sh", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "My Script", + "description": "A script with proper programmingLanguage declaration.", + "encodingFormat": "application/x-sh", + "contentSize": "1024", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "url": "https://www.gnu.org/software/bash/", + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..0729de7f1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script name — invalid (missing name)", + "description": "RO-Crate with a Script entity that is missing the required name property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "script.sh" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": ["File", "SoftwareSourceCode"], + "description": "A script missing its required name property.", + "encodingFormat": "application/x-sh" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json new file mode 100644 index 000000000..07edea638 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script name \u2014 valid", + "description": "RO-Crate containing a Script entity with a required name property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "My Script", + "description": "A sample shell script with a proper name.", + "encodingFormat": "application/x-sh", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "url": { + "@id": "https://www.gnu.org/software/bash/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ffe4e0a4e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json @@ -0,0 +1,30 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script type — invalid (missing File type)", + "description": "RO-Crate with a Script entity missing the required File type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://example.org/script.sh", + "@type": "SoftwareSourceCode", + "name": "My Script", + "description": "A script with SoftwareSourceCode type only — missing the required File type." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json new file mode 100644 index 000000000..536dcb556 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script type \u2014 valid", + "description": "RO-Crate containing a Script entity with correct @type [File, SoftwareSourceCode].", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "My Script", + "description": "A sample shell script.", + "encodingFormat": "application/x-sh", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "url": { + "@id": "https://www.gnu.org/software/bash/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f0c9bfbe9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json @@ -0,0 +1,39 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow conformsTo — invalid (missing Bioschemas conformsTo)", + "description": "RO-Crate with a Workflow missing the recommended conformsTo Bioschemas profile.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "workflow.ga" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A workflow without the recommended Bioschemas conformsTo declaration.", + "encodingFormat": "application/json", + "programmingLanguage": { "@id": "#galaxy" } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { "@id": "https://galaxyproject.org/" } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af4cfbf1b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json @@ -0,0 +1,104 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow conformsTo \u2014 valid", + "description": "RO-Crate with a Workflow that declares conformsTo referencing a versioned Bioschemas profile URI.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "hasPart": [ + { + "@id": "workflow.ga" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A workflow conforming to the Bioschemas ComputationalWorkflow profile.", + "encodingFormat": "application/json", + "contentSize": "2048", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": "https://galaxyproject.org/", + "version": "1.0" + }, + { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Bioschemas ComputationalWorkflow Profile 1.0-RELEASE", + "description": "Bioschemas profile for computational workflows." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..52ada9885 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow name — invalid (missing name)", + "description": "RO-Crate with a Workflow entity missing the required name property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "workflow.ga" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "description": "A workflow missing its required name property.", + "encodingFormat": "application/json" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json new file mode 100644 index 000000000..943eedc77 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json @@ -0,0 +1,61 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow name \u2014 valid", + "description": "RO-Crate with a Workflow entity that has the required name property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "workflow.ga" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A Galaxy workflow with all required types and name.", + "encodingFormat": "application/json", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json new file mode 100644 index 000000000..e1c23f8eb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json @@ -0,0 +1,30 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow type — invalid (missing File type)", + "description": "RO-Crate with a Workflow entity missing the required File type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://example.org/workflow.ga", + "@type": ["SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A workflow missing the required File type." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json new file mode 100644 index 000000000..0fb8daeb1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json @@ -0,0 +1,30 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow type — invalid (missing SoftwareSourceCode type)", + "description": "RO-Crate with a Workflow entity missing the required SoftwareSourceCode type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://example.org/workflow.ga", + "@type": ["File", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A workflow missing the required SoftwareSourceCode type." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json new file mode 100644 index 000000000..d9dc02c30 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json @@ -0,0 +1,61 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow type \u2014 valid", + "description": "RO-Crate with a Workflow entity correctly typed as [File, SoftwareSourceCode, ComputationalWorkflow].", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "workflow.ga" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A sample Galaxy workflow.", + "encodingFormat": "application/json", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py new file mode 100644 index 000000000..a16394f0e --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py @@ -0,0 +1,324 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_2 import WorkflowsScripts +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__workflows_scripts_crates__ = WorkflowsScripts() + +# General RECOMMENDED checks that fire on minimal test crates regardless of the +# workflow/script-specific property being tested. These are skipped in the +# "valid" RECOMMENDED test cases so the assertion is only about the +# workflow-specific shape. +_GENERIC_RECOMMENDED_SKIP = [ + "ro-crate-1.2_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) + "ro-crate-1.2_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) + "ro-crate-1.2_48.1", # Root Data Entity: recommended funder + "ro-crate-1.2_55.1", # Root Data Entity: recommended publisher + "ro-crate-1.2_62.1", # File Data Entity: RECOMMENDED contentSize + "ro-crate-1.2_63.0", # File: RECOMMENDED conformsTo profile + "ro-crate-1.2_75.1", # Contextual Entity Properties + "ro-crate-1.2_76.1", # Contextual Entity RECOMMENDED description + "ro-crate-1.2_81.2", # License entity: RECOMMENDED properties +] + + +# --------------------------------------------------------------------------- +# Script type checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_script_type(): + """ + A Script entity with `File`, `SoftwareSourceCode` in @type and a `name` + MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_script_type, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + ) + + +def test_invalid_script_type(): + """ + A Script entity missing `File` (schema:MediaObject) in its @type + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_script_type, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Script: REQUIRED `File` type"], + expected_triggered_issues=["A Script MUST include `File` in its `@type`"], + ) + + +# --------------------------------------------------------------------------- +# Script name checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_script_name(): + """ + A Script entity with a `name` property MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_script_name, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + ) + + +def test_invalid_script_name(): + """ + A Script entity missing the `name` property MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_script_name, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Script or Workflow: REQUIRED `name`"], + expected_triggered_issues=["Scripts and Workflows MUST have a human-readable `name` property"], + ) + + +# --------------------------------------------------------------------------- +# Workflow type checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_workflow_type(): + """ + A Workflow entity with `File`, `SoftwareSourceCode`, `ComputationalWorkflow` + in @type and all required properties MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_workflow_type, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + ) + + +def test_invalid_workflow_type_missing_file(): + """ + A Workflow entity missing `File` (schema:MediaObject) in its @type + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_type_missing_file, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Workflow: REQUIRED `File` type"], + expected_triggered_issues=["A Workflow MUST include `File` in its `@type`"], + ) + + +def test_invalid_workflow_type_missing_ssc(): + """ + A Workflow entity missing `SoftwareSourceCode` in its @type + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_type_missing_ssc, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Workflow: REQUIRED `SoftwareSourceCode` type"], + expected_triggered_issues=["A Workflow MUST include `SoftwareSourceCode` in its `@type`"], + ) + + +# --------------------------------------------------------------------------- +# Workflow name checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_workflow_name(): + """ + A Workflow entity with a `name` property MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_workflow_name, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + ) + + +def test_invalid_workflow_name(): + """ + A Workflow entity missing the `name` property MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_name, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Script or Workflow: REQUIRED `name`"], + expected_triggered_issues=["Scripts and Workflows MUST have a human-readable `name` property"], + ) + + +# --------------------------------------------------------------------------- +# programmingLanguage checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_programming_language(): + """ + A Script/Workflow with a `programmingLanguage` referencing a + `ComputerLanguage` entity SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_programming_language, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.2", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_programming_language(): + """ + A Script/Workflow missing a `programmingLanguage` property + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_programming_language, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Script or Workflow: RECOMMENDED `programmingLanguage`"], + expected_triggered_issues=["Scripts and Workflows SHOULD have a `programmingLanguage` property"], + ) + + +# --------------------------------------------------------------------------- +# Workflow conformsTo checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_workflow_conformsTo(): + """ + A Workflow with `conformsTo` pointing to a versioned Bioschemas profile URI + SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_workflow_conformsTo, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.2", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_workflow_conformsTo(): + """ + A Workflow missing `conformsTo` SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_conformsTo, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Workflow: RECOMMENDED Bioschemas `conformsTo`"], + expected_triggered_issues=[ + "Workflows SHOULD declare `conformsTo` referencing a versioned Bioschemas ComputationalWorkflow profile URI" + ], + ) + + +# --------------------------------------------------------------------------- +# Image encodingFormat checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_image_encoding_format(): + """ + A Workflow image ImageObject with `encodingFormat` SHOULD pass + RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_image_encoding_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.2", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_image_encoding_format(): + """ + A Workflow image ImageObject missing `encodingFormat` SHOULD trigger + a RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_image_encoding_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Script/Workflow ImageObject: RECOMMENDED `encodingFormat`"], + expected_triggered_issues=[ + "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `encodingFormat` property" + ], + ) + + +# --------------------------------------------------------------------------- +# Image about checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_image_about(): + """ + A Workflow image ImageObject with `about` referencing the workflow + SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_image_about, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.2", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_image_about(): + """ + A Workflow image ImageObject missing `about` SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_image_about, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Script/Workflow ImageObject: RECOMMENDED `about` reference"], + expected_triggered_issues=[ + "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `about` property " + "referencing the script or workflow" + ], + ) From e57e2ea701651f748aa6bf139158ac0bb051493f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Sep 2026 10:33:15 +0200 Subject: [PATCH 031/271] =?UTF-8?q?refactor(versioning):=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20move=20distribution=20name=20to=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/constants.py | 3 +++ rocrate_validator/utils/versioning.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 6d25b5dcd..37adb5444 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -17,6 +17,9 @@ from rdflib import Namespace +# Define the package distribution name +PACKAGE_NAME = "roc-validator" + # Define SHACL namespace SHACL_NS = "http://www.w3.org/ns/shacl#" diff --git a/rocrate_validator/utils/versioning.py b/rocrate_validator/utils/versioning.py index 313835e06..4356ea4bf 100644 --- a/rocrate_validator/utils/versioning.py +++ b/rocrate_validator/utils/versioning.py @@ -18,12 +18,12 @@ from importlib import metadata from pathlib import Path +from rocrate_validator.constants import PACKAGE_NAME from rocrate_validator.utils import log as logging from rocrate_validator.utils.config import get_config, get_config_path # set up logging logger = logging.getLogger(__name__) -PACKAGE_NAME = "roc-validator" def run_git_command(command: list[str], cwd: Path | None = None) -> str | None: From 9d8078c75e7482c78928d9929784e6d3c6e6f71f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Sep 2026 11:36:35 +0200 Subject: [PATCH 032/271] =?UTF-8?q?feat(rocrate):=20=E2=9C=A8=20add=20sing?= =?UTF-8?q?leton-array=20property=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add shared JSON-LD handling and profile checks for RO-Crate 1.1 and 1.2 to detect entity properties represented as singleton arrays instead of compact single values. Keep the check at RECOMMENDED severity so singleton arrays remain valid for REQUIRED validation, while preserving multi-valued properties and explicit @set/@list semantics. --- ...ity_property_compact_representation_1_1.py | 51 +++++++++++++ ...ity_property_compact_representation_1_2.py | 51 +++++++++++++ rocrate_validator/utils/jsonld.py | 75 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py create mode 100644 rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py create mode 100644 rocrate_validator/utils/jsonld.py diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py b/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py new file mode 100644 index 000000000..419ec6451 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + + +@requirement(name="Entity properties compact representation") +class EntityPropertyCompactRepresentation(PyFunctionCheck): + """ + Single-element arrays used for entity properties SHOULD be unpacked to a + single value in compacted RO-Crate 1.1 JSON-LD. + """ + + @check(name="Entity properties SHOULD use single values rather than singleton arrays") + def check_singleton_property_arrays(self, context: ValidationContext) -> bool: + """Check the RO-Crate 1.1 recommendation for singleton property arrays.""" + try: + findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) + except (AttributeError, TypeError, ValueError): + # Required format checks report malformed metadata structure. + return True + + result = True + for entity_id, property_name in findings: + context.result.add_issue( + f'Entity "{entity_id}" property "{property_name}" SHOULD be represented as a single value, ' + "not a singleton array, according to the RO-Crate 1.1 JSON-LD recommendation", + self, + violatingEntity=entity_id, + violatingProperty=property_name, + violatingPropertyValue="singleton array", + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py b/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py new file mode 100644 index 000000000..3f86777fe --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + + +@requirement(name="Entity properties compact representation") +class EntityPropertyCompactRepresentation(PyFunctionCheck): + """ + Single-element arrays used for entity properties SHOULD be unpacked to a + single value in compacted RO-Crate 1.2 JSON-LD. + """ + + @check(name="Entity properties SHOULD use single values rather than singleton arrays") + def check_singleton_property_arrays(self, context: ValidationContext) -> bool: + """Check the RO-Crate 1.2 recommendation for singleton property arrays.""" + try: + findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) + except (AttributeError, TypeError, ValueError): + # Required format checks report malformed metadata structure. + return True + + result = True + for entity_id, property_name in findings: + context.result.add_issue( + f'Entity "{entity_id}" property "{property_name}" SHOULD be represented as a single value, ' + "not a singleton array, according to the RO-Crate 1.2 JSON-LD recommendation", + self, + violatingEntity=entity_id, + violatingProperty=property_name, + violatingPropertyValue="singleton array", + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/utils/jsonld.py b/rocrate_validator/utils/jsonld.py new file mode 100644 index 000000000..88b4ba6d1 --- /dev/null +++ b/rocrate_validator/utils/jsonld.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + + +def _context_containers(context: object) -> dict[str, set[str]]: + """Return explicit array-preserving containers for locally defined terms.""" + containers: dict[str, set[str]] = {} + + if isinstance(context, list): + for item in context: + containers.update(_context_containers(item)) + elif isinstance(context, dict): + for term, definition in context.items(): + if not isinstance(term, str) or term.startswith("@"): + continue + if isinstance(definition, dict): + raw_container = definition.get("@container") + if isinstance(raw_container, str): + containers[term] = {raw_container} + elif isinstance(raw_container, list): + containers[term] = {value for value in raw_container if isinstance(value, str)} + else: + containers[term] = set() + else: + containers[term] = set() + + return containers + + +def find_singleton_property_arrays(metadata: Mapping[str, Any]) -> list[tuple[str | None, str]]: + """Find singleton arrays used for properties of flattened JSON-LD entities. + + The RO-Crate ``@graph`` array and ``@context`` are structural JSON-LD + arrays, not entity property values. JSON-LD keyword properties are also + excluded. Explicit ``@set`` and ``@list`` containers preserve array + semantics and must not be reported as non-compact property values. + """ + graph = metadata.get("@graph") + if not isinstance(graph, list): + return [] + + containers = _context_containers(metadata.get("@context")) + findings: list[tuple[str | None, str]] = [] + for entity in graph: + if not isinstance(entity, Mapping): + continue + entity_id = entity.get("@id") + if entity_id is not None and not isinstance(entity_id, str): + entity_id = str(entity_id) + for property_name, value in entity.items(): + if not isinstance(property_name, str) or property_name.startswith("@"): + continue + if ( + isinstance(value, list) + and len(value) == 1 + and not {"@set", "@list"}.intersection(containers.get(property_name, set())) + ): + findings.append((entity_id, property_name)) + return findings From 675cdb2788c57bc4b4216642cb146346730d8a5a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Sep 2026 11:39:22 +0200 Subject: [PATCH 033/271] =?UTF-8?q?test(rocrate):=20=E2=9C=85=20cover=20si?= =?UTF-8?q?ngleton-array=20property=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_jsonld.py | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/unit/test_jsonld.py diff --git a/tests/unit/test_jsonld.py b/tests/unit/test_jsonld.py new file mode 100644 index 000000000..4c5830a0d --- /dev/null +++ b/tests/unit/test_jsonld.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + + +def test_find_singleton_property_arrays_preserves_jsonld_semantics(): + metadata = { + "@context": [ + { + "setProperty": {"@id": "https://example.org/setProperty", "@container": "@set"}, + "listProperty": {"@id": "https://example.org/listProperty", "@container": "@list"}, + } + ], + "@graph": [ + { + "@id": "./", + "@type": ["Dataset", "Thing"], + "singletonReference": [{"@id": "file.txt"}], + "singletonLiteral": ["one"], + "multipleValues": ["one", "two"], + "emptyValues": [], + "setProperty": ["one"], + "listProperty": ["one"], + }, + {"@id": "file.txt", "name": [{"@value": "File"}]}, + ], + } + + assert find_singleton_property_arrays(metadata) == [ + ("./", "singletonReference"), + ("./", "singletonLiteral"), + ("file.txt", "name"), + ] + + +def test_find_singleton_property_arrays_ignores_invalid_graph_shape(): + assert find_singleton_property_arrays({"@context": [], "@graph": {}}) == [] From 2aa6bcba5886e36ab92e0f5c505af479eea7e414 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 9 Sep 2026 11:41:36 +0200 Subject: [PATCH 034/271] =?UTF-8?q?test(profiles):=20=E2=9C=85=20cover=20s?= =?UTF-8?q?ingleton-array=20recommendation=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integration coverage for the singleton-array property check in RO-Crate 1.1 and 1.2, including Workflow RO-Crate profile inheritance. --- .../ro-crate-metadata.json | 22 ++++++------- .../ro-crate-metadata.json | 16 ++++------ .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../invalid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 16 ++++------ .../valid/ro-crate-metadata.json | 16 ++++------ .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../valid/ro-crate-metadata.json | 8 ++--- .../ro-crate-metadata.json | 8 ++--- .../ro-crate-metadata.json | 16 ++++------ .../prefix-ro-crate-metadata.json | 8 ++--- .../valid/wrroc-paper/ro-crate-metadata.json | 16 ++++------ .../ro-crate-1.2/test_metadata_document.py | 15 +++++++++ .../ro-crate-1.2/test_referenced_rocrate.py | 6 ++-- .../ro-crate/test_file_descriptor_format.py | 32 ++++++++++++++++++- .../workflow-ro-crate/test_main_workflow.py | 16 +++++++++- tests/unit/test_rocrate.py | 4 +-- 30 files changed, 163 insertions(+), 164 deletions(-) diff --git a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json index c68db6acb..7a38071ea 100644 --- a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json @@ -21,11 +21,9 @@ "mainEntity": { "@id": "sort-and-change-case.ga" }, - "license": [ - { - "@id": "https://spdx.org/licenses/MIT.html" - } - ], + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, "author": { "@id": "https://orcid.org/0000-0002-1825-0097" }, @@ -82,7 +80,7 @@ "subjectOf": { "@id": "sort-and-change-case.cwl" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", @@ -94,16 +92,14 @@ "url": { "@id": "https://galaxyproject.org/" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "blank.png", "@type": ["File", "ImageObject"], - "encodingFormat": [ - { - "@id": "https://www.iana.org/assignments/media-types/image/png" - } - ] + "encodingFormat": { + "@id": "https://www.iana.org/assignments/media-types/image/png" + } }, { "@id": "https://www.iana.org/assignments/media-types/image/png", @@ -116,7 +112,7 @@ "about": { "@id": "./" }, - "encodingFormat": ["text/markdown"] + "encodingFormat": "text/markdown" }, { "@id": "https://spdx.org/licenses/MIT.html", "@type": "CreativeWork" } ] diff --git a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json index 3d93deecb..f996dcd79 100644 --- a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json @@ -21,11 +21,9 @@ "mainEntity": { "@id": "sort-and-change-case.ga" }, - "license": [ - { - "@id": "https://spdx.org/licenses/MIT.html" - } - ], + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, "author": { "@id": "https://orcid.org/0000-0002-1825-0097" }, @@ -82,7 +80,7 @@ "subjectOf": { "@id": "sort-and-change-case.cwl" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", @@ -94,12 +92,12 @@ "url": { "@id": "https://galaxyproject.org/" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "blank.png", "@type": ["File", "ImageObject"], - "encodingFormat": ["image/png"] + "encodingFormat": "image/png" }, { "@id": "README.md", @@ -107,7 +105,7 @@ "about": { "@id": "./" }, - "encodingFormat": ["text/markdown"] + "encodingFormat": "text/markdown" }, { "@id": "https://spdx.org/licenses/MIT.html", "@type": "CreativeWork" } ] diff --git a/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json index 1ffe2309e..17083cce4 100644 --- a/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "script.sh" - } - ], + "hasPart": { + "@id": "script.sh" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json index af4cfbf1b..79d612ff4 100644 --- a/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, - "hasPart": [ - { - "@id": "workflow.ga" - } - ], + "hasPart": { + "@id": "workflow.ga" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json index 362c81473..fd9b4fa9d 100644 --- a/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json index 362c81473..fd9b4fa9d 100644 --- a/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json index b3c407993..c04c0bff3 100644 --- a/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json index 92642673d..17ea52215 100644 --- a/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json @@ -20,11 +20,9 @@ "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, - "hasPart": [ - { - "@id": "my-data-file.txt" - } - ], + "hasPart": { + "@id": "my-data-file.txt" + }, "publisher": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json index 166a1654e..bb253fcf5 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "data/research-data.csv" - } - ], + "hasPart": { + "@id": "data/research-data.csv" + }, "funder": { "@id": "https://ror.org/012345678" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json index 97bff43db..94f936143 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "data/open-data.csv" - } - ], + "hasPart": { + "@id": "data/open-data.csv" + }, "funder": { "@id": "https://ror.org/012345678" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json index 421954310..264d7d103 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "identifier": { "@id": "#uuid" }, - "hasPart": [ - { - "@id": "subdir/" - } - ] + "hasPart": { + "@id": "subdir/" + } }, { "@id": "#uuid", @@ -47,11 +45,9 @@ "@type": "Dataset", "name": "Sub-directory with hasPart", "description": "A local Dataset sub-entity that correctly lists its contents via hasPart.", - "hasPart": [ - { - "@id": "subdir/data.csv" - } - ] + "hasPart": { + "@id": "subdir/data.csv" + } }, { "@id": "subdir/data.csv", diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json index c0d6b3128..479439205 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "identifier": { "@id": "#uuid" }, - "hasPart": [ - { - "@id": "subdir/" - } - ] + "hasPart": { + "@id": "subdir/" + } }, { "@id": "#uuid", @@ -47,11 +45,9 @@ "@type": "Dataset", "name": "Sub-directory", "description": "A local Dataset sub-entity whose @id correctly ends with '/'.", - "hasPart": [ - { - "@id": "subdir/data.csv" - } - ] + "hasPart": { + "@id": "subdir/data.csv" + } }, { "@id": "subdir/data.csv", diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json index 44d0718e9..23010f2dc 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json index 73d8d4ba9..b0999ab6e 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json index 364e3b786..8c53db315 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json index 364e3b786..8c53db315 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json index 8fc1776f2..a388d9fc5 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "https://example.com/data.csv" - } - ], + "hasPart": { + "@id": "https://example.com/data.csv" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json index e099665cb..7083be4a1 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "https://example.com/dataset-dir/" - } - ], + "hasPart": { + "@id": "https://example.com/dataset-dir/" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json index 01ad62a9e..ff2c10d0d 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "https://example.com/landing/dataset" - } - ], + "hasPart": { + "@id": "https://example.com/landing/dataset" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json index 564d070b0..4ed18e3bb 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "https://example.com/data.csv" - } - ], + "hasPart": { + "@id": "https://example.com/data.csv" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json index 7d605e478..fd8ad5c4b 100644 --- a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json @@ -30,11 +30,9 @@ "funder": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "https://example.org/other-ro-crate/" - } - ], + "hasPart": { + "@id": "https://example.org/other-ro-crate/" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json index 512cdfd0d..6936caf75 100644 --- a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json @@ -30,11 +30,9 @@ "funder": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "https://example.org/persistent-crate/" - } - ], + "hasPart": { + "@id": "https://example.org/persistent-crate/" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json index 88e6b56b0..714f2e75d 100644 --- a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json @@ -30,11 +30,9 @@ "funder": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "subcrate/" - } - ], + "hasPart": { + "@id": "subcrate/" + }, "identifier": { "@id": "#uuid" } @@ -59,11 +57,9 @@ "@id": "https://w3id.org/ro/crate/1.2" } ], - "hasPart": [ - { - "@id": "subcrate/data.txt" - } - ], + "hasPart": { + "@id": "subcrate/data.txt" + }, "subjectOf": { "@id": "#subcrate-metadata" } diff --git a/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json index 71cb8c004..8875e0a58 100644 --- a/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json +++ b/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json @@ -26,11 +26,9 @@ "@id": "https://ror.org/123456789" }, "datePublished": "2024-01-01", - "hasPart": [ - { - "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md" - } - ], + "hasPart": { + "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md" + }, "cite-as": { "@id": "https://github.com/crs4/pydoop/tree/develop/" }, diff --git a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json index 3f99ec941..bed26e6bc 100644 --- a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -40,11 +40,9 @@ "about": { "@id": "https://researchobject.org/workflow-run-crate/" }, - "author": [ - { - "@id": "https://orcid.org/0000-0001-8271-5429" - } - ], + "author": { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", "hasPart": [ { @@ -392,11 +390,9 @@ "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" } ], - "author": [ - { - "@id": "https://orcid.org/0000-0001-8271-5429" - } - ], + "author": { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, "creator": { "@id": "https://orcid.org/0000-0001-9842-9718" }, diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index d6eaff044..6e9fe3e6d 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -190,6 +190,21 @@ def test_valid_context_reference(): ) +def test_singleton_entity_property_is_recommended_as_a_single_value(): + """ + Test the RO-Crate 1.2 recommendation that a singleton property array is + represented as a single value in compacted JSON-LD. + """ + do_entity_test( + __metadata_document_crates__.valid_context_reference, + models.Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "hasPart" SHOULD be represented as a single value'], + profile_identifier="ro-crate-1.2", + ) + + def test_not_referenced_contextual_entity(): """ Test that the metadata document is not valid diff --git a/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py b/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py index c3b12dff2..8a86cb0f5 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py +++ b/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py @@ -34,7 +34,7 @@ def test_valid_referenced_rocrate(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_70.1", "ro-crate-1.2_73.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_71.1", "ro-crate-1.2_74.1"], ) @@ -155,7 +155,7 @@ def test_valid_referenced_rocrate_with_identifier(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_70.1", "ro-crate-1.2_73.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_71.1", "ro-crate-1.2_74.1"], ) @@ -170,7 +170,7 @@ def test_valid_referenced_rocrate_with_relative_path(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_70.1", "ro-crate-1.2_73.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_71.1", "ro-crate-1.2_74.1"], ) diff --git a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py index 5ee342bf8..98036267b 100644 --- a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py @@ -15,6 +15,7 @@ import logging from rocrate_validator import models, services +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import InvalidFileDescriptor, ValidROC from tests.shared import do_entity_test @@ -153,7 +154,7 @@ def test_invalid_jsonld_context(): False, ["File Descriptor JSON-LD format"], ["Unable to retrieve the JSON-LD context 'https://w3id.org/ro/terms/invalid/context'"], - profile_identifier="ro-crate", + profile_identifier="ro-crate-1.1", abort_on_first=True, ) @@ -195,3 +196,32 @@ def test_valid_jsonld_custom_term(): which contains custom terms. """ do_entity_test(ValidROC().rocrate_with_custom_terms, models.Severity.REQUIRED, True, [], []) + + +def test_singleton_entity_property_is_recommended_as_a_single_value(): + """ + Test the RO-Crate 1.1 recommendation that a singleton property array is + represented as a single value in compacted JSON-LD. + """ + do_entity_test( + ValidROC().workflow_roc, + models.Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "name" SHOULD be represented as a single value'], + profile_identifier="ro-crate-1.1", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"sort-and-change-case.ga": {"name": ["sort-and-change-case"]}}, + ) + + +def test_singleton_entity_property_remains_valid_at_required_severity(): + """A singleton property array is valid when only REQUIRED checks are run.""" + do_entity_test( + ValidROC().workflow_roc, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.1", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"sort-and-change-case.ga": {"name": ["sort-and-change-case"]}}, + ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py index 5df53d0e3..857aaf628 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -129,7 +129,7 @@ def test_main_workflow_file_existence(): def test_main_workflow_singleton_array(): - """A singleton-array mainEntity resolves to the same workflow as an object.""" + """A singleton-array mainEntity is JSON-LD-equivalent and passes REQUIRED checks.""" do_entity_test( ValidROC().workflow_roc, Severity.REQUIRED, @@ -140,6 +140,20 @@ def test_main_workflow_singleton_array(): ) +def test_main_workflow_singleton_array_should_be_unpacked(): + """RO-Crate 1.1 recommends unpacking a singleton array in compacted JSON-LD.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "mainEntity" SHOULD be represented as a single value'], + profile_identifier="workflow-ro-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + def test_main_workflow_singleton_array_file_existence(): """A missing workflow in a singleton array produces the normal validation issue.""" do_entity_test( diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index b9f8b1c2f..ec872c405 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -224,12 +224,12 @@ def test_valid_local_rocrate(): # test file size size = roc.get_file_size(metadata_file_descriptor) - assert size == 26788, "Size should be 26788" + assert size == 26708, "Size should be 26708" # test crate size — updated after `f8d16ba6` trimmed trailing whitespace # from `index.html` / `ro-crate-preview.html` (~6.7 KB delta from the # historical 311817). - assert roc.size == 305049, "Size should be 305049" + assert roc.size == 304969, "Size should be 304969" # test get_file_content binary mode content = roc.get_file_content(metadata_file_descriptor) From e2bc984662408160535c25a803300c905c58216e Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 9 Sep 2026 11:45:44 +0200 Subject: [PATCH 035/271] update profile and test crates to 1.3 --- .../ro-crate/1.3/may/4_data_entity_metadata.ttl | 2 +- .../ro-crate/1.3/may/61_license_entity.ttl | 2 +- .../1.3/may/6_contextual_entity_metadata.ttl | 10 +++++----- .../1.3/must/0_file_descriptor_format.py | 2 +- .../1.3/must/1_file-descriptor_metadata.ttl | 6 +++--- .../1.3/must/2_root_data_entity_cite_as.py | 2 +- .../1.3/must/2_root_data_entity_haspart.ttl | 4 ++-- .../1.3/must/2_root_data_entity_metadata.ttl | 8 ++++---- .../ro-crate/1.3/must/4_data_entity_metadata.py | 2 +- .../ro-crate/1.3/must/4_data_entity_metadata.ttl | 2 +- .../1.3/must/5_web_data_entity_metadata.ttl | 2 +- .../1.3/must/6_contextual_entity_metadata.ttl | 8 ++++---- .../ro-crate/1.3/must/8_workflow_metadata.ttl | 12 ++++++------ .../profiles/ro-crate/1.3/ontology.ttl | 16 ++++++++-------- .../profiles/ro-crate/1.3/prefixes.ttl | 12 ++++-------- .../profiles/ro-crate/1.3/profile.ttl | 8 ++++---- .../1.3/should/0_entity_identifier_format.py | 8 ++++---- .../ro-crate/1.3/should/0_entity_metadata.ttl | 4 ++-- .../1.3/should/1_file-descriptor_metadata.ttl | 6 +++--- .../1.3/should/2_root_data_entity_conformsto.ttl | 6 +++--- .../1.3/should/2_root_data_entity_funding.ttl | 8 ++++---- .../1.3/should/2_root_data_entity_identifier.py | 2 +- .../1.3/should/2_root_data_entity_identifier.ttl | 2 +- .../1.3/should/2_root_data_entity_metadata.ttl | 4 ++-- .../1.3/should/2_root_data_entity_publisher.ttl | 2 +- .../1.3/should/3_ro_crate_preview_exclusion.ttl | 2 +- .../ro-crate/1.3/should/4_data_entity_license.py | 2 +- .../1.3/should/4_data_entity_metadata.py | 2 +- .../1.3/should/4_data_entity_metadata.ttl | 4 ++-- .../1.3/should/4_dataset_data_entity.ttl | 6 +++--- .../1.3/should/4_missing_file_local_path.py | 2 +- .../ro-crate/1.3/should/4_referenced_rocrate.py | 4 ++-- .../ro-crate/1.3/should/4_referenced_rocrate.ttl | 8 ++++---- .../1.3/should/4_web_dataset_distribution.py | 2 +- .../1.3/should/5_web_data_entity_metadata.ttl | 2 +- .../1.3/should/6_contextual_entity_metadata.ttl | 8 ++++---- .../1.3/should/6_organization_metadata.ttl | 12 ++++++------ .../ro-crate/1.3/should/8_workflow_metadata.ttl | 12 ++++++------ .../image_about/invalid/ro-crate-metadata.json | 4 ++-- .../image_about/valid/ro-crate-metadata.json | 4 ++-- .../invalid/ro-crate-metadata.json | 4 ++-- .../valid/ro-crate-metadata.json | 4 ++-- .../invalid/ro-crate-metadata.json | 4 ++-- .../valid/ro-crate-metadata.json | 4 ++-- .../script_name/invalid/ro-crate-metadata.json | 4 ++-- .../script_name/valid/ro-crate-metadata.json | 4 ++-- .../script_type/invalid/ro-crate-metadata.json | 4 ++-- .../script_type/valid/ro-crate-metadata.json | 4 ++-- .../invalid/ro-crate-metadata.json | 4 ++-- .../valid/ro-crate-metadata.json | 4 ++-- .../workflow_name/invalid/ro-crate-metadata.json | 4 ++-- .../workflow_name/valid/ro-crate-metadata.json | 4 ++-- .../invalid_missing_file/ro-crate-metadata.json | 4 ++-- .../invalid_missing_ssc/ro-crate-metadata.json | 4 ++-- .../workflow_type/valid/ro-crate-metadata.json | 4 ++-- 55 files changed, 135 insertions(+), 139 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl index 488dbfb16..72ee24c48 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl @@ -13,7 +13,7 @@ # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix sh: . @prefix xsd: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl index 4679f9521..8c85ea1a7 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl @@ -13,7 +13,7 @@ # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl index 588e4207b..842e1c885 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl @@ -11,12 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema: . @prefix sh: . -# RO-Crate 1.2, Encoding Format Entity — MAY include `WebPageElement` in +# RO-Crate 1.3, Encoding Format Entity — MAY include `WebPageElement` in # `@type` if the `@id` links to a section of a webpage (i.e. the URI carries # a fragment identifier such as `https://example.org/page.html#section-foo`). # This is purely optional; the shape emits an `sh:Info` suggestion — never a @@ -27,7 +27,7 @@ ro-crate:EncodingFormatOptionalWebPageElement a sh:NodeShape ; sh:description """An encoding format contextual entity whose `@id` links to a section of a webpage (the URI contains a fragment identifier) MAY include `WebPageElement` in its `@type` - (RO-Crate 1.2, Encoding Format Entity).""" ; + (RO-Crate 1.3, Encoding Format Entity).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; @@ -50,7 +50,7 @@ ro-crate:EncodingFormatOptionalWebPageElement a sh:NodeShape ; sh:message "An encoding format entity whose `@id` is a section of a webpage MAY include `WebPageElement` in its `@type`" ; ] . -# RO-Crate 1.2, Software Applications / ComputerLanguage — `alternateName` +# RO-Crate 1.3, Software Applications / ComputerLanguage — `alternateName` # MAY be present if there is a shorter colloquial name for the software or # language (e.g. `"alternateName": "GNU Bourne-Again Shell"` for Bash). # Emits an `sh:Info` suggestion when the property is missing. @@ -59,7 +59,7 @@ ro-crate:SoftwareApplicationOrComputerLanguageOptionalAlternateName a sh:NodeSha sh:description """A contextual entity representing a `SoftwareApplication` or a `ComputerLanguage` MAY declare an `alternateName` property to provide a shorter colloquial name for the software or language - (RO-Crate 1.2, Provenance — Software Applications).""" ; + (RO-Crate 1.3, Provenance — Software Applications).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py index 5c230f089..7692331f0 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py @@ -261,7 +261,7 @@ def check_context(self, context: ValidationContext) -> bool: ) return False - expected_context = "https://w3id.org/ro/crate/1.2/context" + expected_context = "https://w3id.org/ro/crate/1.3/context" jsonld_context = json_dict.get("@context") def has_expected_context(ctx: object) -> bool: diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl index 6c90fb03a..7912fc66d 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix dct: . @prefix rdf: . @prefix schema_org: . @@ -23,7 +23,7 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape ; sh:name "Identify the RO-Crate Metadata File Descriptor" ; sh:description """The RO-Crate Metadata File Descriptor entity describes the RO-Crate itself, and it is named as `*-ro-crate-metadata.json`. It can be identified by name according to the RO-Crate specification - available at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.2/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores).""" ; + available at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.3/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; @@ -64,7 +64,7 @@ ro-crate:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Metadata File Descriptor REQUIRED properties" ; sh:description """RO-Crate Metadata Descriptor MUST be defined according with the requirements details defined in - [RO-Crate Metadata File Descriptor](https://www.researchobject.org/ro-crate/1.2/root-data-entity.html#ro-crate-metadata-file-descriptor)""" ; + [RO-Crate Metadata File Descriptor](https://www.researchobject.org/ro-crate/1.3/root-data-entity.html#ro-crate-metadata-file-descriptor)""" ; sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; sh:property [ a sh:PropertyShape ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py index a5558f926..f253bb273 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py @@ -26,7 +26,7 @@ class CiteAsDownloadableChecker(PyFunctionCheck): If present, the `cite-as` value of the Root Data Entity MUST ultimately provide the RO-Crate as a downloadable item, accessible via Signposting (Link rel="item" or rel="describedby"), direct download, or content - negotiation (RO-Crate 1.2, RFC 8574). + negotiation (RO-Crate 1.3, RFC 8574). """ @staticmethod diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl index 95bffb33a..04c2b8973 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl @@ -11,14 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema_org: . @prefix sh: . ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; sh:description """The Root Data Entity MUST directly or indirectly reference - all Data Entities in the RO-Crate via hasPart (RO-Crate 1.2).""" ; + all Data Entities in the RO-Crate via hasPart (RO-Crate 1.3).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl index 68ce7aaf3..ad8c31f9f 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix prof: . @@ -36,7 +36,7 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape ; sh:name "Identify the Root Data Entity of the RO-Crate" ; 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. It is a schema:Dataset and is indirectly identified by the about property of the metadata descriptor in the RO-Crate - (see the definition at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.2/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores)). + (see the definition at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.3/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores)). """ ; sh:target [ a sh:SPARQLTarget ; @@ -153,7 +153,7 @@ ro-crate:RootDataEntityVersionlessProfileProhibition a sh:NodeShape ; sh:name "RO-Crate Root Data Entity: MUST NOT declare version-less conformsTo profile" ; sh:description """The Root Data Entity MUST NOT declare the version-less generic RO-Crate profile URI (https://w3id.org/ro/crate) in its conformsTo property. - Only versioned profile URIs (e.g., https://w3id.org/ro/crate/1.2) are permitted on the Root Data Entity. + Only versioned profile URIs (e.g., https://w3id.org/ro/crate/1.3) are permitted on the Root Data Entity. The version-less URI is reserved for referenced RO-Crate data entities.""" ; sh:target ro-crate:MetadataAboutTarget ; sh:sparql [ @@ -166,5 +166,5 @@ ro-crate:RootDataEntityVersionlessProfileProhibition a sh:NodeShape ; } """ ; sh:name "Root Data Entity: MUST NOT declare version-less conformsTo" ; - sh:message "The Root Data Entity MUST NOT declare the version-less RO-Crate profile URI (https://w3id.org/ro/crate) in its conformsTo property. Use a versioned URI such as https://w3id.org/ro/crate/1.2 instead." ; + sh:message "The Root Data Entity MUST NOT declare the version-less RO-Crate profile URI (https://w3id.org/ro/crate) in its conformsTo property. Use a versioned URI such as https://w3id.org/ro/crate/1.3 instead." ; ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py index 780ab17f1..2a22782fe 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py @@ -229,7 +229,7 @@ def check_citation(self, context: ValidationContext) -> bool: class WebDataEntityRequiredChecker(PyFunctionCheck): """ Web-based Data Entities MUST be directly downloadable at the time of creation - (RO-Crate 1.2). Downloadability is verified via Signposting (rel=item, + (RO-Crate 1.3). Downloadability is verified via Signposting (rel=item, rel=describedby), direct Content-Type inspection, and content negotiation. """ diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl index ada5c742c..ca650bb21 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl index db63676ad..d1a296bd3 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl @@ -13,7 +13,7 @@ # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix dct: . @prefix schema_org: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl index 80168a0b1..316f540ab 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl @@ -14,7 +14,7 @@ @prefix owl: . @prefix rdf: . @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema: . @prefix schema_org: . @prefix sh: . @@ -56,7 +56,7 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; FILTER NOT EXISTS { ?this a owl:Ontology } # Vocabulary / profile namespace exclusions. Required because - # the RO-Crate 1.2 profile loads ontology.ttl into the same data + # the RO-Crate 1.3 profile loads ontology.ttl into the same data # graph as the metadata file; terms from those vocabularies # appear as subjects with rdf:type + rdfs:label and would # otherwise be misclassified. @@ -65,7 +65,7 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) + FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/terms/")) FILTER(!STRSTARTS(STR(?this), "http://www.opengis.net/")) FILTER(!STRSTARTS(STR(?this), "https://w3id.org/")) FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) @@ -171,7 +171,7 @@ ro-crate:SoftwareApplicationOrComputerLanguageRequiredProperties a sh:NodeShape sh:name "SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`" ; sh:description """A contextual entity representing a `SoftwareApplication` or a `ComputerLanguage` MUST declare `name`, `url`, and `version` - properties (RO-Crate 1.2, Provenance — Software Applications; + properties (RO-Crate 1.3, Provenance — Software Applications; Workflows — programmingLanguage).""" ; sh:target [ a sh:SPARQLTarget ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl index 3e1aa66e1..14b37cf6d 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema: . -@prefix bioschemas: . +@prefix bioschemas: . @prefix sh: . @prefix xsd: . @prefix validator: . @@ -98,7 +98,7 @@ ro-crate:ScriptRequiredFileType a sh:NodeShape ; sh:name "Script: REQUIRED `File` type" ; sh:description """A Script entity MUST include `File` (schema:MediaObject) in its @type alongside `SoftwareSourceCode` - (RO-Crate 1.2, Workflows and Scripts — Script).""" ; + (RO-Crate 1.3, Workflows and Scripts — Script).""" ; sh:targetClass ro-crate:Script ; sh:property [ a sh:PropertyShape ; @@ -119,7 +119,7 @@ ro-crate:WorkflowRequiredFileType a sh:NodeShape ; sh:name "Workflow: REQUIRED `File` type" ; sh:description """A Workflow entity MUST include `File` (schema:MediaObject) in its @type alongside `SoftwareSourceCode` and `ComputationalWorkflow` - (RO-Crate 1.2, Workflows and Scripts — Workflow).""" ; + (RO-Crate 1.3, Workflows and Scripts — Workflow).""" ; sh:targetClass ro-crate:Workflow ; sh:property [ a sh:PropertyShape ; @@ -140,7 +140,7 @@ ro-crate:WorkflowRequiredSoftwareSourceCodeType a sh:NodeShape ; sh:name "Workflow: REQUIRED `SoftwareSourceCode` type" ; sh:description """A Workflow entity MUST include `SoftwareSourceCode` in its @type alongside `File` and `ComputationalWorkflow` - (RO-Crate 1.2, Workflows and Scripts — Workflow).""" ; + (RO-Crate 1.3, Workflows and Scripts — Workflow).""" ; sh:targetClass ro-crate:Workflow ; sh:property [ a sh:PropertyShape ; @@ -160,7 +160,7 @@ ro-crate:WorkflowRequiredSoftwareSourceCodeType a sh:NodeShape ; ro-crate:ScriptOrWorkflowRequiredName a sh:NodeShape ; sh:name "Script or Workflow: REQUIRED `name`" ; sh:description """Scripts and Workflows MUST have a human-readable `name` - property (RO-Crate 1.2, Workflows and Scripts).""" ; + property (RO-Crate 1.3, Workflows and Scripts).""" ; sh:target ro-crate:SoftwareSourceCodeTarget ; sh:property [ a sh:PropertyShape ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl index 84331ccc7..4db6846f3 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl @@ -19,8 +19,8 @@ @prefix xsd: . @prefix rdfs: . @prefix schema: . -@prefix ro-crate: . -@prefix bioschemas: . +@prefix ro-crate: . +@prefix bioschemas: . # @base <./.> . rdf:type owl:Ontology ; @@ -50,35 +50,35 @@ schema:SoftwareSourceCode rdf:type owl:Class ; rdfs:subClassOf schema:CreativeWork . -### https://bioschemas.org/ComputationalWorkflow +### https://bioschemas.org/terms/ComputationalWorkflow bioschemas:ComputationalWorkflow rdf:type owl:Class . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Script +### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.3/Script ro-crate:Script rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject ; rdfs:label "Script"@en . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Workflow +### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.3/Workflow ro-crate:Workflow rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject, bioschemas:ComputationalWorkflow ; rdfs:label "Workflow"@en . -### https://w3id.org/ro/crate/1.2/DataEntity +### https://w3id.org/ro/crate/1.3/DataEntity ro-crate:DataEntity rdf:type owl:Class ; rdfs:subClassOf schema:CreativeWork ; rdfs:label "DataEntity"@en . -# # ### https://w3id.org/ro/crate/1.2/Directory +# # ### https://w3id.org/ro/crate/1.3/Directory ro-crate:Directory rdf:type owl:Class ; rdfs:subClassOf schema:Dataset ; rdfs:label "Directory"@en . -### https://w3id.org/ro/crate/1.2/ContextualEntity +### https://w3id.org/ro/crate/1.3/ContextualEntity ro-crate:ContextualEntity rdf:type owl:Class ; rdfs:subClassOf schema:Thing ; rdfs:label "ContextualEntity"@en . diff --git a/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl b/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl index 8cad6b4dd..f6f8da2c0 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl @@ -14,7 +14,7 @@ @prefix sh: . @prefix xsd: . -@prefix ro-crate: . +@prefix ro-crate: . # Define the prefixes used in the SPARQL queries ro-crate:sparqlPrefixes sh:declare [ @@ -23,15 +23,11 @@ ro-crate:sparqlPrefixes sh:declare [ ] ; sh:declare [ sh:prefix "bioschemas" ; - sh:namespace "https://bioschemas.org/"^^xsd:anyURI ; - ] ; - sh:declare [ - sh:prefix "bioschemas-cw" ; - sh:namespace "https://bioschemas.org/ComputationalWorkflow#"^^xsd:anyURI ; + sh:namespace "https://bioschemas.org/terms/"^^xsd:anyURI ; ] ; sh:declare [ sh:prefix "rocrate" ; - sh:namespace "https://w3id.org/ro/crate/1.2/"^^xsd:anyURI ; + sh:namespace "https://w3id.org/ro/crate/1.3/"^^xsd:anyURI ; ] ; sh:declare [ sh:prefix "wfrun" ; @@ -47,7 +43,7 @@ ro-crate:sparqlPrefixes sh:declare [ ] ; sh:declare [ sh:prefix "ro-crate" ; - sh:namespace "https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/"^^xsd:anyURI ; + sh:namespace "https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/"^^xsd:anyURI ; ] ; sh:declare [ sh:prefix "dct" ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/profile.ttl b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl index b87a59bfe..85b32a263 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/profile.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl @@ -19,13 +19,13 @@ - # a Profile; it's identifying URI + # a Profile; it's identifying URI a prof:Profile ; # common metadata for the Profile # the Profile's label - rdfs:label "RO-Crate Metadata Specification 1.2" ; + rdfs:label "RO-Crate Metadata Specification 1.3" ; # regular metadata, a basic description of the Profile rdfs:comment """RO-Crate Metadata Specification."""@en ; @@ -48,7 +48,7 @@ prof:hasRole role:Vocabulary ; # this profile resource's actual file - prof:hasArtifact ; + prof:hasArtifact ; ] ; # this profile has a human-readable documentation resource @@ -66,7 +66,7 @@ prof:hasRole role:Specification ; # this profile resource's actual file - prof:hasArtifact ; + prof:hasArtifact ; ] ; # a short code to refer to the Profile with when a URI can't be used diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py index 6f9d8745b..c9cdb2de3 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py @@ -40,7 +40,7 @@ @requirement(name="Entity identifier: format recommendations") class EntityIdentifierFormatChecker(PyFunctionCheck): """ - Checks that entity @id values follow RO-Crate 1.2 RECOMMENDED conventions: + Checks that entity @id values follow RO-Crate 1.3 RECOMMENDED conventions: no parent-directory traversal, native UTF-8 for international characters, and '#'-prefixed identifiers for named local entities. """ @@ -49,7 +49,7 @@ class EntityIdentifierFormatChecker(PyFunctionCheck): def check_no_parent_traversal(self, context: ValidationContext) -> bool: """ @id paths SHOULD NOT use `../` to climb out of the RO-Crate Root - (RO-Crate 1.2, JSON-LD appendix). + (RO-Crate 1.3, JSON-LD appendix). """ result = True for entity in context.ro_crate.metadata.as_dict().get("@graph", []): @@ -72,7 +72,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: def check_utf8_identifiers(self, context: ValidationContext) -> bool: """ International characters in @id values SHOULD be written in native - UTF-8 rather than percent-encoded (RO-Crate 1.2, JSON-LD appendix). + UTF-8 rather than percent-encoded (RO-Crate 1.3, JSON-LD appendix). """ result = True for entity in context.ro_crate.metadata.as_dict().get("@graph", []): @@ -98,7 +98,7 @@ def check_named_entity_id_format(self, context: ValidationContext) -> bool: Any Contextual Entity (Person, Organization, ContactPoint, PropertyValue, Place, etc.) SHOULD use an @id that is an absolute URI (permalink), a '#'-prefixed local identifier, or a blank node — not a bare relative - path (RO-Crate 1.2, 5.1 Any Contextual Entity). + path (RO-Crate 1.3, 5.1 Any Contextual Entity). """ result = True ro_crate_metadata = context.ro_crate.metadata diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl index 007c0f5cb..3f684ef91 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix sh: . @prefix schema: . @prefix xsd: . @@ -40,7 +40,7 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) + FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/terms/")) FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) FILTER(!STRSTARTS(STR(?this), "urn:")) } diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl index d6d7e42bf..3620c9567 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl @@ -13,7 +13,7 @@ # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix dct: . @prefix sh: . @@ -21,7 +21,7 @@ ro-crate:ROCrateMetadataFileDescriptorConformsTo a sh:NodeShape ; sh:name "RO-Crate Metadata File Descriptor RECOMMENDED conformsTo" ; sh:description """The RO-Crate Metadata Descriptor SHOULD indicate the versioned RO-Crate specification URI. - In RO-Crate 1.2, conformsTo SHOULD have a single value which is a versioned permalink URI + In RO-Crate 1.3, conformsTo SHOULD have a single value which is a versioned permalink URI of the RO-Crate specification that the RO-Crate JSON-LD conforms to. The URI SHOULD start with https://w3id.org/ro/crate/.""" ; sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; @@ -33,7 +33,7 @@ ro-crate:ROCrateMetadataFileDescriptorConformsTo sh:severity sh:Warning ; sh:nodeKind sh:IRI ; sh:path dct:conformsTo ; - sh:hasValue ; + sh:hasValue ; sh:message "The RO-Crate metadata file descriptor SHOULD have a `conformsTo` property with the RO-Crate specification version" ; ] ; sh:property [ diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl index 344bafc5b..db398b1db 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema_org: . @prefix dct: . @prefix prof: . @@ -22,7 +22,7 @@ ro-crate:RootDataEntityConformsToIfProfiles a sh:NodeShape ; sh:name "Root Data Entity: conformsTo SHOULD be present if profiles exist" ; sh:description """If the RO-Crate conforms to additional profiles, the Root Data Entity SHOULD have a conformsTo property referencing - Profile entities (RO-Crate 1.2).""" ; + Profile entities (RO-Crate 1.3).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; @@ -32,7 +32,7 @@ ro-crate:RootDataEntityConformsToIfProfiles a sh:NodeShape ; ?this a ro-crate:RootDataEntity . FILTER EXISTS { ?profile a prof:Profile . - FILTER(?profile != ) + FILTER(?profile != ) } } """ diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl index 25d708a93..006076510 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . @@ -22,7 +22,7 @@ ro-crate:RootDataEntityRecommendedFunder a sh:NodeShape ; monetary grant (via a `schema:Project`, `schema:Grant`, or `schema:MonetaryGrant` entity in the @graph), the Root Data Entity SHOULD reference the funding source via the `funder` property - (RO-Crate 1.2, Funding and Grants).""" ; + (RO-Crate 1.3, Funding and Grants).""" ; sh:targetClass ro-crate:RootDataEntity ; sh:sparql [ a sh:SPARQLConstraint ; @@ -49,7 +49,7 @@ ro-crate:RootDataEntityRecommendedFunder a sh:NodeShape ; ro-crate:FunderEntityRecommendedType a sh:NodeShape ; sh:name "Funder entity: recommended Organization type" ; sh:description """Entities referenced via `funder` from the Root Data Entity SHOULD be of - type Organization (RO-Crate 1.2, Funding and Grants — K1/K2).""" ; + type Organization (RO-Crate 1.3, Funding and Grants — K1/K2).""" ; sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; @@ -68,7 +68,7 @@ ro-crate:ProjectOrganizationRecommendedFunder a sh:NodeShape ; sh:name "Project Organization: recommended funder reference" ; sh:description """An Organization entity referenced via `funder` from the Root Data Entity SHOULD itself reference the external funding body via its own `funder` property - (RO-Crate 1.2, Funding and Grants — K2).""" ; + (RO-Crate 1.3, Funding and Grants — K2).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py index c957b475a..54aa81243 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py @@ -136,7 +136,7 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): If the Root Data Entity has an ``identifier`` property with a resolvable HTTP URL, resolving that URL SHOULD ultimately provide the RO-Crate Metadata Document or an archive, accessible via Signposting or content negotiation - (RO-Crate 1.2, RECOMMENDED). + (RO-Crate 1.3, RECOMMENDED). """ @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl index 115aa861d..b5d4e3393 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema_org: . @prefix sh: . @prefix xsd: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl index 986d71d47..1db9c7507 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl @@ -11,14 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema_org: . @prefix sh: . ro-crate:RootDataEntityDatePublishedDayPrecision a sh:NodeShape ; sh:name "Root Data Entity: datePublished SHOULD specify at least day precision" ; sh:description """The Root Data Entity SHOULD specify datePublished to at least - the precision of a day (YYYY-MM-DD) per RO-Crate 1.2.""" ; + the precision of a day (YYYY-MM-DD) per RO-Crate 1.3.""" ; sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl index baa88dbc9..1f1f4a281 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl index 37a94d76c..8ae52d716 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema_org: . @prefix sh: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py index 2ab81a7bc..9e8a0470b 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py @@ -16,7 +16,7 @@ # NOTE # ================================================================== # -# The RO-Crate 1.2 specification (section on Licensing) states: +# The RO-Crate 1.3 specification (section on Licensing) states: # # "Data Entities with different license SHOULD have own license property" # (requirement L4 / P2.2) diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py index 713cf47fe..ecffd0a6f 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py @@ -61,7 +61,7 @@ def check_citation(self, context: ValidationContext) -> bool: @requirement(name="Web-based Data Entity: REQUIRED availability") class WebDataEntityRequiredChecker(PyFunctionCheck): """ - Web-based Data Entities SHOULD be directly downloadable (RO-Crate 1.2). + Web-based Data Entities SHOULD be directly downloadable (RO-Crate 1.3). Downloadability is checked via Signposting, Content-Type, and content negotiation. Entities returning an HTML page (splash page / viewer) are also flagged. """ diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl index bcb836835..563cb53f1 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix dct: . @@ -157,7 +157,7 @@ ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; sh:name "Web-based Directory Data Entity: recommended distribution" ; sh:description """If a non-root Dataset Data Entity has a web URI (absolute https?://), it SHOULD include a `distribution` property pointing to a downloadable archive - (RO-Crate 1.2, Data Entities — Directories on the web).""" ; + (RO-Crate 1.3, Data Entities — Directories on the web).""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl index d46263091..7ddaedc21 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -@prefix ro-crate: . +@prefix ro-crate: . @prefix schema_org: . @prefix sh: . @@ -34,7 +34,7 @@ ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; ro-crate:DirectoryDataEntityTrailingSlash a sh:NodeShape ; sh:name "Dataset Data Entity: SHOULD end with trailing slash" ; sh:description """A Dataset Data Entity whose @id is a relative URI SHOULD end with '/' - (RO-Crate 1.2, Dataset Data Entities).""" ; + (RO-Crate 1.3, Dataset Data Entities).""" ; sh:target ro-crate:LocalDatasetTarget ; sh:sparql [ a sh:SPARQLConstraint ; @@ -55,7 +55,7 @@ ro-crate:DirectoryDataEntityTrailingSlash a sh:NodeShape ; ro-crate:DirectoryDataEntityHasPart a sh:NodeShape ; sh:name "Dataset Data Entity: SHOULD have hasPart" ; sh:description """A local Dataset Data Entity SHOULD use `hasPart` to list - the Data Entities it contains (RO-Crate 1.2, Dataset Data Entities).""" ; + the Data Entities it contains (RO-Crate 1.3, Dataset Data Entities).""" ; sh:target ro-crate:LocalDatasetTarget ; sh:property [ a sh:PropertyShape ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py index 08bbe75ce..f1276ea53 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py @@ -49,7 +49,7 @@ class MissingFileLocalPathChecker(PyFunctionCheck): Additionally, a File Data Entity whose ``@id`` begins with ``#`` denotes a deliberately absent file; in that case ``localPath`` SHOULD also be declared to indicate where the file can be found when it exists. - (RO-Crate 1.2 specification, section on File Data Entities.) + (RO-Crate 1.3 specification, section on File Data Entities.) """ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py index 377d0fe0f..76c4d11c9 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py @@ -17,7 +17,7 @@ The structural SHACL shape `ReferencedROCrateSdDatePublishedRecommended` already warns when a referenced RO-Crate data entity with an absolute URI -@id omits both `identifier` and `sdDatePublished`. RO-Crate 1.2 § 4.5 +@id omits both `identifier` and `sdDatePublished`. RO-Crate 1.3 § 4.5 relaxes that obligation when the URI declares Signposting `Link: rel="cite-as"` (which supplies a persistent citation surrogate). This Python check performs the network-dependent refinement. @@ -35,7 +35,7 @@ class ReferencedROCrateSignpostingCiteAsChecker(PyFunctionCheck): """ Network-aware refinement of the `sdDatePublished` requirement for - referenced RO-Crate data entities (RO-Crate 1.2, § 4.5). + referenced RO-Crate data entities (RO-Crate 1.3, § 4.5). """ @staticmethod diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl index 26a96b3a2..52ce8d6c2 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix dct: . @@ -65,7 +65,7 @@ ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; # These shapes validate data entities that represent referenced RO-Crates. # A "referenced RO-Crate" is a Dataset entity whose conformsTo property # includes an RO-Crate specification URI (starting with -# https://w3id.org/ro/crate). The RO-Crate 1.2 specification requires +# https://w3id.org/ro/crate). The RO-Crate 1.3 specification requires # that such entities follow specific conventions regarding conformsTo, # subjectOf, and the structure of their associated metadata descriptor. @@ -122,7 +122,7 @@ ro-crate:ReferencedROCrateSubjectOfRecommended a sh:NodeShape ; # accessed (unless Signposting rel=cite-as is declared — that # conditional refinement is verified separately by the Python # check `ReferencedROCrateSignpostingCiteAsChecker`). -# (RO-Crate 1.2, § 4.5 — Determining entity identifier for a +# (RO-Crate 1.3, § 4.5 — Determining entity identifier for a # referenced RO-Crate, step #3.) # --------------------------------------------------------------- ro-crate:ReferencedROCrateSdDatePublishedRecommended a sh:NodeShape ; @@ -131,7 +131,7 @@ ro-crate:ReferencedROCrateSdDatePublishedRecommended a sh:NodeShape ; whose @id is an absolute HTTP(S) URI — and that does not declare a persistent identifier via `identifier` — SHOULD include the `sdDatePublished` timestamp to indicate when the URI was accessed - (RO-Crate 1.2, § 4.5). This structural check does not inspect + (RO-Crate 1.3, § 4.5). This structural check does not inspect Signposting `Link: rel=cite-as` headers; that refinement is performed by a separate network-aware Python check.""" ; sh:target ro-crate:ReferencedROCrateDataEntityTarget ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py index 810b1646c..2c548512d 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py @@ -53,7 +53,7 @@ def _resolve_distribution_url(dist) -> str | None: class DatasetDistributionChecker(PyFunctionCheck): """ If a Dataset declares a ``distribution`` property pointing to a DataDownload - entity, the referenced archive SHOULD be directly downloadable (RO-Crate 1.2, + entity, the referenced archive SHOULD be directly downloadable (RO-Crate 1.3, RECOMMENDED). Downloadability is verified via Signposting, Content-Type, and content negotiation. """ diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl index 26720849f..8eb0eacdf 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl @@ -13,7 +13,7 @@ # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl index 3103fffc3..57512af55 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema: . @prefix sh: . @@ -77,7 +77,7 @@ ro-crate:ReferencedContextualEntitiesShouldBeDescribed a sh:NodeShape ; # ro-crate:EntityNameRecommendedProperties a sh:NodeShape ; # sh:name "Entity: RECOMMENDED name" ; # sh:description """Entities SHOULD have a human-readable `name`, in particular if its `@id` does not go to a human-readable Web page. -# This follows the RO-Crate 1.2 Common Principles for RO-Crate entities.""" ; +# This follows the RO-Crate 1.3 Common Principles for RO-Crate entities.""" ; # sh:targetClass ro-crate:ContextualEntity ; # sh:property [ # sh:path schema:name ; @@ -213,7 +213,7 @@ ro-crate:LicenseEntityAbsoluteUrl a sh:NodeShape ; sh:name "License entity: SHOULD have absolute URL @id" ; sh:description """A License entity SHOULD be identified by an absolute HTTP(S) URL, for example an SPDX license URI (https://spdx.org/licenses/). - (RO-Crate 1.2, Contextual Entities — License)""" ; + (RO-Crate 1.3, Contextual Entities — License)""" ; sh:targetClass ro-crate:License ; sh:sparql [ a sh:SPARQLConstraint ; @@ -231,7 +231,7 @@ ro-crate:LicenseEntityAbsoluteUrl a sh:NodeShape ; ro-crate:LicenseEntityCreativeWorkType a sh:NodeShape ; sh:name "License entity: SHOULD be typed as CreativeWork" ; sh:description """A License entity SHOULD include `schema:CreativeWork` in its `@type`. - (RO-Crate 1.2, Contextual Entities — License)""" ; + (RO-Crate 1.3, Contextual Entities — License)""" ; sh:targetClass ro-crate:License ; sh:sparql [ a sh:SPARQLConstraint ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl index 48e338c73..b62683f8f 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema: . @prefix sh: . @@ -22,7 +22,7 @@ ro-crate:OrganizationRorIdentifier a sh:NodeShape ; sh:name "Organization: SHOULD have ROR identifier" ; sh:description """An Organization entity SHOULD use a ROR (Research Organization Registry) identifier as its @id, if possible. - (RO-Crate 1.2, Contextual Entities — Organizations as values; Metadata — Recommended Identifiers)""" ; + (RO-Crate 1.3, Contextual Entities — Organizations as values; Metadata — Recommended Identifiers)""" ; sh:targetClass schema:Organization ; sh:sparql [ a sh:SPARQLConstraint ; @@ -40,7 +40,7 @@ ro-crate:OrganizationRorIdentifier a sh:NodeShape ; ro-crate:OrganizationContactPoint a sh:NodeShape ; sh:name "Organization: SHOULD have contactPoint referencing ContactPoint" ; sh:description """An Organization SHOULD have a contactPoint property referencing a ContactPoint contextual entity. - (RO-Crate 1.2, Contextual Entities — Contact Information)""" ; + (RO-Crate 1.3, Contextual Entities — Contact Information)""" ; sh:targetClass schema:Organization ; sh:property [ sh:path schema:contactPoint ; @@ -55,7 +55,7 @@ ro-crate:OrganizationContactPoint a sh:NodeShape ; ro-crate:PersonContactPoint a sh:NodeShape ; sh:name "Person: SHOULD have contactPoint referencing ContactPoint" ; sh:description """A Person's contactPoint property SHOULD reference a ContactPoint contextual entity. - (RO-Crate 1.2, Contextual Entities — Contact Information)""" ; + (RO-Crate 1.3, Contextual Entities — Contact Information)""" ; sh:targetClass schema:Person ; sh:property [ sh:path schema:contactPoint ; @@ -70,7 +70,7 @@ ro-crate:AuthorPublisherContactPoint a sh:NodeShape ; sh:name "Root Data Entity: at least one author/publisher SHOULD have contactPoint" ; sh:description """At least one of the Person or Organization entities referenced as author or publisher from the Root Data Entity SHOULD have a contactPoint property. - (RO-Crate 1.2, Contextual Entities — Contact Information)""" ; + (RO-Crate 1.3, Contextual Entities — Contact Information)""" ; sh:targetClass ro-crate:RootDataEntity ; sh:sparql [ a sh:SPARQLConstraint ; @@ -101,7 +101,7 @@ ro-crate:AuthorPublisherContactPoint a sh:NodeShape ; ro-crate:PersonOrcidIdentifier a sh:NodeShape ; sh:name "Person: SHOULD have ORCID identifier" ; sh:description """A Person entity SHOULD use an ORCID identifier as its @id. - (RO-Crate 1.2, Contextual Entities — People)""" ; + (RO-Crate 1.3, Contextual Entities — People)""" ; sh:targetClass schema:Person ; sh:sparql [ a sh:SPARQLConstraint ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl index c4a9aeebd..b20e51ac0 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. @prefix ro: <./> . -@prefix ro-crate: . +@prefix ro-crate: . @prefix rdf: . @prefix schema: . -@prefix bioschemas: . +@prefix bioschemas: . @prefix dct: . @prefix sh: . @prefix xsd: . @@ -42,7 +42,7 @@ ro-crate:ScriptOrWorkflowProgrammingLanguage a sh:NodeShape ; sh:name "Script or Workflow: RECOMMENDED `programmingLanguage`" ; sh:description """Scripts and Workflows SHOULD have a `programmingLanguage` property referencing a contextual entity of type `ComputerLanguage` - (RO-Crate 1.2, Workflows and Scripts).""" ; + (RO-Crate 1.3, Workflows and Scripts).""" ; sh:target ro-crate:SoftwareSourceCodeTarget ; sh:property [ a sh:PropertyShape ; @@ -80,7 +80,7 @@ ro-crate:WorkflowRecommendedConformsTo a sh:NodeShape ; sh:description """A Workflow SHOULD declare `conformsTo` pointing to a versioned Bioschemas ComputationalWorkflow profile URI (https://bioschemas.org/profiles/ComputationalWorkflow/) - (RO-Crate 1.2, Workflows and Scripts).""" ; + (RO-Crate 1.3, Workflows and Scripts).""" ; sh:targetClass ro-crate:Workflow ; sh:property [ a sh:PropertyShape ; @@ -121,7 +121,7 @@ ro-crate:ScriptOrWorkflowImageEncodingFormat a sh:NodeShape ; sh:description """ImageObject entities referenced via `image` from a Script or Workflow SHOULD have an `encodingFormat` property with an IANA media type string and/or a Pronom identifier reference - (RO-Crate 1.2, Workflows and Scripts — related ImageObject).""" ; + (RO-Crate 1.3, Workflows and Scripts — related ImageObject).""" ; sh:target ro-crate:ScriptOrWorkflowImageTarget ; sh:property [ a sh:PropertyShape ; @@ -143,7 +143,7 @@ ro-crate:ScriptOrWorkflowImageAbout a sh:NodeShape ; sh:description """ImageObject entities referenced via `image` from a Script or Workflow SHOULD have an `about` property that references the script or workflow entity - (RO-Crate 1.2, Workflows and Scripts — related ImageObject).""" ; + (RO-Crate 1.3, Workflows and Scripts — related ImageObject).""" ; sh:target ro-crate:ScriptOrWorkflowImageTarget ; sh:sparql [ a sh:SPARQLConstraint ; diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json index 92ec33c49..813557ec3 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json index 016778ae5..aa7b84e36 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json index 0a98560be..a501f242b 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json index 651d9abb6..9c332570e 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json index 9b21b0539..daec94202 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json index 1ffe2309e..6d3a142bb 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json index 0729de7f1..9dcbc2843 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json index 07edea638..48fe94ed7 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json index ffe4e0a4e..e201ae943 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json index 536dcb556..f60a9f3ca 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json index f0c9bfbe9..7eec1660a 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json index af4cfbf1b..afe7781c3 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json index 52ada9885..f513ae717 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json index 943eedc77..94ad2d688 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json index e1c23f8eb..0aea36a81 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json index 0fb8daeb1..645a9f1a8 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json @@ -1,11 +1,11 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": { "@id": "./" }, - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.2" } + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } }, { "@id": "./", diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json index d9dc02c30..a11a1a3a6 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json @@ -1,5 +1,5 @@ { - "@context": "https://w3id.org/ro/crate/1.2/context", + "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@id": "ro-crate-metadata.json", @@ -8,7 +8,7 @@ "@id": "./" }, "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" + "@id": "https://w3id.org/ro/crate/1.3" } }, { From 3386cf4b8ccd88c8ddf991187da7b67465e937ea Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 9 Sep 2026 12:06:41 +0200 Subject: [PATCH 036/271] add tests/ro_crates_v1_3.py --- tests/ro_crates_v1_3.py | 111 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/ro_crates_v1_3.py diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py new file mode 100644 index 000000000..3d9d9c9c2 --- /dev/null +++ b/tests/ro_crates_v1_3.py @@ -0,0 +1,111 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +from pytest import fixture + +CURRENT_PATH = Path(__file__).resolve().parent +TEST_DATA_PATH = (CURRENT_PATH / "data").absolute() +CRATES_DATA_PATH = TEST_DATA_PATH / "crates" +VALID_CRATES_DATA_PATH = CRATES_DATA_PATH / "valid" +INVALID_CRATES_DATA_PATH = CRATES_DATA_PATH / "invalid" + + +@fixture +def ro_crates_path() -> Path: + return CRATES_DATA_PATH + + +BASE_PATH = CRATES_DATA_PATH / "rocrate-1.3" + + +class WorkflowsScripts: + WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" + + # --- Script type --- + @property + def valid_script_type(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_type" / "valid" + + @property + def invalid_script_type(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_type" / "invalid" + + # --- Script name --- + @property + def valid_script_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_name" / "valid" + + @property + def invalid_script_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_name" / "invalid" + + # --- Workflow type --- + @property + def valid_workflow_type(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_type" / "valid" + + @property + def invalid_workflow_type_missing_file(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_type" / "invalid_missing_file" + + @property + def invalid_workflow_type_missing_ssc(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_type" / "invalid_missing_ssc" + + # --- Workflow name --- + @property + def valid_workflow_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_name" / "valid" + + @property + def invalid_workflow_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_name" / "invalid" + + # --- programmingLanguage --- + @property + def valid_programming_language(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "programming_language" / "valid" + + @property + def invalid_programming_language(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "programming_language" / "invalid" + + # --- Workflow conformsTo --- + @property + def valid_workflow_conformsTo(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_conformsTo" / "valid" + + @property + def invalid_workflow_conformsTo(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_conformsTo" / "invalid" + + # --- ImageObject encodingFormat --- + @property + def valid_image_encoding_format(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_encoding_format" / "valid" + + @property + def invalid_image_encoding_format(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_encoding_format" / "invalid" + + # --- ImageObject about --- + @property + def valid_image_about(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_about" / "valid" + + @property + def invalid_image_about(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_about" / "invalid" From 131bc001f29f785e82f8164a7f8f68534e290edf Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 9 Sep 2026 12:48:18 +0200 Subject: [PATCH 037/271] test_workflows_scripts module for ro-crate-1.3 --- ...ipts.py => test_workflows_scripts_v1_3.py} | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) rename tests/integration/profiles/ro-crate-1.3/{test_workflows_scripts.py => test_workflows_scripts_v1_3.py} (87%) diff --git a/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py similarity index 87% rename from tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py rename to tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py index a16394f0e..b286cbf46 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts.py +++ b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py @@ -15,7 +15,7 @@ import logging from rocrate_validator import models -from tests.ro_crates_v1_2 import WorkflowsScripts +from tests.ro_crates_v1_3 import WorkflowsScripts from tests.shared import do_entity_test logger = logging.getLogger(__name__) @@ -27,15 +27,15 @@ # "valid" RECOMMENDED test cases so the assertion is only about the # workflow-specific shape. _GENERIC_RECOMMENDED_SKIP = [ - "ro-crate-1.2_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) - "ro-crate-1.2_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) - "ro-crate-1.2_48.1", # Root Data Entity: recommended funder - "ro-crate-1.2_55.1", # Root Data Entity: recommended publisher - "ro-crate-1.2_62.1", # File Data Entity: RECOMMENDED contentSize - "ro-crate-1.2_63.0", # File: RECOMMENDED conformsTo profile - "ro-crate-1.2_75.1", # Contextual Entity Properties - "ro-crate-1.2_76.1", # Contextual Entity RECOMMENDED description - "ro-crate-1.2_81.2", # License entity: RECOMMENDED properties + "ro-crate-1.3_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) + "ro-crate-1.3_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) + "ro-crate-1.3_48.1", # Root Data Entity: recommended funder + "ro-crate-1.3_55.1", # Root Data Entity: recommended publisher + "ro-crate-1.3_62.1", # File Data Entity: RECOMMENDED contentSize + "ro-crate-1.3_63.0", # File: RECOMMENDED conformsTo profile + "ro-crate-1.3_75.1", # Contextual Entity Properties + "ro-crate-1.3_76.1", # Contextual Entity RECOMMENDED description + "ro-crate-1.3_81.2", # License entity: RECOMMENDED properties ] @@ -53,7 +53,7 @@ def test_valid_script_type(): __workflows_scripts_crates__.valid_script_type, models.Severity.REQUIRED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", ) @@ -66,7 +66,7 @@ def test_invalid_script_type(): __workflows_scripts_crates__.invalid_script_type, models.Severity.REQUIRED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Script: REQUIRED `File` type"], expected_triggered_issues=["A Script MUST include `File` in its `@type`"], ) @@ -85,7 +85,7 @@ def test_valid_script_name(): __workflows_scripts_crates__.valid_script_name, models.Severity.REQUIRED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", ) @@ -97,7 +97,7 @@ def test_invalid_script_name(): __workflows_scripts_crates__.invalid_script_name, models.Severity.REQUIRED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Script or Workflow: REQUIRED `name`"], expected_triggered_issues=["Scripts and Workflows MUST have a human-readable `name` property"], ) @@ -117,7 +117,7 @@ def test_valid_workflow_type(): __workflows_scripts_crates__.valid_workflow_type, models.Severity.REQUIRED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", ) @@ -130,7 +130,7 @@ def test_invalid_workflow_type_missing_file(): __workflows_scripts_crates__.invalid_workflow_type_missing_file, models.Severity.REQUIRED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Workflow: REQUIRED `File` type"], expected_triggered_issues=["A Workflow MUST include `File` in its `@type`"], ) @@ -145,7 +145,7 @@ def test_invalid_workflow_type_missing_ssc(): __workflows_scripts_crates__.invalid_workflow_type_missing_ssc, models.Severity.REQUIRED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Workflow: REQUIRED `SoftwareSourceCode` type"], expected_triggered_issues=["A Workflow MUST include `SoftwareSourceCode` in its `@type`"], ) @@ -164,7 +164,7 @@ def test_valid_workflow_name(): __workflows_scripts_crates__.valid_workflow_name, models.Severity.REQUIRED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", ) @@ -176,7 +176,7 @@ def test_invalid_workflow_name(): __workflows_scripts_crates__.invalid_workflow_name, models.Severity.REQUIRED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Script or Workflow: REQUIRED `name`"], expected_triggered_issues=["Scripts and Workflows MUST have a human-readable `name` property"], ) @@ -196,7 +196,7 @@ def test_valid_programming_language(): __workflows_scripts_crates__.valid_programming_language, models.Severity.RECOMMENDED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", skip_checks=_GENERIC_RECOMMENDED_SKIP, ) @@ -210,7 +210,7 @@ def test_invalid_programming_language(): __workflows_scripts_crates__.invalid_programming_language, models.Severity.RECOMMENDED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Script or Workflow: RECOMMENDED `programmingLanguage`"], expected_triggered_issues=["Scripts and Workflows SHOULD have a `programmingLanguage` property"], ) @@ -230,7 +230,7 @@ def test_valid_workflow_conformsTo(): __workflows_scripts_crates__.valid_workflow_conformsTo, models.Severity.RECOMMENDED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", skip_checks=_GENERIC_RECOMMENDED_SKIP, ) @@ -243,7 +243,7 @@ def test_invalid_workflow_conformsTo(): __workflows_scripts_crates__.invalid_workflow_conformsTo, models.Severity.RECOMMENDED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Workflow: RECOMMENDED Bioschemas `conformsTo`"], expected_triggered_issues=[ "Workflows SHOULD declare `conformsTo` referencing a versioned Bioschemas ComputationalWorkflow profile URI" @@ -265,7 +265,7 @@ def test_valid_image_encoding_format(): __workflows_scripts_crates__.valid_image_encoding_format, models.Severity.RECOMMENDED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", skip_checks=_GENERIC_RECOMMENDED_SKIP, ) @@ -279,7 +279,7 @@ def test_invalid_image_encoding_format(): __workflows_scripts_crates__.invalid_image_encoding_format, models.Severity.RECOMMENDED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Script/Workflow ImageObject: RECOMMENDED `encodingFormat`"], expected_triggered_issues=[ "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `encodingFormat` property" @@ -301,7 +301,7 @@ def test_valid_image_about(): __workflows_scripts_crates__.valid_image_about, models.Severity.RECOMMENDED, True, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", skip_checks=_GENERIC_RECOMMENDED_SKIP, ) @@ -315,7 +315,7 @@ def test_invalid_image_about(): __workflows_scripts_crates__.invalid_image_about, models.Severity.RECOMMENDED, False, - profile_identifier="ro-crate-1.2", + profile_identifier="ro-crate-1.3", expected_triggered_requirements=["Script/Workflow ImageObject: RECOMMENDED `about` reference"], expected_triggered_issues=[ "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `about` property " From cd5e1ee23460f2a540a139e8e16df0e4888d060a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 12 Sep 2026 19:33:12 +0200 Subject: [PATCH 038/271] feat(models): :sparkles: add tri-state CheckResult Introduce the CheckResult enum with passed, failed, and skipped outcomes. Add normalization for legacy boolean and None check results. --- rocrate_validator/models/__init__.py | 4 +++ rocrate_validator/models/check_result.py | 39 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 rocrate_validator/models/check_result.py diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index eadc17bf8..e979d8d31 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result from rocrate_validator.models.events import ( ProfileValidationEvent, RequirementCheckValidationEvent, @@ -59,6 +60,8 @@ "AggregatedValidationStatistics", "BaseTypes", "CheckIssue", + "CheckResult", + "CheckResultValue", "CustomEncoder", "LevelCollection", "Profile", @@ -80,4 +83,5 @@ "ValidationStatisticsListener", "Validator", "logger", + "normalize_check_result", ] diff --git a/rocrate_validator/models/check_result.py b/rocrate_validator/models/check_result.py new file mode 100644 index 000000000..da1674147 --- /dev/null +++ b/rocrate_validator/models/check_result.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from enum import Enum + + +class CheckResult(str, Enum): + """Normalized outcome of a requirement check.""" + + PASSED = "passed" + FAILED = "failed" + SKIPPED = "skipped" + + +CheckResultValue = bool | CheckResult | None + + +def normalize_check_result(value: CheckResultValue) -> CheckResult: + """Convert legacy boolean and ``None`` outcomes to a check result.""" + if isinstance(value, CheckResult): + return value + if value is None: + return CheckResult.SKIPPED + if isinstance(value, bool): + return CheckResult.PASSED if value else CheckResult.FAILED + raise TypeError(f"Check results must be bool, CheckResult, or None, not {type(value).__name__}") From 80495af484f6a60c2f7fa6fae389c52b8c9dfa3a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 12 Sep 2026 19:46:31 +0200 Subject: [PATCH 039/271] feat(validation): :sparkles: support tri-state check outcomes Integrate CheckResult normalization into check execution, validation events, ValidationResult, and validation statistics. Allow Python checks and the check decorator to return bool, CheckResult, or None, while preserving compatibility with legacy boolean checks. --- rocrate_validator/models/events.py | 5 +- rocrate_validator/models/requirement.py | 24 +++----- rocrate_validator/models/result.py | 45 ++++++++++++--- rocrate_validator/models/statistics.py | 56 ++++++++++++++++++- .../requirements/python/__init__.py | 52 +++++++++++++---- 5 files changed, 143 insertions(+), 39 deletions(-) diff --git a/rocrate_validator/models/events.py b/rocrate_validator/models/events.py index 6f06038c1..2b8156360 100644 --- a/rocrate_validator/models/events.py +++ b/rocrate_validator/models/events.py @@ -19,6 +19,7 @@ from rocrate_validator.events import Event, EventType if TYPE_CHECKING: + from rocrate_validator.models.check_result import CheckResultValue from rocrate_validator.models.profile import Profile from rocrate_validator.models.requirement import Requirement, RequirementCheck from rocrate_validator.models.result import ValidationResult @@ -122,7 +123,7 @@ def __init__( self, event_type: EventType, requirement_check: RequirementCheck, - validation_result: bool | None = None, + validation_result: CheckResultValue = None, message: str | None = None, ): assert event_type in ( @@ -138,7 +139,7 @@ def requirement_check(self) -> RequirementCheck: return self._requirement_check @property - def validation_result(self) -> bool | None: + def validation_result(self) -> CheckResultValue: return self._validation_result def __str__(self) -> str: diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 01b34e66c..a91785544 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -27,6 +27,7 @@ ) from rocrate_validator.events import EventType from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result from rocrate_validator.models.severity import ( LevelCollection, RequirementLevel, @@ -281,32 +282,25 @@ def __execute_check__(self, check, context, all_passed): ) # Execute the check and get the result check_result = check.execute_check(context) - logger.debug("Result of check %s: %s", check.identifier, check_result) - context.result._add_executed_check(check, check_result) + normalized_result = normalize_check_result(check_result) + logger.debug("Result of check %s: %s", check.identifier, normalized_result.value) + context.result._add_executed_check(check, normalized_result) # Notify the end of the check execution if not skip_event_notify is set to True if not skip_event_notify: context.validator.notify( RequirementCheckValidationEvent( EventType.REQUIREMENT_CHECK_VALIDATION_END, check, - validation_result=check_result, + validation_result=normalized_result, ) ) logger.debug( "Ran check '%s'. Got result %s", check.identifier, - check_result, + normalized_result, ) - # Ensure the check result is a boolean value, otherwise log a warning and ignore the check result - if not isinstance(check_result, bool): - logger.warning( - "Ignoring the check %s as it returned the value %r instead of a boolean", - check.name, - check_result, - ) - raise TypeError(f"Ignoring invalid result from check {check.name}") - new_all_passed = all_passed and check_result - should_break = not new_all_passed and context.fail_fast + new_all_passed = all_passed and normalized_result is not CheckResult.FAILED + should_break = normalized_result is CheckResult.FAILED and context.fail_fast return new_all_passed, should_break def __eq__(self, other: object) -> bool: @@ -610,7 +604,7 @@ def hidden(self) -> bool: return self.requirement.hidden @abstractmethod - def execute_check(self, context: ValidationContext) -> bool: + def execute_check(self, context: ValidationContext) -> CheckResultValue: raise NotImplementedError() def get_source_snippet(self) -> SourceSnippet | None: diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 3dcf4979f..af038ab5a 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -23,6 +23,7 @@ from rocrate_validator import __version__ from rocrate_validator.constants import JSON_OUTPUT_FORMAT_VERSION from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result from rocrate_validator.models.requirement import ( Requirement, RequirementCheck, @@ -197,6 +198,7 @@ def __init__(self, context: ValidationContext): # keep track of the checks that have been executed self._executed_checks: set[RequirementCheck] = set() self._executed_checks_results: dict[str, bool] = {} + self._check_results: dict[str, CheckResult] = {} # keep track of the checks that have been skipped self._skipped_checks: set[RequirementCheck] = set() # initialize the statistics @@ -239,20 +241,39 @@ def executed_checks(self) -> set[RequirementCheck]: """ return self._executed_checks - def _add_executed_check(self, check: RequirementCheck, result: bool): + def _record_check_result(self, check: RequirementCheck, result: CheckResultValue) -> CheckResult: + """Record a normalized result and keep check collections consistent.""" + normalized_result = normalize_check_result(result) + self._check_results[check.identifier] = normalized_result + + if normalized_result is CheckResult.SKIPPED: + self._executed_checks.discard(check) + self._skipped_checks.add(check) + self._executed_checks_results.pop(check.identifier, None) + return normalized_result + + self._executed_checks.add(check) + self._skipped_checks.discard(check) + self._executed_checks_results[check.identifier] = normalized_result is CheckResult.PASSED + return normalized_result + + def _add_executed_check(self, check: RequirementCheck, result: CheckResultValue): """ Internal method to add a check to the executed checks """ - self._executed_checks.add(check) - self._executed_checks_results[check.identifier] = result - # remove the check from the skipped checks if it was skipped - if check in self._skipped_checks: - self._skipped_checks.remove(check) - logger.debug("Removing check '%s' from skipped checks", check.name) + normalized_result = self._record_check_result(check, result) + if normalized_result is not CheckResult.SKIPPED: + logger.debug("Recorded check '%s' as %s", check.name, normalized_result.value) + + def get_check_result(self, check: RequirementCheck) -> CheckResult | None: + """Get the normalized result of a processed check.""" + return self._check_results.get(check.identifier) def get_executed_check_result(self, check: RequirementCheck) -> bool | None: """ - Get the result of an executed check + Get the legacy boolean result of an executed check. + + Skipped checks and checks that have not been processed return ``None``. """ return self._executed_checks_results.get(check.identifier) @@ -263,11 +284,16 @@ def skipped_checks(self) -> set[RequirementCheck]: """ return self._skipped_checks + @property + def skipped_checks_count(self) -> int: + """Get the number of skipped checks.""" + return len(self._skipped_checks) + def _add_skipped_check(self, check: RequirementCheck): """ Internal method to add a check to the skipped checks """ - self._skipped_checks.add(check) + self._record_check_result(check, CheckResult.SKIPPED) def _remove_skipped_check(self, check: RequirementCheck): """ @@ -407,6 +433,7 @@ def to_dict(self) -> dict: "validation_settings": validation_settings, "passed": self.passed(cast("Severity", self.context.settings.requirement_severity)), "issues": [issue.to_dict() for issue in self.issues], + "skipped_checks": self.skipped_checks_count, } # add validator version to the settings result["validation_settings"]["rocrate_validator_version"] = __version__ diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index c7f7e3536..1b75e727f 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -20,6 +20,7 @@ from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, normalize_check_result from rocrate_validator.models.events import ( ProfileValidationEvent, RequirementCheckValidationEvent, @@ -191,6 +192,18 @@ def failed_checks(self) -> list[RequirementCheck]: """ return self._stats.get("failed_checks", []) + @property + def skipped_checks(self) -> list[RequirementCheck]: + """ + Get the list of skipped checks + """ + return self._stats.get("skipped_checks", []) + + @property + def total_skipped_checks(self) -> int: + """Get the number of skipped checks.""" + return len(self.skipped_checks) + @property def total_checks(self) -> int: """ @@ -361,6 +374,7 @@ def __initialise__(cls, validation_settings: ValidationSettings): "checks_by_severity": checks_by_severity, "failed_requirements": [], "failed_checks": [], + "skipped_checks": [], "passed_requirements": [], "passed_checks": [], "started_at": None, @@ -400,11 +414,15 @@ def __handle_requirement_check_validation_end__(self, event: Event, ctx: Validat ): if event.validation_result is not None: if event.requirement_check.severity >= requirement_severity: - if event.validation_result: + result = normalize_check_result(event.validation_result) + if result is CheckResult.PASSED: self._stats["passed_checks"].append(event.requirement_check) - else: + elif result is CheckResult.FAILED: self._stats["failed_checks"].append(event.requirement_check) - self._stats["validated_checks"].append(event.requirement_check) + else: + self._stats["skipped_checks"].append(event.requirement_check) + if result is not CheckResult.SKIPPED: + self._stats["validated_checks"].append(event.requirement_check) self.notify_listeners() else: logger.debug( @@ -471,6 +489,7 @@ def to_dict(self) -> dict: "total_checks": self.total_checks, "total_passed_checks": len(self.passed_checks), "total_failed_checks": len(self.failed_checks), + "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, # Requirements involved "requirements": { @@ -508,6 +527,13 @@ def to_dict(self) -> dict: "percentage": (len(self.failed_checks) / self.total_checks * 100) if self.total_checks > 0 else 0.0, "identifiers": sorted([c.identifier for c in self.failed_checks]), }, + "skipped": { + "count": self.total_skipped_checks, + "percentage": ( + len(self.skipped_checks) / self.total_checks * 100 if self.total_checks > 0 else 0.0 + ), + "identifiers": sorted([c.identifier for c in self.skipped_checks]), + }, "identifiers": sorted([c.identifier for c in self.checks]), "by_severity": {k.name: len(v) for k, v in self._stats.get("checks_by_severity", {}).items()}, }, @@ -561,6 +587,7 @@ def to_dict(self) -> dict: "total_checks": self.total_checks, "total_passed_checks": len(self.passed_checks), "total_failed_checks": len(self.failed_checks), + "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, # Requirements involved "requirements": { @@ -598,6 +625,13 @@ def to_dict(self) -> dict: "percentage": (len(self.failed_checks) / self.total_checks * 100) if self.total_checks > 0 else 0.0, "identifiers": [c.identifier for c in self.failed_checks], }, + "skipped": { + "count": self.total_skipped_checks, + "percentage": ( + len(self.skipped_checks) / self.total_checks * 100 if self.total_checks > 0 else 0.0 + ), + "identifiers": [c.identifier for c in self.skipped_checks], + }, "identifiers": [c.identifier for c in self.checks], }, } @@ -679,6 +713,18 @@ def failed_checks(self) -> set[RequirementCheck]: """ return self._overall_stats.get("failed_checks", set()) + @property + def skipped_checks(self) -> set[RequirementCheck]: + """ + Get the set of skipped checks in the aggregated validation + """ + return self._overall_stats.get("skipped_checks", set()) + + @property + def total_skipped_checks(self) -> int: + """Get the number of skipped checks in the aggregated validation.""" + return len(self.skipped_checks) + @property def started_at(self) -> datetime | None: """ @@ -721,6 +767,7 @@ def __aggregate_raw_stats__( checks_by_severity: dict[Severity, set[RequirementCheck]] = {} failed_requirements: set[Requirement] = set() failed_checks: set[RequirementCheck] = set() + skipped_checks: set[RequirementCheck] = set() passed_requirements: set[Requirement] = set() passed_checks: set[RequirementCheck] = set() started_at: datetime | None = None @@ -741,6 +788,7 @@ def __aggregate_raw_stats__( # Aggregate failed and passed requirements and checks failed_requirements.update(stats.failed_requirements) failed_checks.update(stats.failed_checks) + skipped_checks.update(stats.skipped_checks) passed_requirements.update(stats.passed_requirements) passed_checks.update(stats.passed_checks) @@ -763,6 +811,7 @@ def __aggregate_raw_stats__( "checks_by_severity": checks_by_severity, "failed_requirements": failed_requirements, "failed_checks": failed_checks, + "skipped_checks": skipped_checks, "passed_requirements": passed_requirements, "passed_checks": passed_checks, "started_at": started_at, @@ -786,6 +835,7 @@ def __build_sorted_stats_dict__(cls, raw_stats): "checks_by_severity": sorted_checks_by_severity, "failed_requirements": sorted(raw_stats["failed_requirements"], key=lambda r: r.identifier), "failed_checks": sorted(raw_stats["failed_checks"], key=lambda c: c.identifier), + "skipped_checks": sorted(raw_stats["skipped_checks"], key=lambda c: c.identifier), "passed_requirements": sorted(raw_stats["passed_requirements"], key=lambda r: r.identifier), "passed_checks": sorted(raw_stats["passed_checks"], key=lambda c: c.identifier), "started_at": raw_stats["started_at"], diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index c6bfa6f3e..a3c21deee 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -16,10 +16,13 @@ import re from collections.abc import Callable from pathlib import Path -from typing import Any, cast +from types import UnionType +from typing import Any, Union, cast, get_args, get_origin, get_type_hints from rocrate_validator.constants import EXPECTED_CHECK_PARAM_COUNT from rocrate_validator.models import ( + CheckResult, + CheckResultValue, LevelCollection, Profile, Requirement, @@ -46,7 +49,7 @@ def __init__( self, requirement: Requirement, # pylint: disable=redefined-outer-name name: str, - check_function: Callable[[RequirementCheck, ValidationContext], bool], + check_function: Callable[[RequirementCheck, ValidationContext], CheckResultValue], description: str | None = None, level: RequirementLevel | None = LevelCollection.REQUIRED, deactivated: bool = False, @@ -62,15 +65,31 @@ def __init__( "Invalid PyFunctionCheck function. Checks are expected to accept " f"arguments [RequirementCheck, ValidationContext] but this has signature {sig}" ) - if sig.return_annotation not in (bool, inspect.Signature.empty): + try: + return_annotation = get_type_hints(check_function).get("return", sig.return_annotation) + except (NameError, TypeError): + # Keep the original annotation for dynamically loaded functions whose + # forward references cannot be resolved in their defining module. + return_annotation = sig.return_annotation + allowed_return_types = {bool, CheckResult, type(None)} + return_origin = get_origin(return_annotation) + valid_return_annotation = ( + return_annotation is inspect.Signature.empty + or return_annotation in allowed_return_types + or ( + return_origin in (Union, UnionType) + and set(get_args(return_annotation)).issubset(allowed_return_types) + ) + ) + if not valid_return_annotation: raise RuntimeError( "Invalid PyFunctionCheck function. Checks are expected to " - f"return bool but this only returns {sig.return_annotation}" + f"return bool, CheckResult, or None but this only returns {return_annotation}" ) self._check_function = check_function - def execute_check(self, context: ValidationContext) -> bool: + def execute_check(self, context: ValidationContext) -> CheckResultValue: if ( self.requirement.profile.identifier != context.profile_identifier and context.settings.disable_inherited_profiles_issue_reporting @@ -80,7 +99,7 @@ def execute_check(self, context: ValidationContext) -> bool: self.requirement.identifier, self.requirement.profile.identifier, ) - return True + return CheckResult.SKIPPED return self._check_function(self, context) def get_source_snippet(self) -> SourceSnippet | None: @@ -204,7 +223,11 @@ def decorator(cls): return decorator -def check(name: str | None = None, severity: Severity | None = None, deactivated: bool = False): +def check( + name: str | None = None, + severity: Severity | None = None, + deactivated: bool = False, +): """ A decorator to mark a function as a check. @@ -213,7 +236,7 @@ def check(name: str | None = None, severity: Severity | None = None, deactivated - a :py:class:`rocrate_validator.models.RequirementCheck` instance - a :py:class:`rocrate_validator.models.ValidationContext` instance - The function should return a boolean value. + The function should return a boolean value, ``CheckResult``, or ``None``. The decorator can be used to set the name of the check and the severity level. @@ -241,10 +264,19 @@ def decorator(func): f"Invalid check {check_name}. Checks are expected to " f"accept two arguments but this only takes {len(sig.parameters)}" ) - if sig.return_annotation not in (bool, inspect.Signature.empty): + try: + return_annotation = get_type_hints(func).get("return", sig.return_annotation) + except (NameError, TypeError): + return_annotation = sig.return_annotation + valid_return_annotation = return_annotation in (bool, CheckResult, type(None)) + if get_origin(return_annotation) in (UnionType, Union): + valid_return_annotation = all( + annotation in (bool, CheckResult, type(None)) for annotation in get_args(return_annotation) + ) + if return_annotation is not inspect.Signature.empty and not valid_return_annotation: raise RuntimeError( f"Invalid check {check_name}. Checks are expected to " - f"return bool but this only returns {sig.return_annotation}" + f"return bool, CheckResult, or None but this returns {sig.return_annotation}" ) func.check = True func.name = check_name From 199382eda5c419e59f517c147b0fb4e1a3779d9a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 12 Sep 2026 19:50:05 +0200 Subject: [PATCH 040/271] feat(profiles): :sparkles: use tri-state check results Update Python profile checks to return CheckResultValue. Use CheckResult.SKIPPED when checks are not applicable, disabled, or lack the metadata required for evaluation. --- rocrate_validator/models/requirement.py | 1 + .../1.1/must/0_file_descriptor_format.py | 34 ++++++------ .../1.1/must/4_data_entity_metadata.py | 8 +-- .../should/2_root_data_entity_relative_uri.py | 6 +-- .../1.1/should/4_data_entity_existence.py | 8 +-- .../1.1/should/5_web_data_entity_metadata.py | 10 ++-- .../1.2/must/0_file_descriptor_format.py | 52 +++++++++---------- .../ro-crate/1.2/must/1_ro_crate_preview.py | 8 +-- .../1.2/must/2_root_data_entity_cite_as.py | 8 +-- .../1.2/must/2_root_data_entity_identifier.py | 8 +-- .../1.2/must/2_root_identifier_property.py | 6 +-- .../1.2/must/4_data_entity_metadata.py | 42 +++++++-------- .../1.2/should/0_entity_identifier_format.py | 14 ++--- .../1.2/should/1_file_descriptor_name.py | 6 +-- .../should/2_root_data_entity_identifier.py | 26 +++++----- .../1.2/should/4_data_entity_license.py | 8 +-- .../1.2/should/4_data_entity_metadata.py | 24 ++++----- .../1.2/should/4_missing_file_local_path.py | 10 ++-- .../1.2/should/4_referenced_rocrate.py | 10 ++-- .../1.2/should/4_web_dataset_distribution.py | 8 +-- .../workflow-ro-crate/may/1_main_workflow.py | 12 ++--- .../workflow-ro-crate/must/0_main_workflow.py | 6 +-- .../requirements/python/__init__.py | 1 + 23 files changed, 159 insertions(+), 157 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index a91785544..15a2c3025 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -519,6 +519,7 @@ def __init__( level: RequirementLevel | None = LevelCollection.REQUIRED, description: str | None = None, hidden: bool | None = None, + *, deactivated: bool = False, ): self._requirement: Requirement = requirement diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index 909bbb480..09086d4c1 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -21,7 +21,7 @@ from rocrate_validator.constants import HTTP_STATUS_OK from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester @@ -35,13 +35,13 @@ class FileDescriptorExistence(PyFunctionCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Descriptor Existence") - def test_existence(self, context: ValidationContext) -> bool: + def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) @@ -49,13 +49,13 @@ def test_existence(self, context: ValidationContext) -> bool: return True @check(name="File Descriptor size check") - def test_size(self, context: ValidationContext) -> bool: + def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False @@ -69,7 +69,7 @@ class FileDescriptorJsonFormat(PyFunctionCheck): """ @check(name="File Descriptor JSON format") - def check(self, context: ValidationContext) -> bool: + def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) @@ -88,7 +88,7 @@ def check(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self @@ -213,7 +213,7 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) return is_valid @check(name="File Descriptor @context property validation") - def check_context(self, context: ValidationContext) -> bool: + def check_context(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor contains the @context property and it is a valid JSON-LD context @@ -230,14 +230,14 @@ def check_context(self, context: ValidationContext) -> bool: return self.__check_contexts__(context, json_dict["@context"]) except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error extracting @context from file descriptor") return False @check(name="File Descriptor JSON-LD must be flattened") - def check_flattened(self, context: ValidationContext) -> bool: + def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" return self._check_flattened_graph( context, @@ -326,14 +326,14 @@ def _check_flattened_graph(self, context, is_flat): return result except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error flattening JSON-LD file descriptor") return False @check(name="Validation of the @id property of the file descriptor entities") - def check_identifiers(self, context: ValidationContext) -> bool: + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -349,14 +349,14 @@ def check_identifiers(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @id property of file descriptor entities") return False @check(name="Validation of the @type property of the file descriptor entities") - def check_types(self, context: ValidationContext) -> bool: + def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -372,7 +372,7 @@ def check_types(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor type check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @type property of file descriptor entities") @@ -451,7 +451,7 @@ def __record_unexpected_key__(self, k: str, context_keys: set, unexpected_keys: unexpected_keys[k] = unexpected_keys.get(k, 0) + 1 @check(name="Validation of the compaction format of the file descriptor") - def check_compaction(self, context: ValidationContext) -> bool: + def check_compaction(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -497,7 +497,7 @@ def check_compaction(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py index 0ee74daf2..aaa8f7a4a 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,14 +30,14 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: """ Check the presence of the Data Entity in the RO-Crate """ # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED # Perform the check result = True # Web-based Data Entities (absolute URIs with any scheme other than `file`, @@ -47,7 +47,7 @@ def check_availability(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py index 46c3e634b..a8d667f6a 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,13 +30,13 @@ class RootDataEntityRelativeURI(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED value") - def check_relative_uris(self, context: ValidationContext) -> bool: + def check_relative_uris(self, context: ValidationContext) -> CheckResultValue: """Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD""" try: if context.ro_crate.metadata.get_root_data_entity().id != "./": context.result.add_issue("Root Data Entity URI is not denoted by the string `./`", self) return False - return True + return CheckResult.SKIPPED except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity URI check: metadata descriptor is not available") return True diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py index 2a82d87c3..b5fd63ab0 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -31,7 +31,7 @@ class DataEntityRecommendedChecker(PyFunctionCheck): """ @check(name="Data Entity: RECOMMENDED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: """ Check the availability of the Data Entity with absolute URI paths are available at the time of RO-Crate creation @@ -39,7 +39,7 @@ def check_availability(self, context: ValidationContext) -> bool: # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED # Perform the check result = True try: @@ -50,7 +50,7 @@ def check_availability(self, context: ValidationContext) -> bool: ] except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" try: diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py index 1ddcb39ed..ab94a2752 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.uri import AvailabilityStatus @@ -32,7 +32,7 @@ class WebDataEntityRecommendedChecker(PyFunctionCheck): """ @check(name="Web-based Data Entity: resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: """ Check if the Web-based Data Entity is directly downloadable by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs. @@ -47,7 +47,7 @@ def check_availability(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" try: @@ -81,7 +81,7 @@ def check_availability(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentSize` property") - def check_content_size(self, context: ValidationContext) -> bool: + def check_content_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the Web-based Data Entity has a `contentSize` property and if it is set to actual size of the downloadable content @@ -91,7 +91,7 @@ def check_content_size(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip entities whose scheme the validator cannot natively fetch diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 8de3f28a6..fb0ec124f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -19,7 +19,7 @@ from urllib.parse import urljoin from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester @@ -59,13 +59,13 @@ class FileDescriptorExistence(PyFunctionCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Descriptor Existence") - def test_existence(self, context: ValidationContext) -> bool: + def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) @@ -73,13 +73,13 @@ def test_existence(self, context: ValidationContext) -> bool: return True @check(name="File Descriptor size check") - def test_size(self, context: ValidationContext) -> bool: + def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False @@ -93,13 +93,13 @@ class FileDescriptorEncodingCheck(PyFunctionCheck): """ @check(name="File Descriptor UTF-8 encoding") - def check(self, context: ValidationContext) -> bool: + def check(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is UTF-8 encoded """ if context.settings.metadata_only: logger.debug("Skipping file descriptor encoding check in metadata-only mode") - return True + return CheckResult.SKIPPED try: raw_data = context.ro_crate.metadata.as_bytes() if isinstance(raw_data, str): @@ -113,7 +113,7 @@ def check(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED @requirement(name="File Descriptor JSON format") @@ -123,7 +123,7 @@ class FileDescriptorJsonFormat(PyFunctionCheck): """ @check(name="File Descriptor JSON format") - def check(self, context: ValidationContext) -> bool: + def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) @@ -142,7 +142,7 @@ def check(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self @@ -252,7 +252,7 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) return is_valid @check(name="File Descriptor @context property validation") - def check_context(self, context: ValidationContext) -> bool: + def check_context(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor contains the @context property and it is a valid JSON-LD context @@ -287,7 +287,7 @@ def has_expected_context(ctx: object) -> bool: return self.__check_contexts__(context, json_dict["@context"]) except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -370,7 +370,7 @@ def _is_entity_flat(self, context: ValidationContext, entity: Any, is_first: boo return result @check(name="File Descriptor JSON-LD must be flattened") - def check_flattened(self, context: ValidationContext) -> bool: + def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" try: fail_fast = bool(context.settings.abort_on_first) @@ -389,14 +389,14 @@ def check_flattened(self, context: ValidationContext) -> bool: return result except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @check(name="Validation of the @id property of the file descriptor entities") - def check_identifiers(self, context: ValidationContext) -> bool: + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -412,14 +412,14 @@ def check_identifiers(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @check(name="Validation of the @type property of the file descriptor entities") - def check_types(self, context: ValidationContext) -> bool: + def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -435,14 +435,14 @@ def check_types(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor type check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @check(name="Validation of unique @id values") - def check_unique_identifiers(self, context: ValidationContext) -> bool: + def check_unique_identifiers(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() identifiers = [entity.get("@id") for entity in json_dict.get("@graph", [])] @@ -455,7 +455,7 @@ def check_unique_identifiers(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping unique identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -486,7 +486,7 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke return None @check(name="Validation of entity references") - def check_entity_references(self, context: ValidationContext) -> bool: + def check_entity_references(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() graph = json_dict.get("@graph", []) @@ -504,14 +504,14 @@ def check_entity_references(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping entity reference check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @check(name="Validation of subject and keyword properties") - def check_subject_keywords(self, context: ValidationContext) -> bool: + def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() graph = json_dict.get("@graph", []) @@ -526,7 +526,7 @@ def check_subject_keywords(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping subject and keyword check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -600,7 +600,7 @@ def add_unexpected_key(k: str, u_keys: dict) -> None: return unexpected_keys @check(name="Validation of the compaction format of the file descriptor") - def check_compaction(self, context: ValidationContext) -> bool: + def check_compaction(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -646,7 +646,7 @@ def check_compaction(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index 1c7c0b79f..baa7274d6 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -15,7 +15,7 @@ from pathlib import Path from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,13 +30,13 @@ class ROCrateWebsiteChecker(PyFunctionCheck): """ @check(name="RO-Crate Website HTML5 doctype") - def check_preview_html(self, context: ValidationContext) -> bool: + def check_preview_html(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED except ROCrateMetadataNotFoundError: logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): return True diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py index 7cccb1c04..13db5c100 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -51,13 +51,13 @@ def _resolve_cite_as_url(cite_as_raw: object) -> str | None: return cite_as_url @check(name="Root Data Entity: `cite-as` MUST reference a downloadable item") - def check_cite_as_downloadable(self, context: ValidationContext) -> bool: + def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -84,7 +84,7 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> bool: except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py index cfc84fa3d..354b9ee11 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py @@ -15,7 +15,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,10 +29,10 @@ class RootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: REQUIRED value") - def check_identifier(self, context: ValidationContext) -> bool: + def check_identifier(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if root_entity.id == "./": return True @@ -45,7 +45,7 @@ def check_identifier(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py index ace24b4d0..d89f405eb 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,7 +28,7 @@ class RootIdentifierPropertyChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier PropertyValue value") - def check_identifier_values(self, context: ValidationContext) -> bool: + def check_identifier_values(self, context: ValidationContext) -> CheckResultValue: try: root = context.ro_crate.metadata.get_root_data_entity() identifiers = root.get_property("identifier") @@ -46,7 +46,7 @@ def check_identifier_values(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 0482c93aa..8fea3eeab 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -16,7 +16,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -32,7 +32,7 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: # noqa: C901 + def check_availability(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """ Check the presence of the Data Entity in the RO-Crate """ @@ -40,21 +40,21 @@ def check_availability(self, context: ValidationContext) -> bool: # noqa: C901 is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if is_detached: logger.debug("Skipping data entity payload checks for detached RO-Crate") - return True + return CheckResult.SKIPPED # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED # Perform the check result = True try: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) @@ -104,14 +104,14 @@ class DetachedDataEntityChecker(PyFunctionCheck): """ @check(name="Detached RO-Crate: data entities MUST be web-based") - def check_detached_entities(self, context: ValidationContext) -> bool: + def check_detached_entities(self, context: ValidationContext) -> CheckResultValue: try: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if not is_detached: - return True + return CheckResult.SKIPPED result = True root_entity_id = None with contextlib.suppress(Exception): @@ -120,7 +120,7 @@ def check_detached_entities(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: continue @@ -144,7 +144,7 @@ class DataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 result = True root_entity_id = None root_entity_is_local = False @@ -160,7 +160,7 @@ def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: continue @@ -197,20 +197,20 @@ def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 return result @check(name="Data Entity: relative @id for payload files") - def check_relative_paths(self, context: ValidationContext) -> bool: + def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: try: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if is_detached: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if entity.has_local_identifier() or entity.is_remote(): continue @@ -233,13 +233,13 @@ class DataEntityCitationChecker(PyFunctionCheck): """ @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> bool: + def check_citation(self, context: ValidationContext) -> CheckResultValue: result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") if citations is None: @@ -289,19 +289,19 @@ def _not_downloadable_message(entity_id: str, dl) -> str: return msg @check(name="Web-based Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip directory URIs: assumed available, not directly downloadable by spec diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py index 6b79215c4..7fc0f14e1 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py @@ -23,7 +23,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -47,7 +47,7 @@ class EntityIdentifierFormatChecker(PyFunctionCheck): """ @check(name="Entity identifiers SHOULD NOT use ../", severity=Severity.RECOMMENDED) - def check_no_parent_traversal(self, context: ValidationContext) -> bool: + def check_no_parent_traversal(self, context: ValidationContext) -> CheckResultValue: """ @id paths SHOULD NOT use `../` to climb out of the RO-Crate Root (RO-Crate 1.2, JSON-LD appendix). @@ -57,7 +57,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping parent traversal check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") if "../" in entity_id: @@ -75,7 +75,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: name="Entity identifiers SHOULD use native UTF-8, not percent-encoding", severity=Severity.RECOMMENDED, ) - def check_utf8_identifiers(self, context: ValidationContext) -> bool: + def check_utf8_identifiers(self, context: ValidationContext) -> CheckResultValue: """ International characters in @id values SHOULD be written in native UTF-8 rather than percent-encoded (RO-Crate 1.2, JSON-LD appendix). @@ -85,7 +85,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") if _PCT_NON_ASCII_RE.search(entity_id): @@ -104,7 +104,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: name="Contextual entity @id SHOULD be absolute URI or '#'-prefixed", severity=Severity.RECOMMENDED, ) - def check_named_entity_id_format(self, context: ValidationContext) -> bool: + def check_named_entity_id_format(self, context: ValidationContext) -> CheckResultValue: """ Any Contextual Entity (Person, Organization, ContactPoint, PropertyValue, Place, etc.) SHOULD use an @id that is an absolute URI (permalink), @@ -125,7 +125,7 @@ def check_named_entity_id_format(self, context: ValidationContext) -> bool: entities = ro_crate_metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping named entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") if entity_id in non_contextual_ids: diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py index e7034f3d1..d4ebc2b8c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py @@ -13,7 +13,7 @@ # limitations under the License. -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,7 +29,7 @@ class FileDescriptorExistence(PyFunctionCheck): """ @check(name="Detached RO-Crate file descriptor RECOMMENDED naming convention") - def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: + def test_detached_descriptor_filename(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor of a Detached RO-Crate exists and is named according to the convention. In a Detached RO-Crate, the file descriptor SHOULD be named `{prefix}-ro-crate-metadata.json`, @@ -37,7 +37,7 @@ def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py index 2b62c28d3..fae9fae52 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -29,16 +29,16 @@ class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED identifier") - def check_identifier(self, context: ValidationContext) -> bool: # noqa: PLR0911 + def check_identifier(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 """ In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. """ try: if not context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): - return True + return CheckResult.SKIPPED if root_entity.id == "./": context.result.add_issue( @@ -54,7 +54,7 @@ def check_identifier(self, context: ValidationContext) -> bool: # noqa: PLR0911 return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False @@ -68,17 +68,17 @@ class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: use cite-as for resolvable identifiers") - def check_cite_as_reference(self, context: ValidationContext) -> bool: # noqa: PLR0911 + def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 """ If the Root Data Entity has a resolvable identifier, it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. """ try: if not context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): - return True + return CheckResult.SKIPPED # Check if the `cite-as` property is present and references the Root Data Entity cite_as = root_entity.get_property("cite-as") @@ -102,7 +102,7 @@ def check_cite_as_reference(self, context: ValidationContext) -> bool: # noqa: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) return False @@ -147,11 +147,11 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) - def check_identifier_resolvable(self, context: ValidationContext) -> bool: # noqa: PLR0911 + def check_identifier_resolvable(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 if context.settings.skip_availability_check: - return True + return CheckResult.SKIPPED if context.settings.metadata_only: - return True + return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() identifier_raw = root_entity.get_property("identifier") @@ -178,7 +178,7 @@ def check_identifier_resolvable(self, context: ValidationContext) -> bool: # no return result except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py index e602a91b3..6e3bd9ee1 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py @@ -47,7 +47,7 @@ # valid crates. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -101,12 +101,12 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> bool: # noqa: C901 + def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() except Exception: - return True + return CheckResult.SKIPPED if root_entity is None: return True @@ -122,7 +122,7 @@ def check_license_divergence(self, context: ValidationContext) -> bool: # noqa: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity license check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if entity.id == root_entity.id: continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py index 71f5a22d7..46fb8320b 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py @@ -15,7 +15,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -31,13 +31,13 @@ class DataEntityCitationChecker(PyFunctionCheck): """ @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> bool: + def check_citation(self, context: ValidationContext) -> CheckResultValue: result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") if citations is None: @@ -87,20 +87,20 @@ def _not_downloadable_message(entity_id: str, dl) -> str: return msg @check(name="Web-based Data Entity: RECOMMENDED resource availability", severity=Severity.RECOMMENDED) - def check_availability_warning(self, context: ValidationContext) -> bool: + def check_availability_warning(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or context.settings.creation_time or context.settings.enforce_availability or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.id.endswith("/"): @@ -118,15 +118,15 @@ def check_availability_warning(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) - def check_content_size(self, context: ValidationContext) -> bool: # noqa: C901 + def check_content_size(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.is_available(): @@ -159,15 +159,15 @@ def check_content_size(self, context: ValidationContext) -> bool: # noqa: C901 return result @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) - def check_content_url(self, context: ValidationContext) -> bool: # noqa: C901 + def check_content_url(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: content_url = entity.get_property("contentUrl") if not content_url: diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py index e28156930..ad45cafff 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py @@ -33,7 +33,7 @@ import contextlib from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,14 +100,14 @@ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: return True @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) - def check_missing_file_local_path(self, context: ValidationContext) -> bool: + def check_missing_file_local_path(self, context: ValidationContext) -> CheckResultValue: try: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if is_detached or context.settings.metadata_only: - return True + return CheckResult.SKIPPED root_entity_id = None with contextlib.suppress(Exception): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id @@ -116,7 +116,7 @@ def check_missing_file_local_path(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py index 8a05b4ce4..382ead777 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py @@ -24,7 +24,7 @@ """ from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import has_signposting_cite_as @@ -62,7 +62,7 @@ def _needs_sddatepublished_check(entity, root_id) -> bool: name="Referenced RO-Crate: `sdDatePublished` SHOULD be present when Signposting cite-as is not declared", severity=Severity.RECOMMENDED, ) - def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: + def check_sddatepublished_signposting(self, context: ValidationContext) -> CheckResultValue: """ For each referenced RO-Crate data entity whose @id is an absolute URI with no declared `identifier` and no `sdDatePublished`, verify whether @@ -74,19 +74,19 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED result = True try: root = context.ro_crate.metadata.get_root_data_entity() except Exception: - return True + return CheckResult.SKIPPED try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py index d3e3ac09f..124c343e5 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -80,15 +80,15 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo return True @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) - def check_distribution_downloadable(self, context: ValidationContext) -> bool: + def check_distribution_downloadable(self, context: ValidationContext) -> CheckResultValue: if context.settings.skip_availability_check or context.settings.metadata_only: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: distribution_raw = entity.get_property("distribution") if not distribution_raw: diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py index ce2970879..d05612160 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,11 +28,11 @@ class WorkflowFilesExistence(PyFunctionCheck): """Checks for workflow-related crate files existence.""" @check(name="Workflow diagram existence") - def check_workflow_diagram(self, context: ValidationContext) -> bool: + def check_workflow_diagram(self, context: ValidationContext) -> CheckResultValue: """Check if the crate contains the workflow diagram.""" if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED try: main_workflow = context.ro_crate.metadata.get_main_workflow() @@ -47,14 +47,14 @@ def check_workflow_diagram(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping workflow diagram check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking main workflow image existence") return False @check(name="Workflow description existence") - def check_workflow_description(self, context: ValidationContext) -> bool: + def check_workflow_description(self, context: ValidationContext) -> CheckResultValue: """Check if the crate contains the workflow CWL description.""" try: main_workflow = context.ro_crate.metadata.get_main_workflow() @@ -71,7 +71,7 @@ def check_workflow_description(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping workflow description check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking workflow description existence") diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py index e2ae6951f..9209a3e5f 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,7 +28,7 @@ class MainWorkflowFileExistence(PyFunctionCheck): """Checks for main workflow file existence.""" @check(name="Main Workflow file must exist") - def check_workflow(self, context: ValidationContext) -> bool: + def check_workflow(self, context: ValidationContext) -> CheckResultValue: """Check if the crate contains the main workflow file.""" try: main_workflow = context.ro_crate.metadata.get_main_workflow() @@ -41,7 +41,7 @@ def check_workflow(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping main workflow check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except (TypeError, ValueError) as error: if str(error) == "no main workflow in metadata file descriptor": message = ( diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index a3c21deee..cd20d9800 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -50,6 +50,7 @@ def __init__( requirement: Requirement, # pylint: disable=redefined-outer-name name: str, check_function: Callable[[RequirementCheck, ValidationContext], CheckResultValue], + *, description: str | None = None, level: RequirementLevel | None = LevelCollection.REQUIRED, deactivated: bool = False, From 581fbf4a55e9b110e3981ce14e6fb415a0dd4035 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 12 Sep 2026 19:52:51 +0200 Subject: [PATCH 041/271] test(check-result): :white_check_mark: cover tri-state outcomes Cover normalization of boolean, CheckResult, and None outcomes. Verify that the check decorator accepts tri-state return annotations and rejects invalid annotations. --- tests/unit/test_check_result.py | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/unit/test_check_result.py diff --git a/tests/unit/test_check_result.py b/tests/unit/test_check_result.py new file mode 100644 index 000000000..029809b41 --- /dev/null +++ b/tests/unit/test_check_result.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, cast + +import pytest + +from rocrate_validator.models import CheckResult, normalize_check_result +from rocrate_validator.requirements.python import check + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (True, CheckResult.PASSED), + (False, CheckResult.FAILED), + (None, CheckResult.SKIPPED), + (CheckResult.PASSED, CheckResult.PASSED), + ], +) +def test_normalize_check_result(value, expected): + assert normalize_check_result(value) is expected + + +def test_normalize_check_result_rejects_invalid_values(): + with pytest.raises(TypeError, match="Check results must be bool"): + normalize_check_result(cast("Any", "passed")) + + +@pytest.mark.parametrize("result_type", [bool, CheckResult, type(None), bool | CheckResult | None]) +def test_check_decorator_accepts_tri_state_return_annotations(result_type): + def check_function(check_instance, context): + return True + + check_function.__annotations__["return"] = result_type + decorated = check()(check_function) + + assert decorated.check + + +def test_check_decorator_rejects_invalid_return_annotation(): + with pytest.raises(RuntimeError, match="return bool, CheckResult, or None"): + + @check() + def invalid_check(check_instance, context) -> int: + return 1 From 77c4741bf8e1cdb28627906ed7bd8c2611708e7f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 08:45:40 +0200 Subject: [PATCH 042/271] feat(models): :sparkles: support check dependencies Add check dependency declarations, dependency-aware topological ordering, dependency closure handling, and dedicated configuration errors. --- rocrate_validator/errors.py | 26 +++ rocrate_validator/models/requirement.py | 148 ++++++++++++++++++ rocrate_validator/models/validation.py | 48 ++++-- .../requirements/python/__init__.py | 23 ++- 4 files changed, 227 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 6d3da1133..6f8da43f0 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -151,6 +151,32 @@ def __repr__(self): return f"DuplicateRequirementCheck({self._check_name!r}, {self._profile_name!r})" +class CheckDependencyError(ROCValidatorError): + """Raised when a check dependency cannot be resolved or ordered.""" + + def __init__(self, message: str, profile_name: str | None = None): + self._message = message + self._profile_name = profile_name + + @property + def message(self) -> str: + """The dependency configuration error message.""" + return self._message + + @property + def profile_name(self) -> str | None: + """The profile containing the invalid dependency.""" + return self._profile_name + + def __str__(self) -> str: + if self._profile_name: + return f"Invalid check dependency in profile {self._profile_name!r}: {self._message}" + return f"Invalid check dependency: {self._message}" + + def __repr__(self): + return f"CheckDependencyError({self._message!r}, {self._profile_name!r})" + + class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 15a2c3025..39001a7dc 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -25,6 +25,7 @@ PROFILE_FILE_EXTENSIONS, PROFILE_SPECIFICATION_FILE, ) +from rocrate_validator.errors import CheckDependencyError from rocrate_validator.events import EventType from rocrate_validator.models._logging import logger from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result @@ -186,6 +187,12 @@ def __init_checks__(self) -> list[RequirementCheck]: def get_checks(self) -> list[RequirementCheck]: return self._checks.copy() + def set_checks_order(self, checks: list[RequirementCheck]) -> None: + """Replace the check order and refresh the public check numbers.""" + self._checks = checks.copy() + for i, check in enumerate(self._checks, start=1): + check.order_number = i + def get_check(self, name: str) -> RequirementCheck | None: for check in self._checks: if check.name == name: @@ -488,6 +495,7 @@ def ok_file(p: Path) -> bool: ), reverse=False, ) + requirements = RequirementLoader.order_by_dependencies(requirements) # assign order numbers to requirements for i, requirement in enumerate(requirements): requirement._order_number = i + 1 @@ -495,6 +503,138 @@ def ok_file(p: Path) -> bool: logger.debug("Profile %s loaded %s requirements: %s", profile.identifier, len(requirements), requirements) return requirements + @staticmethod + def _check_index(requirements: list[Requirement]) -> dict[str, list[RequirementCheck]]: + checks_by_name: dict[str, list[RequirementCheck]] = {} + for requirement in requirements: + for check in requirement.get_checks(): + checks_by_name.setdefault(check.name, []).append(check) + return checks_by_name + + @staticmethod + def _resolve_dependency( + check: RequirementCheck, + dependency_name: str, + checks_by_name: dict[str, list[RequirementCheck]], + ) -> RequirementCheck: + candidates = checks_by_name.get(dependency_name, []) + profile_name = check.requirement.profile.identifier + if not candidates: + raise CheckDependencyError( + f"check {check.name!r} depends on unknown check {dependency_name!r}", + profile_name, + ) + if len(candidates) > 1: + raise CheckDependencyError( + f"check {check.name!r} depends on ambiguous check {dependency_name!r}", + profile_name, + ) + return candidates[0] + + @staticmethod + def _topological_order( + nodes: list[Any], edges: dict[int, set[int]], profile_name: str, node_type: str + ) -> list[Any]: + indegree = [0] * len(nodes) + for targets in edges.values(): + for target in targets: + indegree[target] += 1 + + ready = [index for index, degree in enumerate(indegree) if degree == 0] + ordered_indices: list[int] = [] + while ready: + ready.sort() + index = ready.pop(0) + ordered_indices.append(index) + for target in sorted(edges.get(index, ())): + indegree[target] -= 1 + if indegree[target] == 0: + ready.append(target) + + if len(ordered_indices) != len(nodes): + cycle_nodes = [nodes[index] for index, degree in enumerate(indegree) if degree > 0] + cycle_names = ", ".join(getattr(node, "name", str(node)) for node in cycle_nodes) + raise CheckDependencyError(f"dependency cycle detected among {node_type}: {cycle_names}", profile_name) + + return [nodes[index] for index in ordered_indices] + + @classmethod + def order_by_dependencies(cls, requirements: list[Requirement]) -> list[Requirement]: + """Apply check dependencies while preserving the baseline requirement order.""" + if not requirements: + return [] + + checks_by_name = cls._check_index(requirements) + requirement_indices = {id(requirement): index for index, requirement in enumerate(requirements)} + requirement_edges: dict[int, set[int]] = {} + + for requirement in requirements: + checks = requirement.get_checks() + check_indices = {id(check): index for index, check in enumerate(checks)} + check_edges: dict[int, set[int]] = {} + for check in checks: + for dependency_name in check.depends_on: + dependency = cls._resolve_dependency(check, dependency_name, checks_by_name) + dependency_requirement_index = requirement_indices[id(dependency.requirement)] + source_requirement_index = requirement_indices[id(requirement)] + if dependency_requirement_index == source_requirement_index: + check_edges.setdefault(check_indices[id(dependency)], set()).add(check_indices[id(check)]) + else: + requirement_edges.setdefault(dependency_requirement_index, set()).add(source_requirement_index) + + if check_edges: + ordered_checks = cls._topological_order( + checks, + check_edges, + requirement.profile.identifier, + "checks", + ) + requirement.set_checks_order(ordered_checks) + + return cls._topological_order( + requirements, + requirement_edges, + requirements[0].profile.identifier, + "requirements", + ) + + @classmethod + def dependency_closure( + cls, + requirements: list[Requirement], + *, + include_dependencies: bool = True, + ) -> list[Requirement]: + """Return selected requirements plus their transitive check dependencies.""" + selected: list[Requirement] = [] + selected_ids: set[int] = set() + queue = list(requirements) + + while queue: + requirement = queue.pop(0) + if id(requirement) in selected_ids: + continue + selected_ids.add(id(requirement)) + selected.append(requirement) + + profile_requirements = requirement.profile.requirements + checks_by_name = cls._check_index(profile_requirements) + for check in requirement.get_checks(): + for dependency_name in check.depends_on: + dependency = cls._resolve_dependency(check, dependency_name, checks_by_name) + dependency_requirement = dependency.requirement + if id(dependency_requirement) in selected_ids: + continue + if not include_dependencies: + raise CheckDependencyError( + f"check {check.name!r} depends on {dependency_name!r}, " + "but its requirement was not selected", + requirement.profile.identifier, + ) + queue.append(dependency_requirement) + + return selected + @dataclass(frozen=True) class SourceSnippet: @@ -521,6 +661,7 @@ def __init__( hidden: bool | None = None, *, deactivated: bool = False, + depends_on: tuple[str, ...] | None = None, ): self._requirement: Requirement = requirement self._order_number = 0 @@ -529,6 +670,7 @@ def __init__( self._description = description self._hidden = hidden self._deactivated = deactivated + self._depends_on = tuple(depends_on or ()) @property def order_number(self) -> int: @@ -598,6 +740,11 @@ def overridden(self) -> bool: def deactivated(self) -> bool: return self._deactivated + @property + def depends_on(self) -> tuple[str, ...]: + """Names of checks that must run before this check.""" + return self._depends_on + @property def hidden(self) -> bool: if self._hidden is not None: @@ -624,6 +771,7 @@ def to_dict(self, with_requirement: bool = True, with_profile: bool = True) -> d "name": self.name, "description": self.description, "severity": self.severity.name, + "depends_on": list(self.depends_on), } if with_requirement: result["requirement"] = self.requirement.to_dict(with_profile=with_profile, with_checks=False) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index a91a1443f..3974aabe0 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -142,15 +142,27 @@ def validate(self) -> ValidationResult: """ return self.__do_validate__() - def validate_requirements(self, requirements: list[Requirement]) -> ValidationResult: + def validate_requirements( + self, + requirements: list[Requirement], + *, + include_dependencies: bool = True, + ) -> ValidationResult: """ - Validates the RO-Crate against the specified subset of the profile requirements + Validates the RO-Crate against the specified subset of the profile requirements. + + By default, requirements containing transitive check dependencies are added to + the selected subset. Set ``include_dependencies`` to ``False`` to require the + caller to provide the complete dependency closure explicitly. """ assert all(isinstance(requirement, Requirement) for requirement in requirements), "Invalid requirement type" - # perform the requirements validation - return self.__do_validate__(requirements) + resolved_requirements = RequirementLoader.dependency_closure( + requirements, + include_dependencies=include_dependencies, + ) + return self.__do_validate__(resolved_requirements) - def __do_validate__(self, requirements: list[Requirement] | None = None) -> ValidationResult: + def __do_validate__(self, requirements: list[Requirement] | None = None) -> ValidationResult: # noqa: C901, PLR0912 # initialize the validation context context = ValidationContext(self, self.validation_settings) @@ -170,6 +182,9 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali # profiles that have not yet been visited. for p in profiles: _ = p.requirements + selected_requirement_ids = ( + None if requirements is None else {id(requirement) for requirement in requirements} + ) self.notify(EventType.VALIDATION_START) for profile in profiles: logger.debug( @@ -181,23 +196,30 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali context._target_validation_profile = profile self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_START, profile=profile)) # perform the requirements validation - requirements = profile.get_requirements( - context.requirement_severity, - exact_match=context.requirement_severity_only, - ) + if selected_requirement_ids is None: + profile_requirements = profile.get_requirements( + context.requirement_severity, + exact_match=context.requirement_severity_only, + ) + else: + profile_requirements = [ + requirement + for requirement in profile.requirements + if id(requirement) in selected_requirement_ids + ] logger.debug( "Validating profile %s with %s requirements", profile.identifier, - len(requirements), + len(profile_requirements), ) logger.debug( "For profile %s, validating these %s requirements: %s", profile.identifier, - len(requirements), - requirements, + len(profile_requirements), + profile_requirements, ) terminate = False - for requirement in requirements: + for requirement in profile_requirements: if not requirement.overridden: self.notify( RequirementValidationEvent( diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index cd20d9800..4a1fb96dc 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -54,11 +54,19 @@ def __init__( description: str | None = None, level: RequirementLevel | None = LevelCollection.REQUIRED, deactivated: bool = False, + depends_on: tuple[str, ...] | None = None, ): """ check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. """ - super().__init__(requirement, name, description=description, level=level, deactivated=deactivated) + super().__init__( + requirement, + name, + description=description, + level=level, + deactivated=deactivated, + depends_on=tuple(getattr(check_function, "depends_on", depends_on or ())), + ) sig = inspect.signature(check_function) if len(sig.parameters) != EXPECTED_CHECK_PARAM_COUNT: @@ -77,10 +85,7 @@ def __init__( valid_return_annotation = ( return_annotation is inspect.Signature.empty or return_annotation in allowed_return_types - or ( - return_origin in (Union, UnionType) - and set(get_args(return_annotation)).issubset(allowed_return_types) - ) + or (return_origin in (Union, UnionType) and set(get_args(return_annotation)).issubset(allowed_return_types)) ) if not valid_return_annotation: raise RuntimeError( @@ -228,6 +233,7 @@ def check( name: str | None = None, severity: Severity | None = None, deactivated: bool = False, + depends_on: tuple[str, ...] | None = None, ): """ A decorator to mark a function as a check. @@ -252,6 +258,8 @@ def check( a check with the same name as one in a parent profile and set this flag to disable the inherited check. :type deactivated: bool + :param depends_on: names of checks that must run before this check. + :type depends_on: Optional[Tuple[str, ...]] :return: the decorated function :rtype: Callable @@ -265,6 +273,10 @@ def decorator(func): f"Invalid check {check_name}. Checks are expected to " f"accept two arguments but this only takes {len(sig.parameters)}" ) + if depends_on is not None and ( + not isinstance(depends_on, tuple) or not all(isinstance(dependency, str) for dependency in depends_on) + ): + raise TypeError("Check dependencies must be provided as a tuple of strings") try: return_annotation = get_type_hints(func).get("return", sig.return_annotation) except (NameError, TypeError): @@ -283,6 +295,7 @@ def decorator(func): func.name = check_name func.severity = severity func.deactivated = deactivated + func.depends_on = depends_on or () return func return decorator From 2444e97ad3d38ae41c5d3fcd8b11cfa5ee13c9ec Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 09:06:02 +0200 Subject: [PATCH 043/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20handle=20metadata=20errors=20in=20profile=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle ROCrateMetadataNotFoundError raised by metadata access after #192 in the singleton-property checks for RO-Crate 1.1 and 1.2. Prevent metadata-dependent checks from falling through to the generic requirement error handler and emitting misleading warnings when the descriptor is missing or cannot be inspected. Log the reason for skipped checks at DEBUG level. Subsequent improvements will manage these skipped checks in a more structured way. --- .../3_entity_property_compact_representation_1_1.py | 10 +++++++++- .../3_entity_property_compact_representation_1_2.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py b/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py index 419ec6451..b0a92533f 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py @@ -14,10 +14,14 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging from rocrate_validator.utils.jsonld import find_singleton_property_arrays +logger = logging.getLogger(__name__) + @requirement(name="Entity properties compact representation") class EntityPropertyCompactRepresentation(PyFunctionCheck): @@ -31,8 +35,12 @@ def check_singleton_property_arrays(self, context: ValidationContext) -> bool: """Check the RO-Crate 1.1 recommendation for singleton property arrays.""" try: findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) - except (AttributeError, TypeError, ValueError): + except ROCrateMetadataNotFoundError as error: + logger.debug("Skipping singleton property array check: metadata descriptor is not available (%s)", error) + return True + except (AttributeError, TypeError, ValueError) as error: # Required format checks report malformed metadata structure. + logger.debug("Skipping singleton property array check: metadata cannot be inspected (%s)", error) return True result = True diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py b/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py index 3f86777fe..53b0db0b7 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py @@ -14,10 +14,14 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging from rocrate_validator.utils.jsonld import find_singleton_property_arrays +logger = logging.getLogger(__name__) + @requirement(name="Entity properties compact representation") class EntityPropertyCompactRepresentation(PyFunctionCheck): @@ -31,8 +35,12 @@ def check_singleton_property_arrays(self, context: ValidationContext) -> bool: """Check the RO-Crate 1.2 recommendation for singleton property arrays.""" try: findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) - except (AttributeError, TypeError, ValueError): + except ROCrateMetadataNotFoundError as error: + logger.debug("Skipping singleton property array check: metadata descriptor is not available (%s)", error) + return True + except (AttributeError, TypeError, ValueError) as error: # Required format checks report malformed metadata structure. + logger.debug("Skipping singleton property array check: metadata cannot be inspected (%s)", error) return True result = True From 2343867f920cf66a2d384429bc8448173e5efea2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 09:39:24 +0200 Subject: [PATCH 044/271] test(dependencies): :white_check_mark: cover dependency ordering Cover dependency ordering within and across requirements, transitive closure, and invalid configurations such as missing, ambiguous, and cyclic dependencies. --- tests/unit/requirements/test_dependencies.py | 117 ++++++++++++++++++ .../requirements/test_load_requirements.py | 41 ++++-- tests/unit/requirements/test_profiles.py | 11 ++ 3 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 tests/unit/requirements/test_dependencies.py diff --git a/tests/unit/requirements/test_dependencies.py b/tests/unit/requirements/test_dependencies.py new file mode 100644 index 000000000..508877086 --- /dev/null +++ b/tests/unit/requirements/test_dependencies.py @@ -0,0 +1,117 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from rocrate_validator.errors import CheckDependencyError +from rocrate_validator.models import CheckResult, RequirementCheck, RequirementLoader + + +class _Profile: + identifier = "test" + + def __init__(self): + self.requirements = [] + + +class _Requirement: + def __init__(self, profile, name): + self.profile = profile + self.name = name + self._checks = [] + profile.requirements.append(self) + + def get_checks(self): + return self._checks.copy() + + def set_checks_order(self, checks): + self._checks = checks.copy() + for order_number, check in enumerate(self._checks, start=1): + check.order_number = order_number + + +class _Check(RequirementCheck): + def execute_check(self, context): + return CheckResult.PASSED + + +def _check(requirement, name, depends_on=()): + check = _Check(requirement, name, depends_on=depends_on) + requirement._checks.append(check) + return check + + +def test_order_by_dependencies_reorders_checks_within_a_requirement(): + profile = _Profile() + requirement = _Requirement(profile, "Requirement") + dependent = _check(requirement, "dependent", ("base",)) + base = _check(requirement, "base") + + RequirementLoader.order_by_dependencies([requirement]) + + assert requirement.get_checks() == [base, dependent] + assert [check.order_number for check in requirement.get_checks()] == [1, 2] + + +def test_order_by_dependencies_reorders_requirements(): + profile = _Profile() + dependent_requirement = _Requirement(profile, "Dependent") + dependency_requirement = _Requirement(profile, "Dependency") + dependent = _check(dependent_requirement, "dependent", ("base",)) + base = _check(dependency_requirement, "base") + + assert RequirementLoader.order_by_dependencies([dependent_requirement, dependency_requirement]) == [ + dependency_requirement, + dependent_requirement, + ] + assert dependent.depends_on == ("base",) + assert base.depends_on == () + + +@pytest.mark.parametrize( + ("dependency_name", "message"), + [ + ("missing", "unknown check"), + ], +) +def test_order_by_dependencies_rejects_unknown_dependencies(dependency_name, message): + profile = _Profile() + requirement = _Requirement(profile, "Requirement") + _check(requirement, "dependent", (dependency_name,)) + + with pytest.raises(CheckDependencyError, match=message): + RequirementLoader.order_by_dependencies([requirement]) + + +def test_order_by_dependencies_rejects_ambiguous_dependencies(): + profile = _Profile() + dependent_requirement = _Requirement(profile, "Dependent") + first_dependency = _Requirement(profile, "First") + second_dependency = _Requirement(profile, "Second") + _check(dependent_requirement, "dependent", ("base",)) + _check(first_dependency, "base") + _check(second_dependency, "base") + + with pytest.raises(CheckDependencyError, match="ambiguous check"): + RequirementLoader.order_by_dependencies(profile.requirements) + + +def test_order_by_dependencies_rejects_cycles(): + profile = _Profile() + requirement = _Requirement(profile, "Requirement") + _check(requirement, "first", ("second",)) + _check(requirement, "second", ("first",)) + + with pytest.raises(CheckDependencyError, match="dependency cycle"): + RequirementLoader.order_by_dependencies([requirement]) diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index 577bae5a8..c21ccbe10 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -16,8 +16,11 @@ from pathlib import Path from typing import Any +import pytest + from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import LevelCollection, Profile, Severity +from rocrate_validator.errors import CheckDependencyError +from rocrate_validator.models import LevelCollection, Profile, RequirementLoader, Severity from rocrate_validator.requirements.shacl.requirements import SHACLRequirement from tests.ro_crates import InvalidFileDescriptorEntity @@ -126,17 +129,7 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): requirement.path, ) - # Sort requirements by their order - requirements = sorted( - requirements, - key=lambda x: ( - -(x.severity_from_path.value if x.severity_from_path else 0), - x.path.name if x.path else "", - x.name, - ), - ) - - # Check the order of the requirements + # Requirements are loaded in stable dependency-aware order. for i, requirement in enumerate(requirements): if i < len(requirements) - 1: assert requirement < requirements[i + 1] @@ -164,6 +157,30 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): assert requirement_check.severity == Severity.RECOMMENDED, "The severity of the requirement check is incorrect" +def test_check_dependencies_are_ordered_and_closed(profiles_path: str): + profiles = Profile.load_profiles(profiles_path=profiles_path, severity=Severity.REQUIRED) + profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.2") + + requirements = {requirement.name: requirement for requirement in profile.requirements} + existence = requirements["File Descriptor existence"] + encoding = requirements["File Descriptor UTF-8 encoding"] + json_format = requirements["File Descriptor JSON format"] + + ordered_names = [requirement.name for requirement in profile.requirements] + assert ordered_names.index(existence.name) < ordered_names.index(encoding.name) + assert ordered_names.index(encoding.name) < ordered_names.index(json_format.name) + + closure = RequirementLoader.dependency_closure([json_format]) + assert {requirement.name for requirement in closure} == { + existence.name, + encoding.name, + json_format.name, + } + + with pytest.raises(CheckDependencyError, match="was not selected"): + RequirementLoader.dependency_closure([json_format], include_dependencies=False) + + def test_hidden_requirements(profiles_loading_hidden_requirements: str): # Define the list of requirements names diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 48b4c3469..114a37abf 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -477,6 +477,17 @@ def enabled(self, ctx): assert enabled.deactivated is False # pyright: ignore[reportFunctionMemberAccess] +def test_python_check_decorator_sets_dependencies(): + """The @check decorator must preserve declared check dependencies.""" + from rocrate_validator.requirements.python import check + + @check(name="dependent", depends_on=("base",)) + def dependent(self, ctx): + return True + + assert dependent.depends_on == ("base",) # pyright: ignore[reportFunctionMemberAccess] + + def test_shacl_shape_with_deactivated_marks_check_skipped(fake_profiles_path: str): """A child profile that overrides an inherited NodeShape by `sh:name` and sets `sh:deactivated true` should produce a check whose `deactivated` From 42699e840133922e952a9af7dee60520a6d4bc60 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 09:52:49 +0200 Subject: [PATCH 045/271] feat(profiles): :sparkles: declare file descriptor dependencies Declare file descriptor check prerequisites for RO-Crate 1.1 and 1.2. Ensure existence, encoding, JSON, and JSON-LD checks execute in a valid dependency order. --- .../1.1/must/0_file_descriptor_format.py | 29 +++++++++--- .../1.2/must/0_file_descriptor_format.py | 46 ++++++++++++++----- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index 09086d4c1..3c6a01e27 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -48,7 +48,7 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: return False return True - @check(name="File Descriptor size check") + @check(name="File Descriptor size check", depends_on=("File Descriptor Existence",)) def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty @@ -68,7 +68,7 @@ class FileDescriptorJsonFormat(PyFunctionCheck): The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor JSON format") + @check(name="File Descriptor JSON format", depends_on=("File Descriptor Existence",)) def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: @@ -212,7 +212,10 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) # return if the context is valid return is_valid - @check(name="File Descriptor @context property validation") + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) def check_context(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor contains @@ -236,7 +239,10 @@ def check_context(self, context: ValidationContext) -> CheckResultValue: logger.exception("Error extracting @context from file descriptor") return False - @check(name="File Descriptor JSON-LD must be flattened") + @check( + name="File Descriptor JSON-LD must be flattened", + depends_on=("File Descriptor JSON format",), + ) def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" return self._check_flattened_graph( @@ -332,7 +338,10 @@ def _check_flattened_graph(self, context, is_flat): logger.exception("Error flattening JSON-LD file descriptor") return False - @check(name="Validation of the @id property of the file descriptor entities") + @check( + name="Validation of the @id property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: @@ -355,7 +364,10 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: logger.exception("Error validating @id property of file descriptor entities") return False - @check(name="Validation of the @type property of the file descriptor entities") + @check( + name="Validation of the @type property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: @@ -450,7 +462,10 @@ def __record_unexpected_key__(self, k: str, context_keys: set, unexpected_keys: logger.debug(f"Key {k} does not have a valid prefix in context keys, adding to unexpected keys") unexpected_keys[k] = unexpected_keys.get(k, 0) + 1 - @check(name="Validation of the compaction format of the file descriptor") + @check( + name="Validation of the compaction format of the file descriptor", + depends_on=("File Descriptor JSON format",), + ) def check_compaction(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the **compacted** JSON-LD format""" try: diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index fb0ec124f..fb611964d 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -72,7 +72,7 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: return False return True - @check(name="File Descriptor size check") + @check(name="File Descriptor size check", depends_on=("File Descriptor Existence",)) def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty @@ -92,7 +92,7 @@ class FileDescriptorEncodingCheck(PyFunctionCheck): The file descriptor MUST be UTF-8 encoded """ - @check(name="File Descriptor UTF-8 encoding") + @check(name="File Descriptor UTF-8 encoding", depends_on=("File Descriptor Existence",)) def check(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is UTF-8 encoded @@ -122,7 +122,7 @@ class FileDescriptorJsonFormat(PyFunctionCheck): The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor JSON format") + @check(name="File Descriptor JSON format", depends_on=("File Descriptor UTF-8 encoding",)) def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: @@ -251,7 +251,10 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) # return if the context is valid return is_valid - @check(name="File Descriptor @context property validation") + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) def check_context(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor contains @@ -369,7 +372,10 @@ def _is_entity_flat(self, context: ValidationContext, entity: Any, is_first: boo return False return result - @check(name="File Descriptor JSON-LD must be flattened") + @check( + name="File Descriptor JSON-LD must be flattened", + depends_on=("File Descriptor JSON format",), + ) def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" try: @@ -395,7 +401,10 @@ def check_flattened(self, context: ValidationContext) -> CheckResultValue: logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of the @id property of the file descriptor entities") + @check( + name="Validation of the @id property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: @@ -418,7 +427,10 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of the @type property of the file descriptor entities") + @check( + name="Validation of the @type property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: @@ -441,7 +453,10 @@ def check_types(self, context: ValidationContext) -> CheckResultValue: logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of unique @id values") + @check( + name="Validation of unique @id values", + depends_on=("File Descriptor JSON format",), + ) def check_unique_identifiers(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() @@ -485,7 +500,10 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke return message return None - @check(name="Validation of entity references") + @check( + name="Validation of entity references", + depends_on=("File Descriptor JSON format",), + ) def check_entity_references(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() @@ -510,7 +528,10 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of subject and keyword properties") + @check( + name="Validation of subject and keyword properties", + depends_on=("File Descriptor JSON format",), + ) def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() @@ -599,7 +620,10 @@ def add_unexpected_key(k: str, u_keys: dict) -> None: return unexpected_keys - @check(name="Validation of the compaction format of the file descriptor") + @check( + name="Validation of the compaction format of the file descriptor", + depends_on=("File Descriptor JSON format",), + ) def check_compaction(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the **compacted** JSON-LD format""" try: From 7a5a17497426e5cd86eb219ca5ca2c5df2fe53ec Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 10:57:42 +0200 Subject: [PATCH 046/271] feat(validation): :shield: propagate dependency outcomes Make depends_on an execution prerequisite. Skip checks whose dependencies failed, were skipped, or were not processed, propagate the outcome transitively, and preserve independent check execution. Add focused tests for direct, transitive, and independent dependencies. --- rocrate_validator/models/requirement.py | 53 +++++- tests/unit/test_dependency_propagation.py | 189 ++++++++++++++++++++++ 2 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_dependency_propagation.py diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 39001a7dc..f149f71a8 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -224,7 +224,15 @@ def _do_validate_(self, context: ValidationContext) -> bool: for _ in self._checks if not context.settings.skip_checks or _.identifier not in context.settings.skip_checks ] + configured_skips = [check for check in self._checks if check not in checks_to_perform] + for check in configured_skips: + context.result._add_skipped_check(check) for check in checks_to_perform: + dependency_skip_reason = self.__dependency_skip_reason__(check, context) + if dependency_skip_reason: + logger.debug("Skipping check '%s' because: %s", check.name, dependency_skip_reason) + self.__record_dependency_skip__(check, context, dependency_skip_reason) + continue try: all_passed, should_break = self.__execute_check__(check, context, all_passed) if should_break: @@ -244,8 +252,6 @@ def _do_validate_(self, context: ValidationContext) -> bool: # Stop running further checks once the metadata is known to be unusable. if context.aborted: break - skipped_checks = set(self._checks) - set(checks_to_perform) - context.result.skipped_checks.update(skipped_checks) logger.debug( "Checks for Requirement '%s' completed. Checks passed? %s", self.name, @@ -253,6 +259,49 @@ def _do_validate_(self, context: ValidationContext) -> bool: ) return all_passed + @staticmethod + def __dependency_skip_reason__(check, context: ValidationContext) -> str | None: + if not check.depends_on: + return None + + blocked_dependencies = [] + for dependency_name in check.depends_on: + dependency = check.requirement.profile.get_requirement_check(dependency_name) + if dependency is None: + raise CheckDependencyError( + f"check {check.name!r} depends on unknown check {dependency_name!r}", + check.requirement.profile.identifier, + ) + dependency_result = context.result.get_check_result(dependency) + if dependency_result is not CheckResult.PASSED: + result_name = dependency_result.value if dependency_result else "not processed" + blocked_dependencies.append(f"{dependency.name} ({result_name})") + + if not blocked_dependencies: + return None + return "Check dependency did not pass: " + ", ".join(blocked_dependencies) + + @staticmethod + def __record_dependency_skip__(check, context: ValidationContext, message: str) -> None: + from rocrate_validator.models.events import ( # noqa: PLC0415 + RequirementCheckValidationEvent, + ) + + context.result._add_skipped_check(check) + inherited_reporting_disabled = ( + check.requirement.profile.identifier != context.profile_identifier + and context.settings.disable_inherited_profiles_issue_reporting + ) + if not inherited_reporting_disabled: + context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, + check, + validation_result=CheckResult.SKIPPED, + message=message, + ) + ) + def __execute_check__(self, check, context, all_passed): from rocrate_validator.models.events import ( # noqa: PLC0415 RequirementCheckValidationEvent, diff --git a/tests/unit/test_dependency_propagation.py b/tests/unit/test_dependency_propagation.py new file mode 100644 index 000000000..66a7d1d32 --- /dev/null +++ b/tests/unit/test_dependency_propagation.py @@ -0,0 +1,189 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace + +from rocrate_validator.models import CheckResult, Requirement, RequirementCheck, RequirementLoader +from rocrate_validator.models.check_result import normalize_check_result + + +class _Profile: + identifier = "test" + + def __init__(self): + self.requirements = [] + + @property + def parents(self): + return [] + + @property + def siblings(self): + return [] + + def get_requirement_check(self, name): + for requirement in self.requirements: + check = requirement.get_check(name) + if check: + return check + return None + + +class _Requirement(Requirement): + def __init__(self, profile, name): + super().__init__(profile, name=name, initialize_checks=False) + profile.requirements.append(self) + + @property + def hidden(self): + return False + + def __init_checks__(self): + return [] + + +class _Check(RequirementCheck): + def __init__(self, requirement, name, result, depends_on=()): + super().__init__(requirement, name, depends_on=depends_on) + self.result = result + self.calls = 0 + + def execute_check(self, context): + self.calls += 1 + return self.result + + +class _Result: + def __init__(self): + self._check_results = {} + self.skipped_checks = set() + + def _add_executed_check(self, check, result): + normalized_result = normalize_check_result(result) + self._check_results[check.identifier] = normalized_result + if normalized_result is CheckResult.SKIPPED: + self.skipped_checks.add(check) + + def _add_skipped_check(self, check): + self._check_results[check.identifier] = CheckResult.SKIPPED + self.skipped_checks.add(check) + + def get_check_result(self, check): + return self._check_results.get(check.identifier) + + +class _Validator: + def __init__(self): + self.events = [] + + def notify(self, event): + self.events.append(event) + + +class _Context: + def __init__(self, profile, skip_checks=(), fail_fast=False): + self.profile_identifier = profile.identifier + self.settings = SimpleNamespace( + skip_checks=list(skip_checks), + disable_inherited_profiles_issue_reporting=False, + ) + self.result = _Result() + self.validator = _Validator() + self.fail_fast = fail_fast + self.aborted = False + + +def _check(requirement, name, result, depends_on=(), deactivated=False): + check = _Check(requirement, name, result, depends_on=depends_on) + check._deactivated = deactivated + requirement._checks.append(check) + check.order_number = len(requirement._checks) + return check + + +def _run(requirements, *, skip_checks=(), fail_fast=False): + profile = requirements[0].profile + ordered_requirements = RequirementLoader.order_by_dependencies(requirements) + for order_number, requirement in enumerate(ordered_requirements, start=1): + requirement._order_number = order_number + context = _Context(profile, skip_checks=skip_checks, fail_fast=fail_fast) + for requirement in ordered_requirements: + requirement._do_validate_(context) + return context + + +def test_failed_dependency_skips_dependent_check(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + dependent_requirement = _Requirement(profile, "Dependent") + base = _check(base_requirement, "base", False) + dependent = _check(dependent_requirement, "dependent", True, depends_on=("base",)) + + context = _run([dependent_requirement, base_requirement]) + + assert context.result.get_check_result(base) is CheckResult.FAILED + assert context.result.get_check_result(dependent) is CheckResult.SKIPPED + assert dependent.calls == 0 + + +def test_skipped_dependency_skips_dependent_check(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + dependent_requirement = _Requirement(profile, "Dependent") + base = _check(base_requirement, "base", CheckResult.SKIPPED) + dependent = _check(dependent_requirement, "dependent", True, depends_on=("base",)) + + context = _run([base_requirement, dependent_requirement]) + + assert context.result.get_check_result(base) is CheckResult.SKIPPED + assert context.result.get_check_result(dependent) is CheckResult.SKIPPED + assert dependent.calls == 0 + + +def test_dependency_skip_propagates_transitively(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + middle_requirement = _Requirement(profile, "Middle") + leaf_requirement = _Requirement(profile, "Leaf") + base = _check(base_requirement, "base", False) + middle = _check(middle_requirement, "middle", True, depends_on=("base",)) + leaf = _check(leaf_requirement, "leaf", True, depends_on=("middle",)) + + context = _run([leaf_requirement, middle_requirement, base_requirement]) + + assert context.result.get_check_result(base) is CheckResult.FAILED + assert context.result.get_check_result(middle) is CheckResult.SKIPPED + assert context.result.get_check_result(leaf) is CheckResult.SKIPPED + assert middle.calls == 0 + assert leaf.calls == 0 + + +def test_independent_checks_continue_after_dependency_failure(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + dependent_requirement = _Requirement(profile, "Dependent") + independent_requirement = _Requirement(profile, "Independent") + base = _check(base_requirement, "base", False) + dependent = _check(dependent_requirement, "dependent", True, depends_on=("base",)) + independent = _check(independent_requirement, "independent", True) + + context = _run( + [dependent_requirement, independent_requirement, base_requirement], + fail_fast=False, + ) + + assert context.result.get_check_result(base) is CheckResult.FAILED + assert context.result.get_check_result(dependent) is CheckResult.SKIPPED + assert context.result.get_check_result(independent) is CheckResult.PASSED + assert independent.calls == 1 From c30448029d979b6af44d0ccf67831e8076e2eaf4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 12:02:50 +0200 Subject: [PATCH 047/271] feat(models): add structured skipped-check details Add SkipCategory and SkippedCheckDetail models with stable serialized values, human-readable descriptions, and validation-result tracking. --- rocrate_validator/models/__init__.py | 9 ++- rocrate_validator/models/result.py | 44 ++++++++++++++- rocrate_validator/models/skipped_check.py | 68 +++++++++++++++++++++++ 3 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 rocrate_validator/models/skipped_check.py diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index e979d8d31..5d4174969 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -28,11 +28,7 @@ SkipRequirementCheck, SourceSnippet, ) -from rocrate_validator.models.result import ( - CheckIssue, - CustomEncoder, - ValidationResult, -) +from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationResult from rocrate_validator.models.settings import ( DEFAULT_PROFILES_PATH, BaseTypes, @@ -43,6 +39,7 @@ RequirementLevel, Severity, ) +from rocrate_validator.models.skipped_check import SkipCategory, SkippedCheckDetail from rocrate_validator.models.statistics import ( AggregatedValidationStatistics, ValidationStatistics, @@ -73,7 +70,9 @@ "RequirementLoader", "RequirementValidationEvent", "Severity", + "SkipCategory", "SkipRequirementCheck", + "SkippedCheckDetail", "SourceSnippet", "ValidationContext", "ValidationEvent", diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index af038ab5a..73058390d 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -32,6 +32,7 @@ RequirementLevel, Severity, ) +from rocrate_validator.models.skipped_check import SkipCategory, SkippedCheckDetail if TYPE_CHECKING: from collections.abc import Collection @@ -201,6 +202,7 @@ def __init__(self, context: ValidationContext): self._check_results: dict[str, CheckResult] = {} # keep track of the checks that have been skipped self._skipped_checks: set[RequirementCheck] = set() + self._skipped_check_details: dict[str, SkippedCheckDetail] = {} # initialize the statistics self._statistics = ValidationStatistics(context.settings) @@ -241,19 +243,34 @@ def executed_checks(self) -> set[RequirementCheck]: """ return self._executed_checks - def _record_check_result(self, check: RequirementCheck, result: CheckResultValue) -> CheckResult: + def _record_check_result( + self, + check: RequirementCheck, + result: CheckResultValue, + skip_message: str | None = None, + skip_category: SkipCategory = SkipCategory.RETURNED, + ) -> CheckResult: """Record a normalized result and keep check collections consistent.""" normalized_result = normalize_check_result(result) self._check_results[check.identifier] = normalized_result if normalized_result is CheckResult.SKIPPED: + skip_category = SkipCategory(skip_category) self._executed_checks.discard(check) self._skipped_checks.add(check) self._executed_checks_results.pop(check.identifier, None) + current_detail = self._skipped_check_details.get(check.identifier) + if current_detail is None or (skip_message and current_detail.category == "returned"): + self._skipped_check_details[check.identifier] = SkippedCheckDetail( + check=check, + message=skip_message or "Check returned SKIPPED", + category=skip_category, + ) return normalized_result self._executed_checks.add(check) self._skipped_checks.discard(check) + self._skipped_check_details.pop(check.identifier, None) self._executed_checks_results[check.identifier] = normalized_result is CheckResult.PASSED return normalized_result @@ -289,17 +306,37 @@ def skipped_checks_count(self) -> int: """Get the number of skipped checks.""" return len(self._skipped_checks) - def _add_skipped_check(self, check: RequirementCheck): + @property + def skipped_check_details(self) -> list[SkippedCheckDetail]: + """Get the ordered explanations for skipped checks.""" + return list(self._skipped_check_details.values()) + + def record_skip( + self, + check: RequirementCheck, + message: str, + category: SkipCategory = SkipCategory.RETURNED, + ) -> None: + """Record a skip message while preserving the normalized skipped result.""" + self._record_check_result(check, CheckResult.SKIPPED, message, category) + + def _add_skipped_check( + self, + check: RequirementCheck, + message: str = "Check was skipped", + category: SkipCategory = SkipCategory.RETURNED, + ): """ Internal method to add a check to the skipped checks """ - self._record_check_result(check, CheckResult.SKIPPED) + self._record_check_result(check, CheckResult.SKIPPED, message, category) def _remove_skipped_check(self, check: RequirementCheck): """ Internal method to remove a check from the skipped checks """ self._skipped_checks.remove(check) + self._skipped_check_details.pop(check.identifier, None) # --- Issues --- @property @@ -434,6 +471,7 @@ def to_dict(self) -> dict: "passed": self.passed(cast("Severity", self.context.settings.requirement_severity)), "issues": [issue.to_dict() for issue in self.issues], "skipped_checks": self.skipped_checks_count, + "skipped_check_details": [detail.to_dict() for detail in self.skipped_check_details], } # add validator version to the settings result["validation_settings"]["rocrate_validator_version"] = __version__ diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py new file mode 100644 index 000000000..ce151ef3a --- /dev/null +++ b/rocrate_validator/models/skipped_check.py @@ -0,0 +1,68 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum, unique +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from rocrate_validator.models.requirement import RequirementCheck + + +@unique +class SkipCategory(str, Enum): + """Reason category for a skipped requirement check.""" + + RETURNED = ("returned", "The check returned SKIPPED without another skip reason.") + CONFIGURED = ("configured", "The check was skipped by validation settings.") + DEACTIVATED = ("deactivated", "The check is deactivated in the profile.") + DEPENDENCY = ("dependency", "The check was skipped because a dependency did not pass.") + NOT_REACHED = ("not_reached", "The check was not reached because validation stopped earlier.") + INHERITED = ("inherited", "The check belongs to an inherited profile that was not reported or run.") + + def __new__(cls, value: str, description: str): + member = str.__new__(cls, value) + member._value_ = value + member._description = description + return member + + @property + def description(self) -> str: + """Human-readable explanation of this category.""" + return self._description + + +@dataclass(frozen=True) +class SkippedCheckDetail: + """Structured information explaining why a check was skipped.""" + + check: RequirementCheck + message: str + category: SkipCategory = SkipCategory.RETURNED + + def __post_init__(self) -> None: + object.__setattr__(self, "category", SkipCategory(self.category)) + + def to_dict(self) -> dict[str, str]: + return { + "identifier": self.check.identifier, + "profile": self.check.requirement.profile.identifier, + "requirement": self.check.requirement.name, + "name": self.check.name, + "severity": self.check.severity.name, + "message": self.message, + "category": self.category.value, + } From af0c8de70c78b365008eb726cf28a76758c0731d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 12:07:25 +0200 Subject: [PATCH 048/271] test(models): cover structured skipped-check details Cover skip detail serialization, lifecycle cleanup, enum values, and category descriptions. --- tests/unit/test_skipped_check_details.py | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/unit/test_skipped_check_details.py diff --git a/tests/unit/test_skipped_check_details.py b/tests/unit/test_skipped_check_details.py new file mode 100644 index 000000000..d08fa62e9 --- /dev/null +++ b/tests/unit/test_skipped_check_details.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace + +from rocrate_validator.models import CheckResult, SkipCategory, ValidationResult + + +def _result(): + result = object.__new__(ValidationResult) + result._check_results = {} + result._executed_checks = set() + result._executed_checks_results = {} + result._skipped_checks = set() + result._skipped_check_details = {} + return result + + +def _check(): + check = SimpleNamespace( + identifier="test_1.1", + name="dependent", + severity=SimpleNamespace(name="REQUIRED"), + requirement=SimpleNamespace( + name="Dependent requirement", + profile=SimpleNamespace(identifier="test"), + ), + ) + return type("TestCheck", (), check.__dict__)() + + +def test_skipped_check_detail_preserves_reason_and_category(): + result = _result() + check = _check() + + result._record_check_result(check, CheckResult.SKIPPED, "base failed", SkipCategory.DEPENDENCY) + + assert result.skipped_check_details[0].category is SkipCategory.DEPENDENCY + assert result.skipped_check_details[0].to_dict() == { + "identifier": "test_1.1", + "profile": "test", + "requirement": "Dependent requirement", + "name": "dependent", + "severity": "REQUIRED", + "message": "base failed", + "category": "dependency", + } + + +def test_skipped_check_detail_is_removed_when_check_passes(): + result = _result() + check = _check() + + result.record_skip(check, "base skipped", SkipCategory.DEPENDENCY) + result._record_check_result(check, CheckResult.PASSED) + + assert result.skipped_check_details == [] + + +def test_skip_category_has_stable_values_and_descriptions(): + assert {category.value for category in SkipCategory} == { + "returned", + "configured", + "deactivated", + "dependency", + "not_reached", + "inherited", + } + assert all(category.description for category in SkipCategory) + assert SkipCategory.DEPENDENCY.description == "The check was skipped because a dependency did not pass." From 035e5485c7f439c927bd1709ad6f5b83b04b87ca Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 12:19:09 +0200 Subject: [PATCH 049/271] feat(validation): record structured skip reasons Classify configured, deactivated, dependency, inherited, and not-reached skips and expose their messages through validation events and results. --- rocrate_validator/models/requirement.py | 107 ++++++++++++++---- rocrate_validator/models/validation.py | 32 +++++- .../requirements/shacl/validator.py | 12 +- 3 files changed, 123 insertions(+), 28 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index f149f71a8..1cc04a3b4 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -34,6 +34,7 @@ RequirementLevel, Severity, ) +from rocrate_validator.models.skipped_check import SkipCategory from rocrate_validator.utils import log as logging from rocrate_validator.utils.python_helpers import ( get_requirement_name_from_file, @@ -47,9 +48,15 @@ class SkipRequirementCheck(Exception): - def __init__(self, check: RequirementCheck, message: str = ""): + def __init__( + self, + check: RequirementCheck, + message: str = "", + category: SkipCategory = SkipCategory.RETURNED, + ): self.check = check self.message = message + self.category = SkipCategory(category) def __str__(self): return f"SkipRequirementCheck(check={self.check})" @@ -226,8 +233,15 @@ def _do_validate_(self, context: ValidationContext) -> bool: ] configured_skips = [check for check in self._checks if check not in checks_to_perform] for check in configured_skips: - context.result._add_skipped_check(check) + context.result._record_check_result( + check, + CheckResult.SKIPPED, + "Check was skipped by validation settings", + SkipCategory.CONFIGURED, + ) + processed_checks: set[RequirementCheck] = set() for check in checks_to_perform: + processed_checks.add(check) dependency_skip_reason = self.__dependency_skip_reason__(check, context) if dependency_skip_reason: logger.debug("Skipping check '%s' because: %s", check.name, dependency_skip_reason) @@ -239,7 +253,7 @@ def _do_validate_(self, context: ValidationContext) -> bool: break except SkipRequirementCheck as e: logger.debug("Skipping check '%s' because: %s", check.name, e) - context.result._add_skipped_check(check) + self.__record_skipped_check__(check, context, e.message or "Check requested a skip", e.category) continue except Exception as e: if context.maybe_warn_offline_cache_miss(e): @@ -252,6 +266,18 @@ def _do_validate_(self, context: ValidationContext) -> bool: # Stop running further checks once the metadata is known to be unusable. if context.aborted: break + self.__record_skipped_checks__( + configured_skips, + context, + "Check was skipped by validation settings", + SkipCategory.CONFIGURED, + ) + self.__record_skipped_checks__( + set(checks_to_perform) - processed_checks, + context, + "Validation stopped before this check was executed", + SkipCategory.NOT_REACHED, + ) logger.debug( "Checks for Requirement '%s' completed. Checks passed? %s", self.name, @@ -259,6 +285,52 @@ def _do_validate_(self, context: ValidationContext) -> bool: ) return all_passed + @staticmethod + def __record_skipped_check__( + check, + context, + message: str = "Check was skipped", + category: SkipCategory = SkipCategory.RETURNED, + ) -> None: + from rocrate_validator.models.events import ( # noqa: PLC0415 + RequirementCheckValidationEvent, + ) + + context.result._record_check_result(check, CheckResult.SKIPPED, message, category) + inherited_reporting_disabled = ( + check.requirement.profile.identifier != context.profile_identifier + and context.settings.disable_inherited_profiles_issue_reporting + ) + if not inherited_reporting_disabled: + context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, + check, + validation_result=CheckResult.SKIPPED, + message=message, + ) + ) + + @classmethod + def __record_skipped_checks__( + cls, + checks, + context, + message: str = "Check was skipped", + category: SkipCategory = SkipCategory.RETURNED, + ) -> None: + for check in sorted(checks): + cls.__record_skipped_check__(check, context, message, category) + + @classmethod + def record_skipped_checks(cls, requirements: list[Requirement], context: ValidationContext) -> None: + """Record checks belonging to requirements that validation will not reach.""" + message = "Validation stopped before this check was executed" + if context.abort_reason: + message = f"Validation stopped before this check was executed: {context.abort_reason}" + for requirement in requirements: + cls.__record_skipped_checks__(requirement.get_checks(), context, message, SkipCategory.NOT_REACHED) + @staticmethod def __dependency_skip_reason__(check, context: ValidationContext) -> str | None: if not check.depends_on: @@ -283,24 +355,7 @@ def __dependency_skip_reason__(check, context: ValidationContext) -> str | None: @staticmethod def __record_dependency_skip__(check, context: ValidationContext, message: str) -> None: - from rocrate_validator.models.events import ( # noqa: PLC0415 - RequirementCheckValidationEvent, - ) - - context.result._add_skipped_check(check) - inherited_reporting_disabled = ( - check.requirement.profile.identifier != context.profile_identifier - and context.settings.disable_inherited_profiles_issue_reporting - ) - if not inherited_reporting_disabled: - context.validator.notify( - RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, - check, - validation_result=CheckResult.SKIPPED, - message=message, - ) - ) + Requirement.__record_skipped_check__(check, context, message, SkipCategory.DEPENDENCY) def __execute_check__(self, check, context, all_passed): from rocrate_validator.models.events import ( # noqa: PLC0415 @@ -316,7 +371,7 @@ def __execute_check__(self, check, context, all_passed): return all_passed, False if check.deactivated: logger.debug("Skipping check '%s' because deactivated", check.identifier) - context.result._add_skipped_check(check) + self.__record_skipped_check__(check, context, "Check is deactivated", SkipCategory.DEACTIVATED) return all_passed, False # Determine whether to skip event notification for inherited profiles skip_event_notify = False @@ -340,7 +395,12 @@ def __execute_check__(self, check, context, all_passed): check_result = check.execute_check(context) normalized_result = normalize_check_result(check_result) logger.debug("Result of check %s: %s", check.identifier, normalized_result.value) - context.result._add_executed_check(check, normalized_result) + skip_message = None + skip_category = SkipCategory.RETURNED + if skip_event_notify and normalized_result is CheckResult.SKIPPED: + skip_message = "Inherited profile issue reporting is disabled" + skip_category = SkipCategory.INHERITED + context.result._record_check_result(check, normalized_result, skip_message, skip_category) # Notify the end of the check execution if not skip_event_notify is set to True if not skip_event_notify: context.validator.notify( @@ -348,6 +408,7 @@ def __execute_check__(self, check, context, all_passed): EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=normalized_result, + message=skip_message, ) ) logger.debug( diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 3974aabe0..9974d482b 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -34,11 +34,13 @@ from rocrate_validator.models.profile import Profile from rocrate_validator.models.requirement import ( Requirement, + RequirementCheck, RequirementLoader, ) from rocrate_validator.models.result import ValidationResult from rocrate_validator.models.settings import ValidationSettings from rocrate_validator.models.severity import Severity +from rocrate_validator.models.skipped_check import SkipCategory from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import find_offline_cache_miss @@ -162,7 +164,7 @@ def validate_requirements( ) return self.__do_validate__(resolved_requirements) - def __do_validate__(self, requirements: list[Requirement] | None = None) -> ValidationResult: # noqa: C901, PLR0912 + def __do_validate__(self, requirements: list[Requirement] | None = None) -> ValidationResult: # noqa: C901, PLR0912, PLR0915 # initialize the validation context context = ValidationContext(self, self.validation_settings) @@ -186,7 +188,7 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali None if requirements is None else {id(requirement) for requirement in requirements} ) self.notify(EventType.VALIDATION_START) - for profile in profiles: + for profile_index, profile in enumerate(profiles): logger.debug( "Validating profile %s (id: %s)", profile.name, @@ -219,7 +221,8 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali profile_requirements, ) terminate = False - for requirement in profile_requirements: + for requirement_index in range(len(profile_requirements)): + requirement = profile_requirements[requirement_index] if not requirement.overridden: self.notify( RequirementValidationEvent( @@ -257,6 +260,20 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali break self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_END, profile=profile)) if terminate: + Requirement.record_skipped_checks(profile_requirements[requirement_index + 1 :], context) + for remaining_profile in profiles[profile_index + 1 :]: + if selected_requirement_ids is None: + remaining_requirements = remaining_profile.get_requirements( + context.requirement_severity, + exact_match=context.requirement_severity_only, + ) + else: + remaining_requirements = [ + requirement + for requirement in remaining_profile.requirements + if id(requirement) in selected_requirement_ids + ] + Requirement.record_skipped_checks(remaining_requirements, context) break # finalize the requirement types @@ -364,6 +381,15 @@ def result(self) -> ValidationResult: self._result = ValidationResult(self) return self._result + def record_skip( + self, + check: RequirementCheck, + message: str, + category: SkipCategory = SkipCategory.RETURNED, + ) -> None: + """Record a structured reason while a check is returning ``SKIPPED``.""" + self.result.record_skip(check, message, category) + @property def settings(self) -> ValidationSettings: """ diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index dc4b73448..241a9c22a 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -33,7 +33,13 @@ VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES, ) -from rocrate_validator.models import Profile, RequirementCheck, Severity, ValidationContext, ValidationResult +from rocrate_validator.models import ( + Profile, + RequirementCheck, + Severity, + ValidationContext, + ValidationResult, +) from rocrate_validator.requirements.shacl.models import ShapesRegistry from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging @@ -69,7 +75,9 @@ def __enter__(self) -> SHACLValidationContext: logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.identifier) if self._profile.identifier != self._context.settings.profile_identifier: logger.debug("Skipping validation of profile %s", self._profile.identifier) - self.context.result._add_skipped_check(self._check) + # This is a temporary control-flow skip. The target profile runs + # SHACL once over the merged shapes graph and records the check + # outcome afterwards. raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.identifier}") logger.debug("ValidationContext of profile %s initialized", self._profile.identifier) return self._shacl_context From 44b08b10c6b178c3fe1dbea1b233cacb66bfce60 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 12:19:59 +0200 Subject: [PATCH 050/271] refactor(models): group skipped-check types Move SkipRequirementCheck next to SkipCategory and SkippedCheckDetail while preserving the public model exports. --- rocrate_validator/models/__init__.py | 3 +-- rocrate_validator/models/requirement.py | 17 +---------------- rocrate_validator/models/skipped_check.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index 5d4174969..559da0e0e 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -25,7 +25,6 @@ Requirement, RequirementCheck, RequirementLoader, - SkipRequirementCheck, SourceSnippet, ) from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationResult @@ -39,7 +38,7 @@ RequirementLevel, Severity, ) -from rocrate_validator.models.skipped_check import SkipCategory, SkippedCheckDetail +from rocrate_validator.models.skipped_check import SkipCategory, SkippedCheckDetail, SkipRequirementCheck from rocrate_validator.models.statistics import ( AggregatedValidationStatistics, ValidationStatistics, diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 1cc04a3b4..d822fb810 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -34,7 +34,7 @@ RequirementLevel, Severity, ) -from rocrate_validator.models.skipped_check import SkipCategory +from rocrate_validator.models.skipped_check import SkipCategory, SkipRequirementCheck from rocrate_validator.utils import log as logging from rocrate_validator.utils.python_helpers import ( get_requirement_name_from_file, @@ -47,21 +47,6 @@ from rocrate_validator.models.validation import ValidationContext -class SkipRequirementCheck(Exception): - def __init__( - self, - check: RequirementCheck, - message: str = "", - category: SkipCategory = SkipCategory.RETURNED, - ): - self.check = check - self.message = message - self.category = SkipCategory(category) - - def __str__(self): - return f"SkipRequirementCheck(check={self.check})" - - @total_ordering class Requirement(ABC): """ diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py index ce151ef3a..fe56104ef 100644 --- a/rocrate_validator/models/skipped_check.py +++ b/rocrate_validator/models/skipped_check.py @@ -45,6 +45,23 @@ def description(self) -> str: return self._description +class SkipRequirementCheck(Exception): + """Signal that a requirement check should be skipped.""" + + def __init__( + self, + check: RequirementCheck, + message: str = "", + category: SkipCategory = SkipCategory.RETURNED, + ): + self.check = check + self.message = message + self.category = SkipCategory(category) + + def __str__(self) -> str: + return f"SkipRequirementCheck(check={self.check})" + + @dataclass(frozen=True) class SkippedCheckDetail: """Structured information explaining why a check was skipped.""" From 95437bab7bb3b291e24e58b3bb4bdb821568e8bb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 12:21:48 +0200 Subject: [PATCH 051/271] test(validation): update dependency propagation fixture Adapt the dependency propagation result fixture to the structured check-result recording API. --- tests/unit/test_dependency_propagation.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_dependency_propagation.py b/tests/unit/test_dependency_propagation.py index 66a7d1d32..7e0ff00ac 100644 --- a/tests/unit/test_dependency_propagation.py +++ b/tests/unit/test_dependency_propagation.py @@ -14,7 +14,7 @@ from types import SimpleNamespace -from rocrate_validator.models import CheckResult, Requirement, RequirementCheck, RequirementLoader +from rocrate_validator.models import CheckResult, Requirement, RequirementCheck, RequirementLoader, SkipCategory from rocrate_validator.models.check_result import normalize_check_result @@ -69,15 +69,26 @@ def __init__(self): self._check_results = {} self.skipped_checks = set() - def _add_executed_check(self, check, result): + def _record_check_result( + self, + check, + result, + skip_message=None, + skip_category=SkipCategory.RETURNED, + ): normalized_result = normalize_check_result(result) self._check_results[check.identifier] = normalized_result if normalized_result is CheckResult.SKIPPED: self.skipped_checks.add(check) + else: + self.skipped_checks.discard(check) + return normalized_result + + def _add_executed_check(self, check, result): + self._record_check_result(check, result) def _add_skipped_check(self, check): - self._check_results[check.identifier] = CheckResult.SKIPPED - self.skipped_checks.add(check) + self._record_check_result(check, CheckResult.SKIPPED) def get_check_result(self, check): return self._check_results.get(check.identifier) From 70c47ab5a0b4c93ed326e57ad0d06a24121d4c8c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 15:05:24 +0200 Subject: [PATCH 052/271] feat(cli): :sparkles: expose skipped-check details Add the --show-skipped-checks option to the validate command. Render skipped checks with their profile, identifier, category, and reason. Include skipped-check counts in the live validation report and support appending the details to text report files. --- rocrate_validator/cli/commands/validate.py | 67 +++++++++++++++++++ .../io_helpers/output/text/layout/report.py | 18 ++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 91e1e6c79..640164e40 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -21,6 +21,8 @@ import rich_click as click from rich.padding import Padding from rich.rule import Rule +from rich.table import Table +from rich.text import Text from rocrate_validator import constants, services from rocrate_validator.cli.commands.errors import handle_error @@ -29,6 +31,7 @@ from rocrate_validator.errors import ROCrateInvalidURIError from rocrate_validator.models import Severity, ValidationResult, ValidationSettings from rocrate_validator.utils import log as logging +from rocrate_validator.utils.io_helpers.colors import get_severity_color from rocrate_validator.utils.io_helpers.input import get_single_char, multiple_choice from rocrate_validator.utils.io_helpers.output.console import Console from rocrate_validator.utils.io_helpers.output.json import JSONOutputFormatter @@ -172,6 +175,13 @@ def validate_uri(ctx, param, value): "[bold orange1]rocrate-validator profiles describe -v[/bold orange1]" ), ) +@click.option( + "--show-skipped-checks", + is_flag=True, + help="List skipped checks and their reasons at the end of the validation", + default=False, + show_default=True, +) @click.option( "-v", "--verbose", @@ -250,6 +260,7 @@ def validate_uri(ctx, param, value): @click.pass_context def validate( ctx, + *, profiles_path: Path = DEFAULT_PROFILES_PATH, extra_profiles_path: Path | None = None, profile_identifier: tuple[str, ...] = (), @@ -262,6 +273,7 @@ def validate( requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, skip_checks: list[str] | None = None, + show_skipped_checks: bool = False, rocrate_uri: str | Path = ".", relative_root_path: Path | None = None, fail_fast: bool = False, @@ -412,6 +424,14 @@ def validate( output_file=output_file, output_line_width=output_line_width, ) + _emit_skipped_checks_report( + results, + show_skipped_checks=show_skipped_checks, + output_format=output_format, + output_file=output_file, + output_line_width=output_line_width, + console=console, + ) # Exit with appropriate status code. # using ctx.exit seems to raise an Exception that gets caught below, @@ -677,3 +697,50 @@ def _emit_json_report( if interactive and output_file: console.print("[bold]DONE![/bold]", end="\n\n") + + +def _print_skipped_checks(results: dict[str, ValidationResult], console: Console) -> None: + """Render the collected skipped-check details using the validation palette.""" + details = [detail for result in results.values() for detail in result.skipped_check_details] + if not details: + return + + table = Table( + title=f"Skipped Checks ({len(details)})", + title_style="bold yellow", + border_style="yellow", + header_style="bold yellow", + show_lines=False, + ) + table.add_column("Profile", style="cyan", no_wrap=True) + table.add_column("Check", no_wrap=True) + table.add_column("Category", style="yellow", no_wrap=True) + table.add_column("Reason") + for detail in details: + table.add_row( + Text(detail.check.requirement.profile.identifier, style="cyan"), + Text(detail.check.identifier, style=get_severity_color(detail.check.severity)), + Text(detail.category.value, style="yellow"), + Text(f"{detail.check.name}: {detail.message}"), + ) + console.print(Padding(table, (1, 2))) + + +def _emit_skipped_checks_report( + results: dict[str, ValidationResult], + *, + show_skipped_checks: bool, + output_format: str, + output_file: Path | None, + output_line_width: int | None, + console: Console, +) -> None: + """Write the optional skipped-check report without affecting JSON output.""" + if not show_skipped_checks or output_format == "json": + return + if output_file: + with output_file.open("a", encoding="utf-8") as output: + output_console = Console(color_system=None, width=output_line_width, file=output) + _print_skipped_checks(results, output_console) + else: + _print_skipped_checks(results, console) diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/report.py b/rocrate_validator/utils/io_helpers/output/text/layout/report.py index e31b13cf4..1cdb9c1a2 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/report.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/report.py @@ -75,6 +75,7 @@ def __init__( self.requirement_checks_container_layout: Layout | None = None self.passed_checks: Layout | None = None self.failed_checks: Layout | None = None + self.skipped_checks: Layout | None = None self.report_details_container: Layout | None = None self.overall_result: Layout | None = None self.requirement_checks_by_severity_container_layout: Any = None @@ -131,9 +132,10 @@ def _init_layout(self): ) self.passed_checks = Layout(name="PASSED") self.failed_checks = Layout(name="FAILED") + self.skipped_checks = Layout(name="SKIPPED") # Create the layout of the requirement checks section validated_checks_container = Layout(name="Requirement Checks Validated") - validated_checks_container.split_row(self.passed_checks, self.failed_checks) + validated_checks_container.split_row(self.passed_checks, self.failed_checks, self.skipped_checks) # Create the layout of the requirement checks section self.requirement_checks_by_severity_container_layout = Layout(name="Requirement Checks Validation", size=5) @@ -207,7 +209,9 @@ def _init_layout(self): def update_stats(self, profile_stats: ValidationStatistics | None = None): assert profile_stats, "Profile stats must be provided" - assert self.passed_checks is not None and self.failed_checks is not None, "Layout not initialized" + assert self.passed_checks is not None and self.failed_checks is not None and self.skipped_checks is not None, ( + "Layout not initialized" + ) self.requirement_checks_by_severity_container_layout["required"].update( Panel( Align( @@ -265,6 +269,16 @@ def update_stats(self, profile_stats: ValidationStatistics | None = None): ) ) + self.skipped_checks.update( + Panel( + Align(str(len(profile_stats.skipped_checks)), align="center"), + padding=(1, 1), + title="SKIPPED Checks", + title_align="center", + border_style="yellow", + ) + ) + def show_overall_result(self, result: ValidationResult | None): assert result, "Validation result must be provided" assert self.overall_result is not None, "Layout not initialized" From 892460ece97b738a6c9d1c4ee093983cc5d69df2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 15:09:33 +0200 Subject: [PATCH 053/271] feat(json): :sparkles: aggregate skipped-check details --- rocrate_validator/utils/io_helpers/output/json/formatters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/utils/io_helpers/output/json/formatters.py b/rocrate_validator/utils/io_helpers/output/json/formatters.py index ac62ddd33..842098e44 100644 --- a/rocrate_validator/utils/io_helpers/output/json/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/json/formatters.py @@ -71,6 +71,8 @@ def format_validation_results( # Initialize the overall passed status json_output["passed"] = True + json_output["skipped_checks"] = 0 + json_output["skipped_check_details"] = [] # Initialize the profile results dictionary results_key = "validation_results_by_profile" @@ -90,6 +92,8 @@ def format_validation_results( json_output[results_key][profile_identifier] = result_dict # Update the overall passed status json_output["passed"] = json_output["passed"] and result.passed() + json_output["skipped_checks"] += result.skipped_checks_count + json_output["skipped_check_details"].extend(detail.to_dict() for detail in result.skipped_check_details) # Update the overall list of issues if "issues" not in json_output: json_output["issues"] = [] From f9c2616fd984674261a9f3414fe5827f3eee81d0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 14 Sep 2026 15:11:00 +0200 Subject: [PATCH 054/271] test(cli): :white_check_mark: cover skipped-check reporting Cover skipped-check reporting through the CLI. Verify the console table produced by --show-skipped-checks, the text report-file output, and the aggregate skipped-check count and details in JSON reports. --- tests/test_cli.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5f0cdef3d..91971a321 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -27,7 +27,7 @@ from rocrate_validator.utils import log as logging from rocrate_validator.utils.versioning import get_version from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER -from tests.ro_crates import InvalidFileDescriptor, ValidROC +from tests.ro_crates import InvalidFileDescriptor, InvalidFileDescriptorEntity, ValidROC # set up logging logger = logging.getLogger(__name__) @@ -188,6 +188,77 @@ def test_validate_output_file_json_report(cli_runner: CliRunner, tmp_path: Path) json.loads(output_file.read_text(encoding="utf-8")) # must be valid JSON +def test_validate_show_skipped_checks(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, + [ + "validate", + str(InvalidFileDescriptorEntity().invalid_conforms_to), + "--profile-identifier", + "ro-crate-1.1", + "--skip-checks", + "ro-crate-1.1_5.3,ro-crate-1.1_12.1", + "--show-skipped-checks", + "--no-paging", + ], + ) + + assert result.exit_code == 0, result.output + assert "Skipped Checks" in result.output + assert "ro-crate-1.1_5.3" in result.output + assert "configured" in result.output + + +def test_validate_text_report_file_includes_skipped_checks(cli_runner: CliRunner, tmp_path: Path): + output_file = tmp_path / "report.txt" + result = cli_runner.invoke( + cli, + [ + "validate", + str(InvalidFileDescriptorEntity().invalid_conforms_to), + "--profile-identifier", + "ro-crate-1.1", + "--skip-checks", + "ro-crate-1.1_5.3,ro-crate-1.1_12.1", + "--show-skipped-checks", + "--output-file", + str(output_file), + "--no-paging", + ], + ) + + assert result.exit_code == 0, result.output + report = output_file.read_text(encoding="utf-8") + assert "Skipped Checks" in report + assert "ro-crate-1.1_12.1" in report + + +def test_validate_json_report_aggregates_skipped_checks(cli_runner: CliRunner, tmp_path: Path): + output_file = tmp_path / "report.json" + result = cli_runner.invoke( + cli, + [ + "validate", + str(InvalidFileDescriptorEntity().invalid_conforms_to), + "--profile-identifier", + "ro-crate-1.1", + "--skip-checks", + "ro-crate-1.1_5.3,ro-crate-1.1_12.1", + "--output-format", + "json", + "--output-file", + str(output_file), + "--no-paging", + ], + ) + + assert result.exit_code == 0, result.output + payload = json.loads(output_file.read_text(encoding="utf-8")) + assert payload["skipped_checks"] >= 2 + skipped_ids = {detail["identifier"] for detail in payload["skipped_check_details"]} + assert {"ro-crate-1.1_5.3", "ro-crate-1.1_12.1"} <= skipped_ids + + def test_validate_with_invalid_profiles_path_dir(cli_runner: CliRunner): dummy_profiles_path = "/tmp/dummy_profiles" result = cli_runner.invoke( From f2e7f36a9a4fa2b7d77341e3e075bfbd8199aada Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 09:54:45 +0200 Subject: [PATCH 055/271] feat(validation): :boom: propagate execution errors Propagate unexpected check and SHACL engine failures as validation errors instead of warnings, preserving parser location details and original causes. --- rocrate_validator/errors.py | 26 ++++- rocrate_validator/models/requirement.py | 43 +++++---- rocrate_validator/models/validation.py | 94 ++++++++----------- .../requirements/shacl/checks.py | 6 +- .../requirements/shacl/errors.py | 12 ++- .../requirements/shacl/requirements.py | 22 +++-- rocrate_validator/requirements/shacl/utils.py | 17 +++- .../requirements/shacl/validator.py | 47 +++++----- tests/unit/requirements/test_profiles.py | 72 +++++++++++++- tests/unit/requirements/test_shacl_utils.py | 17 +++- tests/unit/test_remote_context_retrieval.py | 12 +++ tests/unit/test_requirement_lifecycle.py | 47 +++++++++- 12 files changed, 302 insertions(+), 113 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 6f8da43f0..41cf52b67 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -198,10 +198,20 @@ def __repr__(self): class BadSyntaxError(ROCValidatorError): """Raised when a syntax error occurs.""" - def __init__(self, message, path: str = ".", code: int = -1): + def __init__( + self, + message, + path: str = ".", + code: int = -1, + *, + line: int | None = None, + character: int | None = None, + ): self._message = message self._path = path self._code = code + self._line = line + self._character = character @property def message(self) -> str: @@ -218,6 +228,16 @@ def code(self) -> int: """The error code.""" return self._code + @property + def line(self) -> int | None: + """The line where the syntax error occurred, when available.""" + return self._line + + @property + def character(self) -> int | None: + """The character where the syntax error occurred, when available.""" + return self._character + def __str__(self) -> str: return self._message @@ -255,6 +275,10 @@ def __repr__(self): return f"ValidationError({self._message!r}, {self._path!r})" +class ValidationExecutionError(ValidationError): + """Raised when validation cannot complete and no result can be trusted.""" + + class CheckValidationError(ValidationError): """Raised when a validation check fails.""" diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index d822fb810..fa842c20a 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -25,7 +25,7 @@ PROFILE_FILE_EXTENSIONS, PROFILE_SPECIFICATION_FILE, ) -from rocrate_validator.errors import CheckDependencyError +from rocrate_validator.errors import CheckDependencyError, ROCrateMetadataNotFoundError, ValidationExecutionError from rocrate_validator.events import EventType from rocrate_validator.models._logging import logger from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result @@ -35,7 +35,6 @@ Severity, ) from rocrate_validator.models.skipped_check import SkipCategory, SkipRequirementCheck -from rocrate_validator.utils import log as logging from rocrate_validator.utils.python_helpers import ( get_requirement_name_from_file, ) @@ -241,13 +240,7 @@ def _do_validate_(self, context: ValidationContext) -> bool: self.__record_skipped_check__(check, context, e.message or "Check requested a skip", e.category) continue except Exception as e: - if context.maybe_warn_offline_cache_miss(e): - logger.debug("Offline cache miss during check %s: %s", check, e) - else: - logger.warning("Unexpected error during check %s. Exception: %s", check, e) - logger.warning("Consider reporting this as a bug.") - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unhandled exception during check execution", exc_info=e) + self.__handle_check_error__(check, context, e) # Stop running further checks once the metadata is known to be unusable. if context.aborted: break @@ -270,6 +263,29 @@ def _do_validate_(self, context: ValidationContext) -> bool: ) return all_passed + @staticmethod + def __handle_check_error__(check, context, error: Exception) -> None: + if isinstance(error, (FileNotFoundError, ROCrateMetadataNotFoundError)): + # A missing descriptor/metadata graph is an input problem reported by + # the dedicated descriptor checks, not an implementation failure. + logger.debug("Skipping check %s: validation input is unavailable: %s", check, error) + return + + if isinstance(error, ValidationExecutionError): + # An engine-level failure means validation did not complete. Do not + # turn it into a warning and accidentally return a clean result. + raise error.with_traceback(error.__traceback__) + + if context.maybe_warn_offline_cache_miss(error): + logger.debug("Offline cache miss during check %s: %s", check, error) + return + + check_path = str(check.requirement.path) if check.requirement.path else check.identifier + raise ValidationExecutionError( + message=f"Unexpected error while executing check '{check.identifier}': {type(error).__name__}: {error}", + path=check_path, + ) from error + @staticmethod def __record_skipped_check__( check, @@ -517,14 +533,7 @@ def __get_requirement_classes__() -> list[type[Requirement]]: # Ensure known requirement modules are imported so subclasses are registered. for requirement_type in ("python", "shacl"): module_name = f"rocrate_validator.requirements.{requirement_type}" - try: - importlib.import_module(module_name) - except Exception: - logger.debug( - "Unable to import requirement module: %s", - module_name, - exc_info=True, - ) + importlib.import_module(module_name) def all_subclasses( base_class: type[Requirement], diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 9974d482b..3d870963e 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -40,7 +40,7 @@ from rocrate_validator.models.result import ValidationResult from rocrate_validator.models.settings import ValidationSettings from rocrate_validator.models.severity import Severity -from rocrate_validator.models.skipped_check import SkipCategory +from rocrate_validator.models.skipped_check import SkipCategory, SkipCategoryInput from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import find_offline_cache_miss @@ -84,59 +84,47 @@ def detect_rocrate_profiles(self) -> list[Profile]: """ Detect the profiles to validate against """ - try: - # initialize the validation context - context = ValidationContext(self, self.validation_settings) - candidate_profiles_uris: set[str] = set() - try: - candidate_profiles_uris.update(context.ro_crate.metadata.get_conforms_to() or []) - except Exception as e: - logger.debug("Error while getting candidate profiles URIs: %s", e) - try: - candidate_profiles_uris.update(context.ro_crate.metadata.get_root_data_entity_conforms_to() or []) - except Exception as e: - logger.debug("Error while getting candidate profiles URIs: %s", e) - - logger.debug("Candidate profiles: %s", candidate_profiles_uris) - if not candidate_profiles_uris: - logger.debug("Unable to determine the profile to validate against") - return [] - # load the profiles - profiles = [] - candidate_profiles = [] - available_profiles = Profile.load_profiles( - context.profiles_path, - extra_profiles_path=context.extra_profiles_path, - publicID=context.publicID, - severity=context.requirement_severity, - ) - profiles = [p for p in available_profiles if p.uri in candidate_profiles_uris] - # get the candidate profiles - for profile in profiles: - candidate_profiles.append(profile) - inherited_profiles = profile.inherited_profiles - for inherited_profile in inherited_profiles: - if inherited_profile in candidate_profiles: - candidate_profiles.remove(inherited_profile) - logger.debug( - "%d Candidate Profiles found: %s", - len(candidate_profiles), - candidate_profiles, - ) - # unmatched candidate profiles - unmatched_profiles = candidate_profiles_uris.difference({p.uri for p in profiles}) - logger.debug("Unmatched Candidate Profiles URIs: %s", unmatched_profiles) - if len(unmatched_profiles) > 0: - logger.warning( - "The conformance to the following profiles could not be verified: %s", - ", ".join(unmatched_profiles), - ) - return candidate_profiles + # initialize the validation context + context = ValidationContext(self, self.validation_settings) + candidate_profiles_uris: set[str] = set() + candidate_profiles_uris.update(context.ro_crate.metadata.get_conforms_to() or []) + candidate_profiles_uris.update(context.ro_crate.metadata.get_root_data_entity_conforms_to() or []) - except Exception: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Error detecting RO-Crate profiles") + logger.debug("Candidate profiles: %s", candidate_profiles_uris) + if not candidate_profiles_uris: + logger.debug("Unable to determine the profile to validate against") return [] + # load the profiles + profiles = [] + candidate_profiles = [] + available_profiles = Profile.load_profiles( + context.profiles_path, + extra_profiles_path=context.extra_profiles_path, + publicID=context.publicID, + severity=context.requirement_severity, + ) + profiles = [p for p in available_profiles if p.uri in candidate_profiles_uris] + # get the candidate profiles + for profile in profiles: + candidate_profiles.append(profile) + inherited_profiles = profile.inherited_profiles + for inherited_profile in inherited_profiles: + if inherited_profile in candidate_profiles: + candidate_profiles.remove(inherited_profile) + logger.debug( + "%d Candidate Profiles found: %s", + len(candidate_profiles), + candidate_profiles, + ) + # unmatched candidate profiles + unmatched_profiles = candidate_profiles_uris.difference({p.uri for p in profiles}) + logger.debug("Unmatched Candidate Profiles URIs: %s", unmatched_profiles) + if len(unmatched_profiles) > 0: + logger.warning( + "The conformance to the following profiles could not be verified: %s", + ", ".join(unmatched_profiles), + ) + return candidate_profiles def validate(self) -> ValidationResult: """ @@ -385,7 +373,7 @@ def record_skip( self, check: RequirementCheck, message: str, - category: SkipCategory = SkipCategory.RETURNED, + category: SkipCategoryInput = SkipCategory.RETURNED, ) -> None: """Record a structured reason while a check is returning ``SKIPPED``.""" self.result.record_skip(check, message, category) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index fe7734225..fff3239ae 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -128,11 +128,7 @@ def deactivated(self) -> bool: owning_profile = self.requirement.profile for profile in Profile.get_descendants(owning_profile): - try: - registry = ShapesRegistry.get_instance(profile) - except Exception as e: - logger.debug("Unable to get shapes registry for profile %s: %s", profile.identifier, e) - continue + registry = ShapesRegistry.get_instance(profile) if registry.is_node_deactivated(shape.node): return True return False diff --git a/rocrate_validator/requirements/shacl/errors.py b/rocrate_validator/requirements/shacl/errors.py index ffccca793..fa1b14e8d 100644 --- a/rocrate_validator/requirements/shacl/errors.py +++ b/rocrate_validator/requirements/shacl/errors.py @@ -13,11 +13,17 @@ # limitations under the License. -from rocrate_validator.errors import ValidationError -from rocrate_validator.requirements.shacl.validator import SHACLValidationResult +from __future__ import annotations +from typing import TYPE_CHECKING -class SHACLValidationError(ValidationError): +from rocrate_validator.errors import ValidationExecutionError + +if TYPE_CHECKING: + from rocrate_validator.requirements.shacl.validator import SHACLValidationResult + + +class SHACLValidationError(ValidationExecutionError): def __init__( self, result: SHACLValidationResult | None = None, diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 72e0d8cb1..68f80601c 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -18,7 +18,7 @@ from rdflib import RDF from rocrate_validator.constants import VALIDATOR_NS -from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.errors import ROCrateMetadataNotFoundError, ValidationExecutionError from rocrate_validator.models import ( Profile, Requirement, @@ -136,16 +136,20 @@ def finalize(cls, context: ValidationContext) -> None: # Make sure the target's shapes (if any) are in the merged registry # and switch the current profile so violations are attributed under # the target profile in the report. - shacl_context.__set_current_validation_profile__(target) - shacl_context._current_validation_profile = target try: + shacl_context.__set_current_validation_profile__(target) + shacl_context._current_validation_profile = target runner.__do_execute_check__(shacl_context) - except ROCrateMetadataNotFoundError as e: + except (FileNotFoundError, ROCrateMetadataNotFoundError) as e: logger.debug( "Forced SHACL run for zero-shape target profile %s skipped: metadata descriptor is not available (%s)", target.identifier, e, ) + except ValidationExecutionError: + # A failed engine run is not a validation result and must reach API/CLI + # callers instead of being reduced to a warning. + raise except Exception as e: if context.maybe_warn_offline_cache_miss(e): logger.debug( @@ -154,9 +158,13 @@ def finalize(cls, context: ValidationContext) -> None: e, ) else: - logger.warning("Forced SHACL run for zero-shape target profile %s failed: %s", target.identifier, e) - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Forced SHACL run for zero-shape target profile failed") + raise ValidationExecutionError( + message=( + f"Unexpected error while finalizing SHACL validation for profile " + f"'{target.identifier}': {type(e).__name__}: {e}" + ), + path=str(target.path), + ) from e finally: shacl_context.__unset_current_validation_profile__() diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index ad2a0b8fc..2eff9df10 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -295,7 +295,22 @@ def load_shapes_from_file(file_path: str, publicID: str | None = None) -> Shapes # Extract the shapes from the graph return load_shapes_from_graph(g) except Exception as e: - raise BadSyntaxError(str(e), file_path) from e + line = getattr(e, "lines", None) + line = line + 1 if isinstance(line, int) else None + character = _get_syntax_error_character(e) + raise BadSyntaxError(str(e), file_path, line=line, character=character) from e + + +def _get_syntax_error_character(error: Exception) -> int | None: + """Return the one-based character offset from an RDFLib parser error.""" + index = getattr(error, "_i", None) + source = getattr(error, "_str", None) + if not isinstance(index, int) or not isinstance(source, bytes): + return None + + source_text = source.decode("utf-8", errors="replace") + line_start = source_text.rfind("\n", 0, index) + 1 + return index - line_start + 1 def load_shapes_from_graph(g: Graph) -> ShapesList: diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 241a9c22a..8367b494e 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -40,6 +40,7 @@ ValidationContext, ValidationResult, ) +from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging @@ -205,12 +206,8 @@ def __get_data_graph_base__(self) -> str | None: :return: The @base value if found, None otherwise """ - try: - metadata_dict = self.ro_crate.metadata.as_dict() - return extract_base_from_jsonld(metadata_dict) - except Exception as e: - logger.debug("Unable to extract @base from data graph metadata: %s", e) - return None + metadata_dict = self.ro_crate.metadata.as_dict() + return extract_base_from_jsonld(metadata_dict) def __load_ontology_graph__( self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE @@ -474,22 +471,28 @@ def validate( _inject_default_prefixes(self._shapes_graph) # validate the data graph using pyshacl.validate - conforms, results_graph, results_text = pyshacl.validate( - data_graph, - shacl_graph=self.shapes_graph, - ont_graph=self.ont_graph, - inference=inference or ("owlrl" if self.ont_graph else None), - inplace=inplace, - abort_on_first=abort_on_first, - allow_infos=allow_infos, - allow_warnings=allow_warnings, - meta_shacl=meta_shacl, - iterate_rules=iterate_rules, - advanced=advanced, - js=False, - debug=False, - **kwargs, - ) + try: + conforms, results_graph, results_text = pyshacl.validate( + data_graph, + shacl_graph=self.shapes_graph, + ont_graph=self.ont_graph, + inference=inference or ("owlrl" if self.ont_graph else None), + inplace=inplace, + abort_on_first=abort_on_first, + allow_infos=allow_infos, + allow_warnings=allow_warnings, + meta_shacl=meta_shacl, + iterate_rules=iterate_rules, + advanced=advanced, + js=False, + debug=False, + **kwargs, + ) + except Exception as e: + version = getattr(pyshacl, "__version__", "unknown") + raise SHACLValidationError( + message=(f"SHACL validation could not be executed by pySHACL {version}: {type(e).__name__}: {e}") + ) from e # log the validation results logger.debug("pyshacl.validate result: Conforms: %r", conforms) logger.debug("pyshacl.validate result: Results Graph: %r", results_graph) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 114a37abf..bd847f642 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -17,12 +17,13 @@ from typing import Any import pytest -from rdflib import Literal, Namespace +from rdflib import Graph, Literal, Namespace from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER, SHACL_NS from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError from rocrate_validator.models import URI, Profile, ValidationContext, ValidationSettings, Validator from rocrate_validator.requirements.shacl.checks import SHACLCheck +from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry from tests.ro_crates import InvalidFileDescriptorEntity, ValidROC @@ -75,6 +76,26 @@ def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): logger.debug("The profiles: %r", profiles) +def test_profile_detection_error_is_not_silenced(monkeypatch): + """Unexpected metadata errors must not become an empty profile selection.""" + + class BrokenMetadata: + def get_conforms_to(self): + raise RuntimeError("metadata profile detection failed") + + class BrokenROCrate: + metadata = BrokenMetadata() + + settings = ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + profile_identifier=DEFAULT_PROFILE_IDENTIFIER, + ) + monkeypatch.setattr(ValidationContext, "ro_crate", property(lambda self: BrokenROCrate())) + + with pytest.raises(RuntimeError, match="profile detection failed"): + Validator(settings).detect_rocrate_profiles() + + def test_load_valid_profile_without_inheritance_from_validation_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" settings_dict: dict[str, Any] = { @@ -297,7 +318,7 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat assert len(requirements_checks) == 3, "The number of requirements should be 2" -def test_zero_shape_target_profile_triggers_pyshacl_run(fake_profiles_path: str): +def test_zero_shape_target_profile_triggers_pyshacl_run(monkeypatch, fake_profiles_path: str): """Regression test for the 0-shape profile bug: when the target profile has no SHACL checks of its own, Validator must still drive a single pyshacl run @@ -305,6 +326,7 @@ def test_zero_shape_target_profile_triggers_pyshacl_run(fake_profiles_path: str) Without the fix in `Validator.__ensure_target_shacl_run__`, no SHACLCheck would be recorded as executed for the wrapper target.""" + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) settings = ValidationSettings( profiles_path=Path(fake_profiles_path), profile_identifier="c-wrapper", @@ -323,6 +345,52 @@ def test_zero_shape_target_profile_triggers_pyshacl_run(fake_profiles_path: str) ) +def test_pyshacl_engine_failure_is_not_silenced(monkeypatch, fake_profiles_path: str): + """A pySHACL crash must abort validation instead of producing a clean result.""" + + def fail_validation(*args, **kwargs): + raise ImportError("cannot import name 'ConjunctiveLike' from 'pyshacl.consts'") + + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + monkeypatch.setattr("rocrate_validator.requirements.shacl.validator.pyshacl.validate", fail_validation) + settings = ValidationSettings( + profiles_path=Path(fake_profiles_path), + profile_identifier="c-deactivated", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + disable_check_for_duplicates=True, + ) + + with pytest.raises(SHACLValidationError, match="ConjunctiveLike") as exc_info: + Validator(settings).validate() + + assert isinstance(exc_info.value.__cause__, ImportError) + + +def test_pyshacl_engine_failure_in_zero_shape_finalizer_is_not_silenced(monkeypatch, fake_profiles_path: str): + """The forced SHACL run for an inheritance-only target must also fail closed.""" + + def fail_validation(*args, **kwargs): + raise ImportError("cannot import name 'ConjunctiveLike' from 'pyshacl.consts'") + + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + monkeypatch.setattr("rocrate_validator.requirements.shacl.validator.pyshacl.validate", fail_validation) + settings = ValidationSettings( + profiles_path=Path(fake_profiles_path), + profile_identifier="c-wrapper", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + disable_check_for_duplicates=True, + ) + + with pytest.raises(SHACLValidationError, match="ConjunctiveLike") as exc_info: + Validator(settings).validate() + + assert isinstance(exc_info.value.__cause__, ImportError) + + def test_profile_parents(check_overriding_profiles_path: str): """Test the order of the loaded profiles.""" logger.debug("The profiles path: %r", check_overriding_profiles_path) diff --git a/tests/unit/requirements/test_shacl_utils.py b/tests/unit/requirements/test_shacl_utils.py index 63b51dde2..eb6c54b54 100644 --- a/tests/unit/requirements/test_shacl_utils.py +++ b/tests/unit/requirements/test_shacl_utils.py @@ -27,7 +27,8 @@ from rdflib.collection import Collection from rocrate_validator.constants import SHACL_NS -from rocrate_validator.requirements.shacl.utils import load_shapes_from_graph +from rocrate_validator.errors import BadSyntaxError +from rocrate_validator.requirements.shacl.utils import load_shapes_from_file, load_shapes_from_graph SH = Namespace(SHACL_NS) EX = Namespace("http://example.org/") @@ -197,3 +198,17 @@ def test_unknown_shape_node_raises(): with pytest.raises(KeyError): shapes_list.get_shape_property_graph(EX.UnknownShape, prop_a) + + +def test_load_shapes_from_file_reports_parser_location(tmp_path): + """Parser errors preserve the line and character of the invalid Turtle.""" + shapes_path = tmp_path / "broken.ttl" + shapes_path.write_text("@prefix ex: .\n xxxx\n", encoding="utf-8") + + with pytest.raises(BadSyntaxError) as exc_info: + load_shapes_from_file(str(shapes_path)) + + error = exc_info.value + assert error.path == str(shapes_path) + assert error.line == 2 + assert error.character == 5 diff --git a/tests/unit/test_remote_context_retrieval.py b/tests/unit/test_remote_context_retrieval.py index 67f9fdf97..0deb4a119 100644 --- a/tests/unit/test_remote_context_retrieval.py +++ b/tests/unit/test_remote_context_retrieval.py @@ -18,6 +18,8 @@ import pytest +from rocrate_validator.utils.http import OfflineCacheMissError + @pytest.fixture(scope="module") def fd_format(): @@ -297,6 +299,16 @@ def test_check_remote_context_invalid(self, fd_format): finally: fd_format.HttpRequester = original_requester + def test_check_remote_context_propagates_offline_cache_miss(self, fd_format): + """Offline cache misses must reach the validation runner unchanged.""" + checker = object.__new__(fd_format.FileDescriptorJsonLdFormat) + checker.__get_remote_context__ = MagicMock( + side_effect=OfflineCacheMissError("https://example.com/context.json") + ) + + with pytest.raises(OfflineCacheMissError): + checker.__check_remote_context__("https://example.com/context.json") + class TestGetContextKeys: def test_get_context_keys_from_string(self, fd_format): diff --git a/tests/unit/test_requirement_lifecycle.py b/tests/unit/test_requirement_lifecycle.py index aff09a66b..4383163e4 100644 --- a/tests/unit/test_requirement_lifecycle.py +++ b/tests/unit/test_requirement_lifecycle.py @@ -15,7 +15,15 @@ import pytest from rocrate_validator import services -from rocrate_validator.models import URI, RequirementLoader, Severity, ValidationContext, ValidationSettings +from rocrate_validator.errors import ValidationExecutionError +from rocrate_validator.models import ( + URI, + Requirement, + RequirementLoader, + Severity, + ValidationContext, + ValidationSettings, +) from tests.ro_crates import InvalidRootDataEntity @@ -138,3 +146,40 @@ def test_lifecycle_hooks_invoked_exactly_once_per_validation_run(lifecycle_spies f"{spy.__name__} should receive exactly one initialize+finalize " f"pair per validation run (got {events} across {runs} runs)" ) + + +def test_unexpected_check_error_is_wrapped_and_propagated(monkeypatch): + """Unexpected check failures must abort validation with their original cause.""" + + class ConcreteRequirement(Requirement): + @property + def hidden(self) -> bool: + return False + + def __init_checks__(self): + return [] + + requirement = object.__new__(ConcreteRequirement) + requirement._name = "Test requirement" + requirement._checks = [] + requirement._path = None + check = type("Check", (), {})() + check.identifier = "test-check" + check.depends_on = [] + check.requirement = requirement + requirement._checks = [check] + + context = type("Context", (), {})() + context.settings = type("Settings", (), {"skip_checks": []})() + context.aborted = False + context.maybe_warn_offline_cache_miss = lambda exc: False + + def fail_check(*args): + raise RuntimeError("unexpected check failure") + + monkeypatch.setattr(Requirement, "__execute_check__", fail_check) + + with pytest.raises(ValidationExecutionError, match="test-check") as exc_info: + requirement._do_validate_(context) + + assert isinstance(exc_info.value.__cause__, RuntimeError) From d78816794847cbd61df6dabe02dd897c6ff028a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 09:54:45 +0200 Subject: [PATCH 056/271] feat(profiles): :memo: report structured skipped checks Record explicit skip reasons and categories across RO-Crate and Workflow profiles, including exception and configuration paths. --- rocrate_validator/models/result.py | 12 +-- rocrate_validator/models/skipped_check.py | 19 ++++- .../1.1/must/0_file_descriptor_format.py | 54 ++++++++++--- .../1.1/must/4_data_entity_metadata.py | 4 +- .../should/2_root_data_entity_relative_uri.py | 7 +- .../1.1/should/4_data_entity_existence.py | 4 +- .../1.1/should/5_web_data_entity_metadata.py | 4 +- .../1.2/must/0_file_descriptor_format.py | 75 +++++++++++++++---- .../ro-crate/1.2/must/1_ro_crate_preview.py | 4 +- .../1.2/must/2_root_data_entity_cite_as.py | 4 +- .../1.2/must/2_root_data_entity_identifier.py | 4 +- .../1.2/must/2_root_identifier_property.py | 3 +- .../1.2/must/4_data_entity_metadata.py | 22 +++++- .../1.2/should/0_entity_identifier_format.py | 9 ++- .../1.2/should/1_file_descriptor_name.py | 1 + .../should/2_root_data_entity_identifier.py | 15 +++- .../1.2/should/4_data_entity_license.py | 10 ++- .../1.2/should/4_data_entity_metadata.py | 13 +++- .../1.2/should/4_missing_file_local_path.py | 5 +- .../1.2/should/4_referenced_rocrate.py | 9 ++- .../1.2/should/4_web_dataset_distribution.py | 4 +- .../workflow-ro-crate/may/1_main_workflow.py | 9 ++- .../workflow-ro-crate/must/0_main_workflow.py | 1 + tests/unit/test_skipped_check_details.py | 10 +++ 24 files changed, 241 insertions(+), 61 deletions(-) diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 73058390d..71c83ede2 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -32,7 +32,7 @@ RequirementLevel, Severity, ) -from rocrate_validator.models.skipped_check import SkipCategory, SkippedCheckDetail +from rocrate_validator.models.skipped_check import SkipCategory, SkipCategoryInput, SkippedCheckDetail if TYPE_CHECKING: from collections.abc import Collection @@ -248,14 +248,14 @@ def _record_check_result( check: RequirementCheck, result: CheckResultValue, skip_message: str | None = None, - skip_category: SkipCategory = SkipCategory.RETURNED, + skip_category: SkipCategoryInput = SkipCategory.RETURNED, ) -> CheckResult: """Record a normalized result and keep check collections consistent.""" normalized_result = normalize_check_result(result) self._check_results[check.identifier] = normalized_result if normalized_result is CheckResult.SKIPPED: - skip_category = SkipCategory(skip_category) + normalized_skip_category = SkipCategory(skip_category) self._executed_checks.discard(check) self._skipped_checks.add(check) self._executed_checks_results.pop(check.identifier, None) @@ -264,7 +264,7 @@ def _record_check_result( self._skipped_check_details[check.identifier] = SkippedCheckDetail( check=check, message=skip_message or "Check returned SKIPPED", - category=skip_category, + category=normalized_skip_category, ) return normalized_result @@ -315,7 +315,7 @@ def record_skip( self, check: RequirementCheck, message: str, - category: SkipCategory = SkipCategory.RETURNED, + category: SkipCategoryInput = SkipCategory.RETURNED, ) -> None: """Record a skip message while preserving the normalized skipped result.""" self._record_check_result(check, CheckResult.SKIPPED, message, category) @@ -324,7 +324,7 @@ def _add_skipped_check( self, check: RequirementCheck, message: str = "Check was skipped", - category: SkipCategory = SkipCategory.RETURNED, + category: SkipCategoryInput = SkipCategory.RETURNED, ): """ Internal method to add a check to the skipped checks diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py index fe56104ef..f059b902e 100644 --- a/rocrate_validator/models/skipped_check.py +++ b/rocrate_validator/models/skipped_check.py @@ -16,7 +16,7 @@ from dataclasses import dataclass from enum import Enum, unique -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, TypeAlias if TYPE_CHECKING: from rocrate_validator.models.requirement import RequirementCheck @@ -28,6 +28,7 @@ class SkipCategory(str, Enum): RETURNED = ("returned", "The check returned SKIPPED without another skip reason.") CONFIGURED = ("configured", "The check was skipped by validation settings.") + EXCEPTION = ("exception", "The check was skipped because an exception prevented validation.") DEACTIVATED = ("deactivated", "The check is deactivated in the profile.") DEPENDENCY = ("dependency", "The check was skipped because a dependency did not pass.") NOT_REACHED = ("not_reached", "The check was not reached because validation stopped earlier.") @@ -45,6 +46,20 @@ def description(self) -> str: return self._description +SkipCategoryInput: TypeAlias = ( + SkipCategory + | Literal[ + "returned", + "configured", + "exception", + "deactivated", + "dependency", + "not_reached", + "inherited", + ] +) + + class SkipRequirementCheck(Exception): """Signal that a requirement check should be skipped.""" @@ -52,7 +67,7 @@ def __init__( self, check: RequirementCheck, message: str = "", - category: SkipCategory = SkipCategory.RETURNED, + category: SkipCategoryInput = SkipCategory.RETURNED, ): self.check = check self.message = message diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index 3c6a01e27..6d68bdf41 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -24,11 +24,14 @@ from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError # set up logging logger = logging.getLogger(__name__) +_EXPECTED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_EXPECTED_REMOTE_CONTEXT_ERRORS = (*_EXPECTED_METADATA_ERRORS, OSError, RuntimeError) + @requirement(name="File Descriptor existence") class FileDescriptorExistence(PyFunctionCheck): @@ -41,6 +44,7 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' @@ -55,6 +59,7 @@ def test_size(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) @@ -88,8 +93,12 @@ def check(self, context: ValidationContext) -> CheckResultValue: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -187,7 +196,9 @@ def __check_remote_context__(self, context_uri: str) -> bool: a valid JSON-LD context: it is not a dictionary" ) return True - except Exception: + except OfflineCacheMissError: + raise + except _EXPECTED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating JSON-LD context is a dictionary") return False @@ -233,8 +244,12 @@ def check_context(self, context: ValidationContext) -> CheckResultValue: return self.__check_contexts__(context, json_dict["@context"]) except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error extracting @context from file descriptor") return False @@ -332,8 +347,12 @@ def _check_flattened_graph(self, context, is_flat): return result except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error flattening JSON-LD file descriptor") return False @@ -354,12 +373,17 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: "file descriptor does not contain the @id attribute", self, ) + context.abort_validation("file descriptor entity does not contain the @id attribute") return False return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @id property of file descriptor entities") return False @@ -384,8 +408,12 @@ def check_types(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @type property of file descriptor entities") return False @@ -466,7 +494,7 @@ def __record_unexpected_key__(self, k: str, context_keys: set, unexpected_keys: name="Validation of the compaction format of the file descriptor", depends_on=("File Descriptor JSON format",), ) - def check_compaction(self, context: ValidationContext) -> CheckResultValue: + def check_compaction(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -479,7 +507,9 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: try: context_keys = self.__get_context_keys__(jsonld_context) logger.debug(f"{context_keys}") - except Exception as e: + except OfflineCacheMissError: + raise + except _EXPECTED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting context keys from JSON-LD") context.result.add_issue(str(e), self) @@ -512,8 +542,12 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception as e: + except _EXPECTED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py index aaa8f7a4a..4a943a7f9 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py @@ -37,6 +37,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED # Perform the check result = True @@ -47,6 +48,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -76,7 +78,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self ) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue( f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self ) diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py index a8d667f6a..fde5a7fb3 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py @@ -36,10 +36,11 @@ def check_relative_uris(self, context: ValidationContext) -> CheckResultValue: if context.ro_crate.metadata.get_root_data_entity().id != "./": context.result.add_issue("Root Data Entity URI is not denoted by the string `./`", self) return False - return CheckResult.SKIPPED + return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity URI check: metadata descriptor is not available") - return True - except Exception as e: + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity URI: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py index b5fd63ab0..07df96062 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py @@ -39,6 +39,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED # Perform the check result = True @@ -50,6 +51,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: ] except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -57,7 +59,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: if not entity.is_available(): context.result.add_issue(f"Data Entity {entity.id} is not available", self) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity {entity.id} is not available: {e}", self) result = False if not result and context.fail_fast: diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py index ab94a2752..7df84b2c5 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py @@ -47,6 +47,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -73,7 +74,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: else: context.result.add_issue(f"Web-based Data Entity {entity.id} is not available", self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity {entity.id} is not available: {e}", self) result = False if not result and context.fail_fast: @@ -91,6 +92,7 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index fb611964d..4c5537b8f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -22,11 +22,14 @@ from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError # set up logging logger = logging.getLogger(__name__) +_EXPECTED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_EXPECTED_REMOTE_CONTEXT_ERRORS = (*_EXPECTED_METADATA_ERRORS, OSError, RuntimeError) + # Property keys whose string values are literals (e.g. names, dates, URLs) rather # than references to other entities, so a matching @id must not be flagged. _LITERAL_KEYS = frozenset( @@ -65,6 +68,7 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' @@ -79,6 +83,7 @@ def test_size(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) @@ -99,6 +104,7 @@ def check(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor encoding check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED try: raw_data = context.ro_crate.metadata.as_bytes() @@ -108,11 +114,13 @@ def check(self, context: ValidationContext) -> CheckResultValue: return True except UnicodeDecodeError: context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) + context.abort_validation("file descriptor is not UTF-8 encoded") if logger.isEnabledFor(logging.DEBUG): logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED @@ -142,8 +150,12 @@ def check(self, context: ValidationContext) -> CheckResultValue: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -226,7 +238,9 @@ def __check_remote_context__(self, context_uri: str) -> bool: jsonLD = self.__get_remote_context__(context_uri) assert isinstance(jsonLD, dict) return True - except Exception: + except OfflineCacheMissError: + raise + except _EXPECTED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -290,8 +304,12 @@ def has_expected_context(ctx: object) -> bool: return self.__check_contexts__(context, json_dict["@context"]) except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -395,8 +413,12 @@ def check_flattened(self, context: ValidationContext) -> CheckResultValue: return result except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -417,12 +439,17 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: "file descriptor does not contain the @id attribute", self, ) + context.abort_validation("file descriptor entity does not contain the @id attribute") return False return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -447,8 +474,12 @@ def check_types(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -470,8 +501,12 @@ def check_unique_identifiers(self, context: ValidationContext) -> CheckResultVal return True except ROCrateMetadataNotFoundError: logger.debug("Skipping unique identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -522,8 +557,12 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu return True except ROCrateMetadataNotFoundError: logger.debug("Skipping entity reference check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -547,8 +586,12 @@ def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue return True except ROCrateMetadataNotFoundError: logger.debug("Skipping subject and keyword check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _EXPECTED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -624,7 +667,7 @@ def add_unexpected_key(k: str, u_keys: dict) -> None: name="Validation of the compaction format of the file descriptor", depends_on=("File Descriptor JSON format",), ) - def check_compaction(self, context: ValidationContext) -> CheckResultValue: + def check_compaction(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -637,7 +680,9 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: try: context_keys = self.__get_context_keys__(jsonld_context) logger.debug(f"{context_keys}") - except Exception as e: + except OfflineCacheMissError: + raise + except _EXPECTED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(str(e), self) @@ -670,8 +715,12 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception as e: + except _EXPECTED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index baa7274d6..093595da7 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -33,9 +33,11 @@ class ROCrateWebsiteChecker(PyFunctionCheck): def check_preview_html(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED except ROCrateMetadataNotFoundError: logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): @@ -46,6 +48,6 @@ def check_preview_html(self, context: ValidationContext) -> CheckResultValue: return True context.result.add_issue("ro-crate-preview.html should include an HTML5 doctype", self) return False - except Exception as e: + except (OSError, UnicodeDecodeError) as e: context.result.add_issue(f"Unable to read ro-crate-preview.html: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py index 13db5c100..31eba0cfc 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_cite_as.py @@ -57,6 +57,7 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultV or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not applicable", "configured") return CheckResult.SKIPPED try: @@ -84,7 +85,8 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultV except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py index 354b9ee11..50f32b2e2 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py @@ -32,6 +32,7 @@ class RootDataEntityIdentifierChecker(PyFunctionCheck): def check_identifier(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if root_entity.id == "./": @@ -45,7 +46,8 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py index d89f405eb..501ac7eb2 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py @@ -46,7 +46,8 @@ def check_identifier_values(self, context: ValidationContext) -> CheckResultValu return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 8fea3eeab..3453324be 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -40,13 +40,16 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if is_detached: logger.debug("Skipping data entity payload checks for detached RO-Crate") + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED # Perform the check result = True @@ -54,6 +57,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -84,7 +88,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self ) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue( f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self ) @@ -109,17 +113,20 @@ def check_detached_entities(self, context: ValidationContext) -> CheckResultValu is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if not is_detached: + context.record_skip(self, "RO-Crate is attached", "returned") return CheckResult.SKIPPED result = True root_entity_id = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: @@ -149,7 +156,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # root_entity_id = None root_entity_is_local = False root_entity_absolute_path = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_data_entity = context.ro_crate.metadata.get_root_data_entity() root_entity_id = root_data_entity.id root_entity_is_local = ( @@ -160,6 +167,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: @@ -202,14 +210,17 @@ def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if is_detached: + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if entity.has_local_identifier() or entity.is_remote(): @@ -239,6 +250,7 @@ def check_citation(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") @@ -295,12 +307,14 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not requested", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -313,7 +327,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) result = False if not result and context.fail_fast: diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py index 7fc0f14e1..c227d9684 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py @@ -57,6 +57,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> CheckResultVa entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping parent traversal check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") @@ -85,6 +86,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> CheckResultValue entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") @@ -114,17 +116,18 @@ def check_named_entity_id_format(self, context: ValidationContext) -> CheckResul result = True ro_crate_metadata = context.ro_crate.metadata non_contextual_ids = set() - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.add(ro_crate_metadata.get_file_descriptor_entity().id) - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.add(ro_crate_metadata.get_root_data_entity().id) - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) try: entities = ro_crate_metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping named entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py index d4ebc2b8c..4261a1ed2 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py @@ -37,6 +37,7 @@ def test_detached_descriptor_filename(self, context: ValidationContext) -> Check """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py index fae9fae52..45fef61cf 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py @@ -35,9 +35,11 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: # n """ try: if not context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is attached", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): + context.record_skip(self, "root data entity is not remote", "returned") return CheckResult.SKIPPED if root_entity.id == "./": @@ -54,8 +56,9 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: # n return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False @@ -75,9 +78,11 @@ def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValu """ try: if not context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is attached", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): + context.record_skip(self, "root data entity is not remote", "returned") return CheckResult.SKIPPED # Check if the `cite-as` property is present and references the Root Data Entity @@ -102,8 +107,9 @@ def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValu return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) return False @@ -149,8 +155,10 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) def check_identifier_resolvable(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 if context.settings.skip_availability_check: + context.record_skip(self, "availability checks are disabled", "configured") return CheckResult.SKIPPED if context.settings.metadata_only: + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -178,7 +186,8 @@ def check_identifier_resolvable(self, context: ValidationContext) -> CheckResult return result except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py index 6e3bd9ee1..050b16650 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py @@ -101,11 +101,16 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 + def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901, PLR0911 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except ValueError: + context.record_skip(self, "root data entity is not available", "exception") return CheckResult.SKIPPED if root_entity is None: return True @@ -122,6 +127,7 @@ def check_license_divergence(self, context: ValidationContext) -> CheckResultVal entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if entity.id == root_entity.id: diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py index 46fb8320b..893fef699 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py @@ -37,6 +37,7 @@ def check_citation(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") @@ -94,12 +95,14 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV or context.settings.enforce_availability or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not applicable", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -110,7 +113,7 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) result = False if not result and context.fail_fast: @@ -120,12 +123,14 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) def check_content_size(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: + context.record_skip(self, "availability checks are disabled", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -140,7 +145,7 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: # content_value = str(content_size) try: content_int = int(str(content_value)) - except Exception: + except (TypeError, ValueError, OverflowError): content_int = None external_size = context.ro_crate.get_external_file_size(entity.id) if external_size is not None and content_int is not None and content_int != external_size: @@ -161,12 +166,14 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: # @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) def check_content_url(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: + context.record_skip(self, "availability checks are disabled", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: content_url = entity.get_property("contentUrl") @@ -186,7 +193,7 @@ def check_content_url(self, context: ValidationContext) -> CheckResultValue: # msg += f": {dl.reason}" context.result.add_issue(msg, self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue( f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}' " f"availability check failed: {e}", diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py index ad45cafff..aaa88c00c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py @@ -105,17 +105,20 @@ def check_missing_file_local_path(self, context: ValidationContext) -> CheckResu is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if is_detached or context.settings.metadata_only: + context.record_skip(self, "RO-Crate is detached or metadata-only mode is active", "configured") return CheckResult.SKIPPED root_entity_id = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id result = True try: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py index 382ead777..2bbf7f492 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py @@ -74,18 +74,25 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> Check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not applicable", "configured") return CheckResult.SKIPPED result = True try: root = context.ro_crate.metadata.get_root_data_entity() - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except ValueError: + context.record_skip(self, "root data entity is not available", "exception") return CheckResult.SKIPPED try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py index 124c343e5..58013d0a8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py @@ -72,7 +72,7 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo msg += f": {dl.reason}" context.result.add_issue(msg, self) return False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue( f"Error checking downloadability of distribution '{url}' for Dataset '{entity_id}': {e}", self ) @@ -82,12 +82,14 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) def check_distribution_downloadable(self, context: ValidationContext) -> CheckResultValue: if context.settings.skip_availability_check or context.settings.metadata_only: + context.record_skip(self, "availability checks are disabled or metadata-only mode is active", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: distribution_raw = entity.get_property("distribution") diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py index d05612160..6766d188f 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py @@ -32,6 +32,7 @@ def check_workflow_diagram(self, context: ValidationContext) -> CheckResultValue """Check if the crate contains the workflow diagram.""" if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED try: @@ -47,8 +48,10 @@ def check_workflow_diagram(self, context: ValidationContext) -> CheckResultValue return True except ROCrateMetadataNotFoundError: logger.debug("Skipping workflow diagram check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow diagram: {e}", self) if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking main workflow image existence") return False @@ -71,8 +74,10 @@ def check_workflow_description(self, context: ValidationContext) -> CheckResultV return True except ROCrateMetadataNotFoundError: logger.debug("Skipping workflow description check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow description: {e}", self) if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking workflow description existence") return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py index 9209a3e5f..86501fbb4 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py @@ -41,6 +41,7 @@ def check_workflow(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping main workflow check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED except (TypeError, ValueError) as error: if str(error) == "no main workflow in metadata file descriptor": diff --git a/tests/unit/test_skipped_check_details.py b/tests/unit/test_skipped_check_details.py index d08fa62e9..ae68b8584 100644 --- a/tests/unit/test_skipped_check_details.py +++ b/tests/unit/test_skipped_check_details.py @@ -68,10 +68,20 @@ def test_skipped_check_detail_is_removed_when_check_passes(): assert result.skipped_check_details == [] +def test_record_skip_normalizes_string_category_to_enum(): + result = _result() + check = _check() + + result.record_skip(check, "metadata descriptor is not available", "exception") + + assert result.skipped_check_details[0].category is SkipCategory.EXCEPTION + + def test_skip_category_has_stable_values_and_descriptions(): assert {category.value for category in SkipCategory} == { "returned", "configured", + "exception", "deactivated", "dependency", "not_reached", From 48bf296844d3868e52c161d300c7d7c11a4ea01f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 09:54:45 +0200 Subject: [PATCH 057/271] feat(cli): :warning: render validation errors safely Render parser and runtime validation failures with concise CLI diagnostics and optional debug tracebacks. --- rocrate_validator/cli/commands/errors.py | 76 ++++++++++++++++------ rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/cli/main.py | 10 +-- tests/test_cli.py | 70 ++++++++++++++++++++ 4 files changed, 131 insertions(+), 27 deletions(-) diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py index 7d3c9f50e..3cd45d1d6 100644 --- a/rocrate_validator/cli/commands/errors.py +++ b/rocrate_validator/cli/commands/errors.py @@ -13,18 +13,18 @@ # limitations under the License. import sys -import textwrap +import traceback from rich.console import Console +from rich.markup import escape +from rich.padding import Padding +from rich.panel import Panel +from rich.syntax import Syntax -from rocrate_validator.errors import InvalidProfilePath, ProfileNotFound, ProfilesDirectoryNotFound -from rocrate_validator.utils import log as logging +from rocrate_validator.errors import BadSyntaxError, InvalidProfilePath, ProfileNotFound, ProfilesDirectoryNotFound -# Create a logger for this module -logger = logging.getLogger(__name__) - -def handle_error(e: Exception, console: Console) -> None: +def handle_error(e: Exception, console: Console, *, debug: bool = False) -> None: error_message = "" if isinstance(e, ProfilesDirectoryNotFound): error_message = f""" @@ -45,19 +45,57 @@ def handle_error(e: Exception, console: Console) -> None: To see the available profiles, run: [cyan bold]rocrate-validator profiles list[/cyan bold] """ - else: - error_message = f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n" - if logger.isEnabledFor(logging.DEBUG): - console.print_exception() - console.print( - textwrap.indent( - "This error may be due to a bug.\n" - "Please report it to the issue tracker " - "along with the following stack trace:\n", - " " * 9, - ) + elif isinstance(e, BadSyntaxError): + location = _format_syntax_error_location(e) + error_message = ( + "The validation profile could not be parsed.\n" + f"Path: {escape(e.path)}\n" + f"Reason: {location}[red]{escape(_summarize_syntax_error(e.message))}[/red]" ) - console.print_exception() + else: + error_message = f"Unexpected error: {escape(str(e))}" console.print(f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") + if debug: + _print_debug_traceback(e, console) sys.exit(2) + + +def _summarize_syntax_error(message: str) -> str: + """Return the useful part of a parser error without its source dump.""" + lines = [line.strip() for line in message.splitlines() if line.strip()] + for line in lines: + if line.startswith("Bad syntax"): + return line.partition(" at ^ in:")[0] + return lines[0] if lines else "Unknown syntax error" + + +def _format_syntax_error_location(error: BadSyntaxError) -> str: + """Format the parser location when the underlying parser provided one.""" + location = [] + if error.line is not None: + location.append(f"line {error.line}") + if error.character is not None: + location.append(f"character {error.character}") + return f"[orange1]{escape(', '.join(location))}[/orange1] --> " if location else "" + + +def _print_debug_traceback(error: Exception, console: Console) -> None: + """Render a safe, readable traceback for exceptions from third-party parsers.""" + traceback_text = "".join(traceback.format_exception(type(error), error, error.__traceback__)).rstrip() + if console.no_color or not getattr(console, "interactive", True): + console.print(f"Debug traceback:\n{traceback_text}", markup=False) + return + + console.print( + Padding( + Panel( + Syntax(traceback_text, "pytb", theme="ansi_dark", line_numbers=False, word_wrap=True), + title="[bold yellow]Debug traceback[/bold yellow]", + title_align="left", + border_style="yellow", + padding=(1, 1, 1, 1), + ), + (1, 1, 0, 1), + ) + ) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 640164e40..c5f01c9af 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -438,7 +438,7 @@ def validate( # so we use sys.exit instead. sys.exit(0 if is_valid else 1) except Exception as e: - handle_error(e, console) + handle_error(e, console, debug=ctx.obj["debug"]) def _log_validation_inputs( diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index ce57f6277..41b82ac5a 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -16,6 +16,7 @@ import rich_click as click +from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.utils import running_in_jupyter from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.output.console import Console @@ -68,6 +69,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ ctx.obj["console"] = console ctx.obj["pager"] = SystemPager() ctx.obj["interactive"] = interactive + ctx.obj["debug"] = debug try: # If the version flag is set, print the version and exit @@ -85,13 +87,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ else: logger.debug("Command invoked: %s", ctx.invoked_subcommand) except Exception as e: - console.print(f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") - console.print("""This error may be due to a bug. - Please report it to the issue tracker - along with the following stack trace: - """) - console.print_exception() - sys.exit(2) + handle_error(e, console, debug=debug) if __name__ == "__main__": diff --git a/tests/test_cli.py b/tests/test_cli.py index 91971a321..0b030197a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -19,9 +19,12 @@ from click.testing import CliRunner from pytest import fixture +from rdflib import Graph from rocrate_validator import services from rocrate_validator.cli.main import cli +from rocrate_validator.errors import BadSyntaxError +from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.utils import log as logging @@ -63,6 +66,73 @@ def test_validate_subcmd_valid_local_folder_rocrate(cli_runner: CliRunner): assert re.search(r"RO-Crate.*is a valid", result.output) +def test_validate_subcmd_pyshacl_engine_failure_exits_with_error(cli_runner: CliRunner, monkeypatch): + """An internal pySHACL failure must never be reported as valid by the CLI.""" + + def fail_validation(*args, **kwargs): + raise ImportError("cannot import name 'ConjunctiveLike' from 'pyshacl.consts'") + + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + monkeypatch.setattr("rocrate_validator.requirements.shacl.validator.pyshacl.validate", fail_validation) + result = cli_runner.invoke( + cli, + ["validate", str(ValidROC().wrroc_paper_long_date), "--no-paging", "-p", "ro-crate"], + ) + + assert result.exit_code == 2 + assert "SHACL validation could not be executed" in result.output + assert "ConjunctiveLike" in result.output + assert "is a valid" not in result.output + + +def test_validate_subcmd_profile_syntax_error_is_concise_without_debug(cli_runner: CliRunner, monkeypatch): + """Parser failures must show a concise diagnostic without a traceback by default.""" + + def fail_validation(*args, **kwargs): + raise BadSyntaxError( + "at line 26 of <>:\nBad syntax (expected '.') at ^ in:\nsource dump", + "shapes.ttl", + line=26, + character=5, + ) + + monkeypatch.setattr("rocrate_validator.cli.commands.validate.services.validate", fail_validation) + result = cli_runner.invoke( + cli, + ["validate", str(ValidROC().wrroc_paper_long_date), "--no-paging", "-p", "ro-crate"], + ) + + assert result.exit_code == 2 + assert "The validation profile could not be parsed" in result.output + assert "line 26, character 5" in result.output + assert "line 26, character 5 -->" in result.output + assert "Bad syntax (expected '.')" in result.output + assert "Traceback" not in result.output + + +def test_validate_subcmd_profile_syntax_error_renders_traceback_in_debug(cli_runner: CliRunner, monkeypatch): + """Debug mode must render parser exception chains without a Rich renderer crash.""" + + def fail_validation(*args, **kwargs): + try: + Graph().parse(data="not valid turtle", format="turtle") + except Exception as cause: + raise BadSyntaxError(str(cause), "shapes.ttl") from cause + raise AssertionError("The RDF parser unexpectedly accepted invalid Turtle") + + monkeypatch.setattr("rocrate_validator.cli.commands.validate.services.validate", fail_validation) + result = cli_runner.invoke( + cli, + ["--debug", "validate", str(ValidROC().wrroc_paper_long_date), "--no-paging", "-p", "ro-crate"], + ) + + assert result.exit_code == 2 + assert "The validation profile could not be parsed" in result.output + assert "Debug traceback" in result.output + assert "Traceback (most recent call last)" in result.output + assert "TypeError: str or Text instance required" not in result.output + + def test_validate_subcmd_valid_remote_rocrate(cli_runner: CliRunner): result = cli_runner.invoke( cli, From 1a7632ccf8af7b399080ccc7123f9ad04cdfa770 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 10:40:39 +0200 Subject: [PATCH 058/271] refactor: :art: resolve linter warnings --- rocrate_validator/cli/commands/cache.py | 2 ++ rocrate_validator/cli/commands/profiles.py | 2 ++ rocrate_validator/cli/commands/validate.py | 11 ++++---- rocrate_validator/models/profile.py | 1 + rocrate_validator/models/skipped_check.py | 2 ++ rocrate_validator/models/statistics.py | 11 ++++---- rocrate_validator/models/validation.py | 1 + .../1.1/must/0_file_descriptor_format.py | 20 +++++++------- .../1.2/must/0_file_descriptor_format.py | 26 +++++++++---------- .../ro-crate/1.2/must/1_ro_crate_preview.py | 2 ++ .../1.2/must/2_root_data_entity_cite_as.py | 7 ++--- .../requirements/shacl/checks.py | 1 + .../requirements/shacl/validator.py | 1 + .../ro-crate-1.2/test_metadata_document.py | 2 +- 14 files changed, 52 insertions(+), 37 deletions(-) diff --git a/rocrate_validator/cli/commands/cache.py b/rocrate_validator/cli/commands/cache.py index 768add09b..c81624577 100644 --- a/rocrate_validator/cli/commands/cache.py +++ b/rocrate_validator/cli/commands/cache.py @@ -138,6 +138,7 @@ def cache_info(ctx, cache_path: Path | None = None): @click.pass_context def cache_list( ctx, + *, cache_path: Path | None = None, url_filter: str | None = None, sort_by: str = "created", @@ -304,6 +305,7 @@ def cache_reset(ctx, cache_path: Path | None = None, yes: bool = False): @click.pass_context def cache_warm( ctx, + *, cache_path: Path | None = None, profiles_path: Path | None = None, extra_profiles_path: Path | None = None, diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index c2cc6a3db..d9b1e61dd 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -173,6 +173,7 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA @click.pass_context def describe_profile( ctx, + *, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, check_identifier: str | None = None, profiles_path: Path = DEFAULT_PROFILES_PATH, @@ -190,6 +191,7 @@ def describe_profile( With -v on a single check, the source code of the check is shown. """ + # Get the console console = ctx.obj["console"] pager = ctx.obj["pager"] diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c5f01c9af..f69d31586 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -365,11 +365,11 @@ def validate( # interactive selection, or fallback to the base `ro-crate` profile). profile_identifiers, autodetection = _resolve_profile_identifiers( console, - interactive, - no_auto_profile, - available_profiles, - list(profile_identifier), - validation_settings, + interactive=interactive, + no_auto_profile=no_auto_profile, + available_profiles=available_profiles, + profile_identifiers=list(profile_identifier), + validation_settings=validation_settings, ) # Validate the RO-Crate against the selected profiles @@ -512,6 +512,7 @@ def _parse_skip_checks(skip_checks: list[str] | None) -> list[str]: def _resolve_profile_identifiers( console: Console, + *, interactive: bool, no_auto_profile: bool, available_profiles: list, diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 6d245fea2..1186f1cc0 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -68,6 +68,7 @@ def __init__( self, profiles_base_path: Path, profile_path: Path, + *, requirements: list[Requirement] | None = None, identifier: str | None = None, publicID: str | None = None, diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py index f059b902e..47c125f6f 100644 --- a/rocrate_validator/models/skipped_check.py +++ b/rocrate_validator/models/skipped_check.py @@ -26,6 +26,8 @@ class SkipCategory(str, Enum): """Reason category for a skipped requirement check.""" + _description: str + RETURNED = ("returned", "The check returned SKIPPED without another skip reason.") CONFIGURED = ("configured", "The check was skipped by validation settings.") EXCEPTION = ("exception", "The check was skipped because an exception prevented validation.") diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index 1b75e727f..82d964205 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -260,6 +260,7 @@ def duration(self) -> float | None: @staticmethod def __collect_requirement_checks__( requirement, + *, severity_validation, validation_settings, target_profile_identifier, @@ -340,11 +341,11 @@ def __initialise__(cls, validation_settings: ValidationSettings): requirement_checks_count = cls.__collect_requirement_checks__( requirement, - severity_validation, - validation_settings, - target_profile_identifier, - checks, - checks_by_severity, + severity_validation=severity_validation, + validation_settings=validation_settings, + target_profile_identifier=target_profile_identifier, + checks=checks, + checks_by_severity=checks_by_severity, ) # count the requirements and checks diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 3d870963e..eda4b1053 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -209,6 +209,7 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali profile_requirements, ) terminate = False + requirement_index = -1 for requirement_index in range(len(profile_requirements)): requirement = profile_requirements[requirement_index] if not requirement.overridden: diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index 6d68bdf41..53c78de57 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -29,8 +29,8 @@ # set up logging logger = logging.getLogger(__name__) -_EXPECTED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) -_EXPECTED_REMOTE_CONTEXT_ERRORS = (*_EXPECTED_METADATA_ERRORS, OSError, RuntimeError) +_HANDLED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_HANDLED_REMOTE_CONTEXT_ERRORS = (*_HANDLED_METADATA_ERRORS, OSError, RuntimeError) @requirement(name="File Descriptor existence") @@ -98,7 +98,7 @@ def check(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -198,7 +198,7 @@ def __check_remote_context__(self, context_uri: str) -> bool: return True except OfflineCacheMissError: raise - except _EXPECTED_REMOTE_CONTEXT_ERRORS: + except _HANDLED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating JSON-LD context is a dictionary") return False @@ -249,7 +249,7 @@ def check_context(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error extracting @context from file descriptor") return False @@ -352,7 +352,7 @@ def _check_flattened_graph(self, context, is_flat): except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error flattening JSON-LD file descriptor") return False @@ -383,7 +383,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @id property of file descriptor entities") return False @@ -413,7 +413,7 @@ def check_types(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @type property of file descriptor entities") return False @@ -509,7 +509,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n logger.debug(f"{context_keys}") except OfflineCacheMissError: raise - except _EXPECTED_REMOTE_CONTEXT_ERRORS as e: + except _HANDLED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting context keys from JSON-LD") context.result.add_issue(str(e), self) @@ -547,7 +547,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS as e: + except _HANDLED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 4c5537b8f..ab5c8876d 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -27,8 +27,8 @@ # set up logging logger = logging.getLogger(__name__) -_EXPECTED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) -_EXPECTED_REMOTE_CONTEXT_ERRORS = (*_EXPECTED_METADATA_ERRORS, OSError, RuntimeError) +_HANDLED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_HANDLED_REMOTE_CONTEXT_ERRORS = (*_HANDLED_METADATA_ERRORS, OSError, RuntimeError) # Property keys whose string values are literals (e.g. names, dates, URLs) rather # than references to other entities, so a matching @id must not be flagged. @@ -155,7 +155,7 @@ def check(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -240,7 +240,7 @@ def __check_remote_context__(self, context_uri: str) -> bool: return True except OfflineCacheMissError: raise - except _EXPECTED_REMOTE_CONTEXT_ERRORS: + except _HANDLED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -309,7 +309,7 @@ def has_expected_context(ctx: object) -> bool: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -418,7 +418,7 @@ def check_flattened(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -449,7 +449,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -479,7 +479,7 @@ def check_types(self, context: ValidationContext) -> CheckResultValue: except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -506,7 +506,7 @@ def check_unique_identifiers(self, context: ValidationContext) -> CheckResultVal except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -562,7 +562,7 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -591,7 +591,7 @@ def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -682,7 +682,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n logger.debug(f"{context_keys}") except OfflineCacheMissError: raise - except _EXPECTED_REMOTE_CONTEXT_ERRORS as e: + except _HANDLED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(str(e), self) @@ -720,7 +720,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _EXPECTED_METADATA_ERRORS as e: + except _HANDLED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index 093595da7..5bf475afd 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -44,6 +44,8 @@ def check_preview_html(self, context: ValidationContext) -> CheckResultValue: return True try: content = context.ro_crate.get_file_content(preview_path, binary_mode=False) + if isinstance(content, bytes): + content = content.decode("utf-8", errors="strict") if " str | None: # cite-as can be a plain string literal or an entity reference {"@id": "..."} if isinstance(cite_as_raw, str): cite_as_url = cite_as_raw - elif hasattr(cite_as_raw, "id"): - cite_as_url = cite_as_raw.id else: - return None + entity_id = getattr(cite_as_raw, "id", None) + if not isinstance(entity_id, str): + return None + cite_as_url = entity_id if not cite_as_url or not cite_as_url.startswith("http"): return None return cite_as_url diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index fff3239ae..9dc7dfd37 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -62,6 +62,7 @@ def __init__( self, requirement: Requirement, shape: Shape, + *, name: str | None = None, root: bool = False, hidden: bool | None = None, diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 8367b494e..74eb48cf0 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -410,6 +410,7 @@ def validate( self, # data to validate data_graph: GraphLike | str | bytes, + *, # validation settings abort_on_first: bool | None = True, advanced: bool | None = True, diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 6e9fe3e6d..189397d5d 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -161,7 +161,7 @@ def test_not_compacted(): def test_invalid_context_reference(): """ - Test that the metadata document is valid when it has an invalid context reference. + Test that the metadata document is invalid when it has an invalid context reference. """ do_entity_test( __metadata_document_crates__.invalid_context_reference, From 41cd9fc3be279ad8c15effd6d891f4770cf2dafb Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 15 Sep 2026 16:31:48 +0200 Subject: [PATCH 059/271] port updates from recent PRs to ro-crate 1.3 --- .../1.3/must/0_file_descriptor_format.py | 52 ++++++++++---- .../ro-crate/1.3/must/1_ro_crate_preview.py | 7 +- .../1.3/must/2_root_data_entity_cite_as.py | 4 ++ .../1.3/must/2_root_data_entity_haspart.ttl | 11 +-- .../1.3/must/2_root_data_entity_identifier.py | 4 ++ .../1.3/must/2_root_identifier_property.py | 4 ++ .../1.3/must/4_data_entity_metadata.py | 68 ++++++++++++++++--- .../1.3/must/4_data_entity_metadata.ttl | 18 +++-- .../1.3/must/6_contextual_entity_metadata.ttl | 12 +++- .../ro-crate/1.3/must/8_workflow_metadata.ttl | 31 +-------- .../profiles/ro-crate/1.3/ontology.ttl | 6 +- .../1.3/should/0_entity_identifier_format.py | 22 +++++- .../ro-crate/1.3/should/0_entity_metadata.ttl | 9 +-- .../should/2_root_data_entity_identifier.py | 16 ++++- ...ity_property_compact_representation_1_3.py | 59 ++++++++++++++++ .../1.3/should/4_data_entity_license.py | 10 ++- .../1.3/should/4_data_entity_metadata.py | 33 +++++++-- .../1.3/should/4_data_entity_metadata.ttl | 6 +- .../1.3/should/4_dataset_data_entity.ttl | 6 +- .../1.3/should/4_missing_file_local_path.py | 15 +++- .../1.3/should/4_referenced_rocrate.py | 8 ++- .../1.3/should/4_referenced_rocrate.ttl | 12 ++-- .../1.3/should/4_web_dataset_distribution.py | 8 ++- .../should/6_contextual_entity_metadata.ttl | 11 +-- .../1.3/should/8_workflow_metadata.ttl | 4 +- .../valid/ro-crate-metadata.json | 8 +-- .../valid/ro-crate-metadata.json | 8 +-- 27 files changed, 325 insertions(+), 127 deletions(-) create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py index 7692331f0..1f5c673ca 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py @@ -15,10 +15,10 @@ import json import re from http import HTTPStatus -from pathlib import Path from typing import Any from urllib.parse import urljoin +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -80,11 +80,7 @@ def test_size(self, context: ValidationContext) -> bool: if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") return True - if not context.ro_crate.has_descriptor(): - message = f"file descriptor {context.rel_fd_path} is empty" - context.result.add_issue(message, self) - return False - if context.ro_crate.metadata.size == 0: + if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -101,19 +97,23 @@ def check(self, context: ValidationContext) -> bool: """ Check if the file descriptor is UTF-8 encoded """ + if context.settings.metadata_only: + logger.debug("Skipping file descriptor encoding check in metadata-only mode") + return True try: - raw_data = context.ro_crate.get_file_content( - Path(context.ro_crate.metadata_descriptor_id), binary_mode=True - ) + raw_data = context.ro_crate.metadata.as_bytes() if isinstance(raw_data, str): return True raw_data.decode("utf-8") return True - except Exception: + except UnicodeDecodeError: context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") + logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") + return True @requirement(name="File Descriptor JSON format") @@ -140,6 +140,9 @@ def check(self, context: ValidationContext) -> bool: # The metadata cannot be parsed: abort to avoid false positives downstream. context.abort_validation(f"file descriptor is not valid JSON: {e}") return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + return True except Exception: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self @@ -250,7 +253,8 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) @check(name="File Descriptor @context property validation") def check_context(self, context: ValidationContext) -> bool: - """Check if the file descriptor contains + """ + Check if the file descriptor contains the @context property and it is a valid JSON-LD context """ try: @@ -281,6 +285,9 @@ def has_expected_context(ctx: object) -> bool: # Check if the context is valid return self.__check_contexts__(context, json_dict["@context"]) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -380,6 +387,9 @@ def check_flattened(self, context: ValidationContext) -> bool: if fail_fast: return False return result + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -400,6 +410,9 @@ def check_identifiers(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -420,6 +433,9 @@ def check_types(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -437,6 +453,9 @@ def check_unique_identifiers(self, context: ValidationContext) -> bool: ) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping unique identifier check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -483,6 +502,9 @@ def check_entity_references(self, context: ValidationContext) -> bool: context.result.add_issue(message, self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping entity reference check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -502,6 +524,9 @@ def check_subject_keywords(self, context: ValidationContext) -> bool: context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'keywords'", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping subject and keyword check: metadata descriptor is not available") + return True except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -618,6 +643,9 @@ def check_compaction(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") return True except Exception as e: if logger.isEnabledFor(logging.DEBUG): diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py index ed1b2e281..1c7c0b79f 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py @@ -14,6 +14,7 @@ from pathlib import Path +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,7 +31,11 @@ class ROCrateWebsiteChecker(PyFunctionCheck): @check(name="RO-Crate Website HTML5 doctype") def check_preview_html(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached(): + try: + if context.ro_crate.is_detached(): + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") return True preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py index f253bb273..8625a6fa3 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -81,6 +82,9 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> bool: ) return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl index 04c2b8973..905ea1f55 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl @@ -19,16 +19,7 @@ ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; sh:description """The Root Data Entity MUST directly or indirectly reference all Data Entities in the RO-Crate via hasPart (RO-Crate 1.3).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ro-crate:RootDataEntity . - } - """ - ] ; + sh:targetClass ro-crate:RootDataEntity ; sh:sparql [ a sh:SPARQLConstraint ; sh:prefixes ro-crate:sparqlPrefixes ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py index 5ac8766c1..cfc84fa3d 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py @@ -14,6 +14,7 @@ import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -42,6 +43,9 @@ def check_identifier(self, context: ValidationContext) -> bool: self, ) return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py index 1c1f17b9f..ace24b4d0 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -43,6 +44,9 @@ def check_identifier_values(self, context: ValidationContext) -> bool: context.result.add_issue("PropertyValue identifiers MUST include a `value`", self) return False return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py index 2a22782fe..85207aade 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py @@ -15,6 +15,7 @@ import contextlib import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -31,11 +32,16 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> bool: # noqa: C901 """ Check the presence of the Data Entity in the RO-Crate """ - if context.ro_crate.is_detached(): + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + return True + if is_detached: logger.debug("Skipping data entity payload checks for detached RO-Crate") return True # Skip the check in metadata-only mode @@ -44,7 +50,12 @@ def check_availability(self, context: ValidationContext) -> bool: return True # Perform the check result = True - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) try: @@ -94,13 +105,23 @@ class DetachedDataEntityChecker(PyFunctionCheck): @check(name="Detached RO-Crate: data entities MUST be web-based") def check_detached_entities(self, context: ValidationContext) -> bool: - if not context.ro_crate.is_detached(): + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + return True + if not is_detached: return True result = True root_entity_id = None with contextlib.suppress(Exception): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + return True + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not entity.is_remote(): @@ -123,7 +144,7 @@ class DataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> bool: + def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 result = True root_entity_id = None root_entity_is_local = False @@ -135,7 +156,12 @@ def check_identifiers(self, context: ValidationContext) -> bool: root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False ) root_entity_absolute_path = root_data_entity.id_as_path if root_data_entity.has_absolute_path() else None - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") + return True + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not root_entity_is_local and not entity.is_remote(): @@ -172,10 +198,20 @@ def check_identifiers(self, context: ValidationContext) -> bool: @check(name="Data Entity: relative @id for payload files") def check_relative_paths(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached(): + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + return True + if is_detached: return True result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + return True + for entity in entities: if entity.has_local_identifier() or entity.is_remote(): continue if entity.has_absolute_path() and ( @@ -199,7 +235,12 @@ class DataEntityCitationChecker(PyFunctionCheck): @check(name="Data Entity: citation must include @id") def check_citation(self, context: ValidationContext) -> bool: result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + return True + for entity in entities: citations = entity.get_property("citation") if citations is None: continue @@ -256,7 +297,12 @@ def check_availability(self, context: ValidationContext) -> bool: ): return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip directory URIs: assumed available, not directly downloadable by spec if entity.id.endswith("/"): diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl index ca650bb21..cc91f01ed 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl @@ -44,9 +44,11 @@ ro-crate:FileDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:MediaObject . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } """ @@ -93,10 +95,12 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . # Exclude all dataset entities that ends with `./#` - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } @@ -161,9 +165,11 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?root schema:hasPart ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(?this != ?metadatafile) FILTER NOT EXISTS { diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl index 316f540ab..fb4268b33 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl @@ -37,14 +37,20 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ - SELECT DISTINCT ?this + SELECT ?this WHERE { # Structural criteria: IRI or blank node, subject of some triple. # SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) # tag even referenced-but-undescribed entities so they become # subjects and are correctly classified as Contextual Entities. - ?this ?p ?o . - FILTER(isIRI(?this) || isBlank(?this)) + # + # The distinct subjects are materialised by a subquery FIRST so the + # exclusion FILTERs below run once per entity instead of once per + # triple (~2.5k subjects vs ~40k triples on a large crate). + { SELECT DISTINCT ?this WHERE { + ?this ?p ?o . + FILTER(isIRI(?this) || isBlank(?this)) + } } # Structural exclusions — Metadata Descriptor, Root Data Entity, # Data Entities (File/Dataset or ro-crate:DataEntity), Ontology. diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl index 14b37cf6d..f96123efa 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl @@ -20,23 +20,11 @@ @prefix xsd: . @prefix validator: . -# Selects every SoftwareSourceCode entity (Scripts and Workflows). -ro-crate:SoftwareSourceCodeTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a schema:SoftwareSourceCode . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } - """ . - # --------------------------------------------------------------- # Identify Script entities # # A Script is an entity with schema:SoftwareSourceCode in @type -# that is NOT a Workflow (no bioschemas:ComputationalWorkflow). +# that is NOT a Workflow (`no bioschemas:ComputationalWorkflow). # --------------------------------------------------------------- ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Script entities" ; @@ -49,8 +37,6 @@ ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; SELECT ?this WHERE { ?this a schema:SoftwareSourceCode . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER NOT EXISTS { ?this a bioschemas:ComputationalWorkflow } } """ @@ -72,18 +58,7 @@ ro-crate:WorkflowDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Workflow entities" ; sh:description """Mark entities as Workflows if they have bioschemas:ComputationalWorkflow in @type.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a bioschemas:ComputationalWorkflow . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } - """ - ] ; + sh:targetClass bioschemas:ComputationalWorkflow ; sh:rule [ a sh:TripleRule ; sh:subject sh:this ; @@ -161,7 +136,7 @@ ro-crate:ScriptOrWorkflowRequiredName a sh:NodeShape ; sh:name "Script or Workflow: REQUIRED `name`" ; sh:description """Scripts and Workflows MUST have a human-readable `name` property (RO-Crate 1.3, Workflows and Scripts).""" ; - sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:targetClass schema:SoftwareSourceCode ; sh:property [ a sh:PropertyShape ; sh:name "Script or Workflow: REQUIRED `name` property" ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl index 4db6846f3..1e12c9fcc 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl @@ -19,7 +19,7 @@ @prefix xsd: . @prefix rdfs: . @prefix schema: . -@prefix ro-crate: . +@prefix ro-crate: . @prefix bioschemas: . # @base <./.> . @@ -54,13 +54,13 @@ schema:SoftwareSourceCode rdf:type owl:Class ; bioschemas:ComputationalWorkflow rdf:type owl:Class . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.3/Script +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/Script ro-crate:Script rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject ; rdfs:label "Script"@en . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.3/Workflow +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/Workflow ro-crate:Workflow rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject, bioschemas:ComputationalWorkflow ; rdfs:label "Workflow"@en . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py index c9cdb2de3..276cf9fc0 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py @@ -22,6 +22,7 @@ import contextlib import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -52,7 +53,12 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: (RO-Crate 1.3, JSON-LD appendix). """ result = True - for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + try: + entities = context.ro_crate.metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping parent traversal check: metadata descriptor is not available") + return True + for entity in entities: entity_id = entity.get("@id", "") if "../" in entity_id: context.result.add_issue( @@ -75,7 +81,12 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: UTF-8 rather than percent-encoded (RO-Crate 1.3, JSON-LD appendix). """ result = True - for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + try: + entities = context.ro_crate.metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") + return True + for entity in entities: entity_id = entity.get("@id", "") if _PCT_NON_ASCII_RE.search(entity_id): context.result.add_issue( @@ -110,7 +121,12 @@ def check_named_entity_id_format(self, context: ValidationContext) -> bool: with contextlib.suppress(Exception): non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) - for entity in ro_crate_metadata.as_dict().get("@graph", []): + try: + entities = ro_crate_metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping named entity identifier check: metadata descriptor is not available") + return True + for entity in entities: entity_id = entity.get("@id", "") if entity_id in non_contextual_ids: continue diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl index 3f684ef91..03a6395df 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl @@ -27,10 +27,11 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; WHERE { ?this a ?type . - # Exclude Root and RO-Crate Metadata File entities - ?root a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } FILTER(?this != ?metadatafile) FILTER (?this != ?root) # Exclude entities with non-IRI identifiers or those from specific namespaces diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py index 54aa81243..1f0fce210 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,7 +29,7 @@ class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED identifier") - def check_identifier(self, context: ValidationContext) -> bool: + def check_identifier(self, context: ValidationContext) -> bool: # noqa: PLR0911 """ In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. """ @@ -50,6 +51,9 @@ def check_identifier(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) @@ -64,7 +68,7 @@ class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: use cite-as for resolvable identifiers") - def check_cite_as_reference(self, context: ValidationContext) -> bool: + def check_cite_as_reference(self, context: ValidationContext) -> bool: # noqa: PLR0911 """ If the Root Data Entity has a resolvable identifier, it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. @@ -95,6 +99,9 @@ def check_cite_as_reference(self, context: ValidationContext) -> bool: ) return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) @@ -140,7 +147,7 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) - def check_identifier_resolvable(self, context: ValidationContext) -> bool: + def check_identifier_resolvable(self, context: ValidationContext) -> bool: # noqa: PLR0911 if context.settings.skip_availability_check: return True if context.settings.metadata_only: @@ -169,6 +176,9 @@ def check_identifier_resolvable(self, context: ValidationContext) -> bool: context.result.add_issue(msg, self) result = False return result + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") + return True except Exception as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py b/rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py new file mode 100644 index 000000000..12e382026 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + +logger = logging.getLogger(__name__) + + +@requirement(name="Entity properties compact representation") +class EntityPropertyCompactRepresentation(PyFunctionCheck): + """ + Single-element arrays used for entity properties SHOULD be unpacked to a + single value in compacted RO-Crate 1.3 JSON-LD. + """ + + @check(name="Entity properties SHOULD use single values rather than singleton arrays") + def check_singleton_property_arrays(self, context: ValidationContext) -> bool: + """Check the RO-Crate 1.3 recommendation for singleton property arrays.""" + try: + findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) + except ROCrateMetadataNotFoundError as error: + logger.debug("Skipping singleton property array check: metadata descriptor is not available (%s)", error) + return True + except (AttributeError, TypeError, ValueError) as error: + # Required format checks report malformed metadata structure. + logger.debug("Skipping singleton property array check: metadata cannot be inspected (%s)", error) + return True + + result = True + for entity_id, property_name in findings: + context.result.add_issue( + f'Entity "{entity_id}" property "{property_name}" SHOULD be represented as a single value, ' + "not a singleton array, according to the RO-Crate 1.3 JSON-LD recommendation", + self, + violatingEntity=entity_id, + violatingProperty=property_name, + violatingPropertyValue="singleton array", + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py index 9e8a0470b..42b86616d 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py @@ -46,6 +46,7 @@ # check provides actionable feedback to authors without penalising # valid crates. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,7 +101,7 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> bool: + def check_license_divergence(self, context: ValidationContext) -> bool: # noqa: C901 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -117,7 +118,12 @@ def check_license_divergence(self, context: ValidationContext) -> bool: if not root_license_id: return True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + return True + for entity in entities: if entity.id == root_entity.id: continue entity_license_raw = entity.get_property("license") diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py index ecffd0a6f..ab2124b61 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py @@ -14,6 +14,7 @@ import re +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -32,7 +33,12 @@ class DataEntityCitationChecker(PyFunctionCheck): @check(name="Data Entity: citation must include @id") def check_citation(self, context: ValidationContext) -> bool: result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + return True + for entity in entities: citations = entity.get_property("citation") if citations is None: continue @@ -90,7 +96,12 @@ def check_availability_warning(self, context: ValidationContext) -> bool: ): return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.id.endswith("/"): continue @@ -107,11 +118,16 @@ def check_availability_warning(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) - def check_content_size(self, context: ValidationContext) -> bool: + def check_content_size(self, context: ValidationContext) -> bool: # noqa: C901 if context.settings.skip_availability_check: return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + return True + for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.is_available(): content_size = entity.get_property("contentSize") @@ -143,11 +159,16 @@ def check_content_size(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) - def check_content_url(self, context: ValidationContext) -> bool: + def check_content_url(self, context: ValidationContext) -> bool: # noqa: C901 if context.settings.skip_availability_check: return True result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") + return True + for entity in entities: content_url = entity.get_property("contentUrl") if not content_url: continue diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl index 563cb53f1..aecc04fe1 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl @@ -164,9 +164,11 @@ ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) FILTER regex(str(?this), "^https?://", "i") diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl index 7ddaedc21..dad77234f 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl @@ -21,9 +21,11 @@ ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) FILTER(!regex(str(?this), "^https?://", "i")) diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py index f1276ea53..50d54dbb9 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py @@ -32,6 +32,7 @@ import contextlib +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,13 +101,23 @@ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) def check_missing_file_local_path(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached() or context.settings.metadata_only: + try: + is_detached = context.ro_crate.is_detached() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping missing local file check: metadata descriptor is not available") + return True + if is_detached or context.settings.metadata_only: return True root_entity_id = None with contextlib.suppress(Exception): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id result = True - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping missing local file check: metadata descriptor is not available") + return True + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not entity.is_file(): diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py index 76c4d11c9..17200ccf1 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py @@ -23,6 +23,7 @@ This Python check performs the network-dependent refinement. """ +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -81,7 +82,12 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: except Exception: return True - for entity in context.ro_crate.metadata.get_dataset_entities(): + try: + entities = context.ro_crate.metadata.get_dataset_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + return True + for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): continue entity_id = entity.id diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl index 52ce8d6c2..e2db267f1 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl @@ -35,8 +35,10 @@ ro-crate:ReferencedROCrateDataEntityTarget a sh:SPARQLTarget ; ?this a schema:Dataset ; dct:conformsTo ?profile . FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } FILTER(?this != ?root) } """ . @@ -56,8 +58,10 @@ ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; dct:conformsTo ?profile ; schema:subjectOf ?this . FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } } FILTER(?crate != ?root) } """ . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py index 2c548512d..5f9ae888b 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -83,7 +84,12 @@ def check_distribution_downloadable(self, context: ValidationContext) -> bool: if context.settings.skip_availability_check or context.settings.metadata_only: return True result = True - for entity in context.ro_crate.metadata.get_dataset_entities(): + try: + entities = context.ro_crate.metadata.get_dataset_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") + return True + for entity in entities: distribution_raw = entity.get_property("distribution") if not distribution_raw: continue diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl index 57512af55..ea05c5f9e 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl @@ -162,16 +162,7 @@ ro-crate:PersonAffiliationRecommendedProperties a sh:NodeShape ; ro-crate:LicenseEntityRecommendedProperties a sh:NodeShape ; sh:name "License entity: RECOMMENDED properties" ; sh:description """License entities SHOULD have `name` and `description`.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?subject schema:license ?this . - } - """ - ] ; + sh:targetObjectsOf schema:license ; sh:property [ sh:path schema:name ; sh:minCount 1 ; diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl index b20e51ac0..1dfa19811 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl @@ -29,8 +29,6 @@ ro-crate:ScriptOrWorkflowImageTarget a sh:SPARQLTarget ; WHERE { ?sw a schema:SoftwareSourceCode . ?sw schema:image ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) } """ . @@ -43,7 +41,7 @@ ro-crate:ScriptOrWorkflowProgrammingLanguage a sh:NodeShape ; sh:description """Scripts and Workflows SHOULD have a `programmingLanguage` property referencing a contextual entity of type `ComputerLanguage` (RO-Crate 1.3, Workflows and Scripts).""" ; - sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:targetClass schema:SoftwareSourceCode ; sh:property [ a sh:PropertyShape ; sh:name "Script or Workflow: RECOMMENDED `programmingLanguage` property" ; diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json index 6d3a142bb..880550b45 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "script.sh" - } - ], + "hasPart": { + "@id": "script.sh" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json index afe7781c3..2709cc83a 100644 --- a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, - "hasPart": [ - { - "@id": "workflow.ga" - } - ], + "hasPart": { + "@id": "workflow.ga" + }, "identifier": { "@id": "#uuid" } From 80531e0add141bd6a439eef73a298138faceeebd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 18:52:12 +0200 Subject: [PATCH 060/271] feat(graph): :sparkles: add graph transformer base model Define the transformer protocol, registration metadata, and deterministic execution registry. --- .../graph_transformers/__init__.py | 17 +++ rocrate_validator/graph_transformers/base.py | 101 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 rocrate_validator/graph_transformers/__init__.py create mode 100644 rocrate_validator/graph_transformers/base.py diff --git a/rocrate_validator/graph_transformers/__init__.py b/rocrate_validator/graph_transformers/__init__.py new file mode 100644 index 000000000..09f9ad102 --- /dev/null +++ b/rocrate_validator/graph_transformers/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.graph_transformers.base import GraphTransformer, graph_transformer + +__all__ = ["GraphTransformer", "graph_transformer"] diff --git a/rocrate_validator/graph_transformers/base.py b/rocrate_validator/graph_transformers/base.py new file mode 100644 index 000000000..62163bc75 --- /dev/null +++ b/rocrate_validator/graph_transformers/base.py @@ -0,0 +1,101 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect +from abc import ABC, abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from typing import ClassVar + +from rdflib import Graph + +TransformerFunction = Callable[[Graph], Graph] + + +@dataclass(frozen=True) +class RegisteredTransformer: + """A transformer definition together with its execution metadata.""" + + name: str + order: int + implementation: TransformerFunction | type[GraphTransformer] + + +# Qualified names make registration idempotent when Python imports or reloads +# a transformer module more than once. A reload replaces the same definition +# instead of scheduling it twice. +_TRANSFORMERS: dict[str, RegisteredTransformer] = {} + + +def _register( + implementation: TransformerFunction | type[GraphTransformer], + order: int, +) -> None: + name = f"{implementation.__module__}.{implementation.__qualname__}" + _TRANSFORMERS[name] = RegisteredTransformer(name, order, implementation) + + +class GraphTransformer(ABC): + """ + Base class for automatically registered graph transformers. + + Concrete subclasses are registered as soon as their module is imported. + Abstract intermediate classes remain unregistered. + """ + + order: ClassVar[int] = 100 + + def __init_subclass__(cls, **kwargs: object) -> None: + super().__init_subclass__(**kwargs) + if not inspect.isabstract(cls): + _register(cls, cls.order) + + @abstractmethod + def transform(self, data_graph: Graph) -> Graph: + """Transform and return the graph passed by the orchestration pipeline.""" + + +def graph_transformer(*, order: int = 100) -> Callable[[TransformerFunction], TransformerFunction]: + """Register a function as a graph transformer.""" + + def decorator(function: TransformerFunction) -> TransformerFunction: + _register(function, order) + return function + + return decorator + + +def get_registered_transformers() -> tuple[RegisteredTransformer, ...]: + """Return registered transformers in deterministic execution order.""" + # Lower values run first. The qualified name is a deterministic tie-breaker + # so execution never depends on filesystem or import iteration order. + return tuple(sorted(_TRANSFORMERS.values(), key=lambda item: (item.order, item.name))) + + +def run_registered_transformers(data_graph: Graph) -> Graph: + """Run every registered transformer sequentially on the same graph.""" + transformed_graph = data_graph + for registered in get_registered_transformers(): + # Class transformers are deliberately instantiated without arguments; + # persistent state must not leak between separate validation runs. + implementation = registered.implementation + if isinstance(implementation, type): + transformed_graph = implementation().transform(transformed_graph) + else: + transformed_graph = implementation(transformed_graph) + if not isinstance(transformed_graph, Graph): + raise TypeError(f"Graph transformer {registered.name} did not return an RDFLib Graph") + return transformed_graph From 74aca87ed604be2dd1f4c0c3df225f645c35ed9b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 18:57:27 +0200 Subject: [PATCH 061/271] test(graph): :white_check_mark: cover transformer base model Cover registration, execution ordering, abstract classes, and return types. --- tests/unit/graph_transformers/test_base.py | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/unit/graph_transformers/test_base.py diff --git a/tests/unit/graph_transformers/test_base.py b/tests/unit/graph_transformers/test_base.py new file mode 100644 index 000000000..811fcdc84 --- /dev/null +++ b/tests/unit/graph_transformers/test_base.py @@ -0,0 +1,76 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod + +import pytest +from rdflib import Graph, Literal, Namespace + +from rocrate_validator.graph_transformers import GraphTransformer, graph_transformer +from rocrate_validator.graph_transformers import base as transformer_base +from rocrate_validator.graph_transformers.base import ( + get_registered_transformers, + run_registered_transformers, +) + +EXAMPLE = Namespace("https://example.org/") + + +def test_abstract_transformers_are_not_registered(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(transformer_base, "_TRANSFORMERS", {}) + + class AbstractTransformer(GraphTransformer): + @abstractmethod + def transform(self, data_graph: Graph) -> Graph: + raise NotImplementedError + + assert get_registered_transformers() == () + + +def test_registered_transformers_run_in_order(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(transformer_base, "_TRANSFORMERS", {}) + trace = EXAMPLE.trace + step = EXAMPLE.step + + @graph_transformer(order=20) + def function_transformer(data_graph: Graph) -> Graph: + assert (trace, step, Literal("class")) in data_graph + data_graph.add((trace, step, Literal("function"))) + return data_graph + + class ClassTransformer(GraphTransformer): + order = 10 + + def transform(self, data_graph: Graph) -> Graph: + data_graph.add((trace, step, Literal("class"))) + return data_graph + + source = Graph() + transformed = run_registered_transformers(source) + + assert (trace, step, Literal("class")) in transformed + assert (trace, step, Literal("function")) in transformed + assert [item.order for item in get_registered_transformers()] == [10, 20] + + +def test_transformer_must_return_an_rdflib_graph(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(transformer_base, "_TRANSFORMERS", {}) + + @graph_transformer() + def invalid_transformer(data_graph: Graph) -> Graph: + del data_graph + return "not a graph" # type: ignore[return-value] + + with pytest.raises(TypeError, match="did not return an RDFLib Graph"): + run_registered_transformers(Graph()) From 9603e8d703082ddfcb8e9420ae7d000cea931aff Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 20:04:20 +0200 Subject: [PATCH 062/271] feat(graph): :sparkles: add validation candidate marker Discover graph transformers and annotate transient validation graphs with candidate markers. --- .../graph_transformers/__init__.py | 71 +++++++++++++++++- .../validation_candidate_marker.py | 75 +++++++++++++++++++ .../graph_transformers/vocabulary.py | 21 ++++++ 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 rocrate_validator/graph_transformers/validation_candidate_marker.py create mode 100644 rocrate_validator/graph_transformers/vocabulary.py diff --git a/rocrate_validator/graph_transformers/__init__.py b/rocrate_validator/graph_transformers/__init__.py index 09f9ad102..0316c5c56 100644 --- a/rocrate_validator/graph_transformers/__init__.py +++ b/rocrate_validator/graph_transformers/__init__.py @@ -12,6 +12,73 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rocrate_validator.graph_transformers.base import GraphTransformer, graph_transformer +from __future__ import annotations -__all__ = ["GraphTransformer", "graph_transformer"] +import importlib +import pkgutil +from threading import Lock + +from rdflib import Dataset, Graph + +from rocrate_validator.graph_transformers.base import ( + GraphTransformer, + graph_transformer, + run_registered_transformers, +) +from rocrate_validator.graph_transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE + +_DISCOVERY_LOCK = Lock() +_TRANSFORMERS_LOADED = False + + +def _load_concrete_transformers() -> None: + """ + Import every transformer module in this package exactly once. + + Importing triggers function decorators and ``GraphTransformer`` subclass + hooks. The lock prevents concurrent validations from observing a partially + populated registry during first use. + """ + global _TRANSFORMERS_LOADED # noqa: PLW0603 + if _TRANSFORMERS_LOADED: + return + with _DISCOVERY_LOCK: + if _TRANSFORMERS_LOADED: + return + package_prefix = f"{__name__}." + for module in pkgutil.walk_packages(__path__, package_prefix): + if module.name != f"{__name__}.base": + importlib.import_module(module.name) + _TRANSFORMERS_LOADED = True + + +def _copy_graph(source: Graph) -> Graph: + """Return a validation-only copy, including namespace bindings.""" + identifier = source.default_graph.identifier if isinstance(source, Dataset) else source.identifier + target = Graph(identifier=identifier) + for prefix, namespace in source.namespaces(): + target.bind(prefix, namespace, replace=True) + # Dataset iteration yields quads; copy the union view as triples so the + # transient validation graph has the same shape for Graph and Dataset input. + for triple in source.triples((None, None, None)): + target.add(triple) + return target + + +def prepare_data_graph(data_graph: Graph) -> Graph: + """Discover and run all concrete transformers on a transient graph copy. + + The copy is created once for the complete pipeline. Each transformer sees + the output of its predecessor, while the cached graph owned by the + validation context remains untouched. + """ + _load_concrete_transformers() + return run_registered_transformers(_copy_graph(data_graph)) + + +__all__ = [ + "VALIDATION_CANDIDATE_PREDICATE", + "GraphTransformer", + "graph_transformer", + "prepare_data_graph", +] diff --git a/rocrate_validator/graph_transformers/validation_candidate_marker.py b/rocrate_validator/graph_transformers/validation_candidate_marker.py new file mode 100644 index 000000000..871c8c9b4 --- /dev/null +++ b/rocrate_validator/graph_transformers/validation_candidate_marker.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging + +from rdflib import RDF, BNode, Graph, Literal, URIRef + +from rocrate_validator.graph_transformers.base import graph_transformer +from rocrate_validator.graph_transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE + +logger = logging.getLogger(__name__) + +# These predicates point to an identifier, format, or vocabulary term without +# claiming that the object is an entity which the crate needs to describe. +REFERENCE_PREDICATES: frozenset[URIRef] = frozenset( + { + RDF.type, + URIRef("http://schema.org/propertyID"), + URIRef("http://www.w3.org/ns/csvw#propertyUrl"), + URIRef("http://schema.org/inDefinedTermSet"), + URIRef("http://schema.org/encodingFormat"), + URIRef("http://purl.org/dc/terms/conformsTo"), + } +) + + +# The default-stage order leaves lower values available for transformers that +# must normalize or annotate the graph before candidate classification. +@graph_transformer(order=100) +def mark_validation_candidates(data_graph: Graph) -> Graph: + """ + Mark validation candidates identified from the current data graph. + + Classification is based exclusively on assertions from the original + RO-Crate graph, before pySHACL adds ontology and inferred triples. An IRI is + kept in validation scope when the crate describes it with a predicate other + than ``rdf:type``, or when another entity points to it through a predicate + that claims it as an entity (for example ``schema:author`` or + ``schema:hasPart``). + + A positive marker keeps the validation scope stable when pySHACL later adds + ontology and inferred terms which were not visible in the original graph. + The orchestration layer provides a transient graph copy, so the validation + context's cached source graph is never mutated. + """ + described_or_claimed: set[URIRef | BNode] = set() + + for subject, predicate, obj in data_graph: + if isinstance(subject, (URIRef, BNode)) and predicate != RDF.type: + described_or_claimed.add(subject) + if isinstance(obj, (URIRef, BNode)) and predicate not in REFERENCE_PREDICATES: + described_or_claimed.add(obj) + + marker_value = Literal(True) + for node in described_or_claimed: + data_graph.add((node, VALIDATION_CANDIDATE_PREDICATE, marker_value)) + + logger.debug( + "Prepared validation graph with %d validation-candidate markers", + len(described_or_claimed), + ) + return data_graph diff --git a/rocrate_validator/graph_transformers/vocabulary.py b/rocrate_validator/graph_transformers/vocabulary.py new file mode 100644 index 000000000..eed89feff --- /dev/null +++ b/rocrate_validator/graph_transformers/vocabulary.py @@ -0,0 +1,21 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rdflib import Namespace, URIRef + +# Private vocabulary for annotations that exist only in the transient graph +# passed to pySHACL. These predicates describe validator bookkeeping, not +# RO-Crate metadata, and must never be serialized back into the crate. +MARKER_NS = Namespace("https://github.com/crs4/rocrate-validator/graph-transformers/") +VALIDATION_CANDIDATE_PREDICATE: URIRef = URIRef(MARKER_NS.validationCandidate) From ab9b37f8d70add31fa4329f1df24419dfab19e17 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 20:06:29 +0200 Subject: [PATCH 063/271] feat(profiles): :sparkles: use graph markers for entity scope Replace namespace filters with validation-candidate markers from the original crate graph. --- .../1.2/must/6_contextual_entity_metadata.ttl | 37 +++---------------- .../profiles/ro-crate/1.2/prefixes.ttl | 6 +++ .../ro-crate/1.2/should/0_entity_metadata.ttl | 17 ++++----- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl index dd1c9e149..e6fe3176f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl @@ -29,10 +29,8 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; (subject of any triple) — SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) tag referenced-but-undescribed entities with rdf:type, so they appear as subjects and are therefore captured. - Vocabulary namespaces loaded from ontologies (RDF/RDFS/OWL/XSD, - schema.org, bioschemas, w3id.org/ro, …) are excluded at the query - level; domain-specific legacy filters that wrongly excluded legitimate - crate identifiers (iana, urn, foaf, …) have been removed.""" ; + Vocabulary terms loaded from ontologies are excluded using provenance + captured from the original crate graph before ontology inoculation.""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; @@ -61,33 +59,10 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; FILTER NOT EXISTS { ?this a schema:Dataset } FILTER NOT EXISTS { ?this a owl:Ontology } - # Vocabulary / profile namespace exclusions. Required because - # the RO-Crate 1.2 profile loads ontology.ttl into the same data - # graph as the metadata file; terms from those vocabularies - # appear as subjects with rdf:type + rdfs:label and would - # otherwise be misclassified. - FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "https://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) - FILTER(!STRSTARTS(STR(?this), "http://www.opengis.net/")) - FILTER(!STRSTARTS(STR(?this), "https://w3id.org/")) - FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) - FILTER(!STRSTARTS(STR(?this), "https://codemeta.github.io/")) - FILTER(!STRSTARTS(STR(?this), "http://pcdm.org/")) - FILTER(!STRSTARTS(STR(?this), "http://xmlns.com/foaf/")) - FILTER(!STRSTARTS(STR(?this), "http://creativecommons.org/ns")) - FILTER(!STRSTARTS(STR(?this), "http://www.iana.org/")) - FILTER(!STRSTARTS(STR(?this), "https://www.iana.org/")) - FILTER(!STRSTARTS(STR(?this), "https://nationaalarchief.nl/archieven/archief/")) - FILTER(!STRSTARTS(STR(?this), "https://www.nationalarchives.gov.uk")) - - # Narrow urn: exclusion — only the internal ontology versionIRI. - # Unlike the legacy blanket `urn:` filter, this keeps legitimate - # `urn:uuid:*` contextual entities (e.g. DigitalDocument) in play. - FILTER(!STRSTARTS(STR(?this), "urn:absolute:")) + # Restrict validation to nodes which the original crate + # describes or claims. Claim-only objects remain in scope after + # SHACL rules materialise their entity type. + FILTER EXISTS { ?this validator-transformers:validationCandidate true } } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl b/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl index 8cad6b4dd..ffa42e3a6 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl @@ -56,4 +56,10 @@ ro-crate:sparqlPrefixes sh:declare [ sh:declare [ sh:prefix "ro" ; sh:namespace "./"^^xsd:anyURI ; + ] ; + # Private predicates added to the transient validation graph by Python + # graph transformers. They are validator bookkeeping, not crate metadata. + sh:declare [ + sh:prefix "validator-transformers" ; + sh:namespace "https://github.com/crs4/rocrate-validator/graph-transformers/"^^xsd:anyURI ; ] . diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl index 96a43f606..012350ea9 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl @@ -34,16 +34,15 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; } } FILTER(?this != ?metadatafile) FILTER (?this != ?root) - # Exclude entities with non-IRI identifiers or those from specific namespaces + # Exclude entities with non-IRI identifiers. FILTER (isIRI(?this)) - FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "https://w3id.org/ro/crate/")) - FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) - FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) - FILTER(!STRSTARTS(STR(?this), "urn:")) + # Restrict validation to nodes which the original crate + # describes or claims, before ontology and inferred terms are + # added to the graph. + FILTER EXISTS { ?this validator-transformers:validationCandidate true } + # Profile descriptors use prof:Profile rather than a Schema.org + # type and have their own dedicated metadata requirements. + FILTER NOT EXISTS { ?this a } } """ ; ] ; From 2ea685f932672f938b375c6afe907a3cb3e14b2c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 20:10:39 +0200 Subject: [PATCH 064/271] test(graph): :white_check_mark: cover validation candidate scope Cover cited references, claimed entities, and profile validation scope. --- .../ro-crate-1.2/test_metadata_entities.py | 54 ++++++- .../test_validation_candidate_marker.py | 142 ++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 tests/unit/requirements/test_validation_candidate_marker.py diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py index ec619d82d..3637c2d0c 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging -from rocrate_validator import models +from rocrate_validator import models, services from tests.ro_crates_v1_2 import MetadataEntities from tests.shared import do_entity_test @@ -24,6 +25,17 @@ __metadata_entities__ = MetadataEntities() +def _validate_metadata(rocrate_path, metadata): + return services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(rocrate_path), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + metadata_dict=metadata, + ) + ) + + def test_valid_recommended_schema_type(): """ Test that the metadata document includes at least one Schema.org type. @@ -76,3 +88,43 @@ def test_invalid_recommended_entity_name_warning(): expected_triggered_requirements=["RO-Crate Metadata Entity: RECOMMENDED properties"], expected_triggered_issues=["Entities SHOULD have a human-readable name"], ) + + +def test_orcid_scheme_reference_is_not_validated_as_an_entity(): + """An identifier scheme referenced through propertyID is only cited.""" + rocrate_path = __metadata_entities__.valid_recommended_schema_type + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + root = next(entity for entity in metadata["@graph"] if entity["@id"] == "./") + root["identifier"] = [root["identifier"], {"@id": "#orcid-reference"}] + metadata["@graph"].append( + { + "@id": "#orcid-reference", + "@type": "PropertyValue", + "name": "ORCID identifier scheme", + "propertyID": {"@id": "https://orcid.org"}, + "value": "ORCID", + } + ) + + result = _validate_metadata(rocrate_path, metadata) + + assert result.passed() + assert not any(issue.violatingEntity == "https://orcid.org" for issue in result.issues) + + +def test_orcid_person_claimed_as_author_remains_in_validation_scope(): + """A person under the same ORCID namespace is claimed, not merely cited.""" + rocrate_path = __metadata_entities__.valid_recommended_schema_type + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + person_id = "https://orcid.org/0009-0000-5074-6239" + root = next(entity for entity in metadata["@graph"] if entity["@id"] == "./") + root["author"] = {"@id": person_id} + metadata["@graph"].append({"@id": person_id, "@type": "Person"}) + + result = _validate_metadata(rocrate_path, metadata) + + assert not result.passed() + assert any( + issue.violatingEntity == person_id and issue.message == "Entities SHOULD have a human-readable name" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) diff --git a/tests/unit/requirements/test_validation_candidate_marker.py b/tests/unit/requirements/test_validation_candidate_marker.py new file mode 100644 index 000000000..f2d9a1bf2 --- /dev/null +++ b/tests/unit/requirements/test_validation_candidate_marker.py @@ -0,0 +1,142 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from rdflib import RDF, BNode, Graph, Literal, Namespace, URIRef +from rdflib.namespace import SH + +from rocrate_validator.graph_transformers import ( + VALIDATION_CANDIDATE_PREDICATE, + prepare_data_graph, +) +from rocrate_validator.requirements.shacl import SHACLValidator + +SCHEMA = Namespace("http://schema.org/") +CSVW = Namespace("http://www.w3.org/ns/csvw#") +DCTERMS = Namespace("http://purl.org/dc/terms/") +EXAMPLE = Namespace("https://example.org/") + + +def _is_validation_candidate(graph: Graph, iri: URIRef) -> bool: + return (iri, VALIDATION_CANDIDATE_PREDICATE, Literal(True)) in graph + + +@pytest.mark.parametrize( + "predicate", + [ + RDF.type, + SCHEMA.propertyID, + CSVW.propertyUrl, + SCHEMA.inDefinedTermSet, + SCHEMA.encodingFormat, + DCTERMS.conformsTo, + ], +) +def test_reference_objects_are_marked_cited_only(predicate: URIRef): + data_graph = Graph() + term = EXAMPLE["term"] + data_graph.add((EXAMPLE.entity, predicate, term)) + + validation_graph = prepare_data_graph(data_graph) + + assert not _is_validation_candidate(validation_graph, term) + + +@pytest.mark.parametrize("predicate", [SCHEMA.author, SCHEMA.hasPart]) +def test_claimed_objects_remain_in_validation_scope(predicate: URIRef): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, predicate, EXAMPLE.claimed)) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, EXAMPLE.claimed) + + +def test_described_subject_remains_in_validation_scope(): + data_graph = Graph() + data_graph.add((EXAMPLE.described, RDF.type, SCHEMA.Person)) + data_graph.add((EXAMPLE.described, SCHEMA.name, Literal("A person"))) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, EXAMPLE.described) + assert not _is_validation_candidate(validation_graph, SCHEMA.Person) + + +def test_claimed_vocabulary_term_is_a_validation_candidate(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.author, EXAMPLE.ontology_term)) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, EXAMPLE.ontology_term) + + +def test_node_absent_from_original_graph_is_not_a_validation_candidate(): + data_graph = Graph() + + validation_graph = prepare_data_graph(data_graph) + + assert not _is_validation_candidate(validation_graph, EXAMPLE.ontology_term) + + +def test_preparation_does_not_mutate_input_graphs(): + data_graph = Graph() + term = EXAMPLE["term"] + data_graph.add((EXAMPLE.entity, SCHEMA.propertyID, term)) + original_data_triples = set(data_graph) + + validation_graph = prepare_data_graph(data_graph) + + assert validation_graph is not data_graph + assert set(data_graph) == original_data_triples + assert len(validation_graph) > len(data_graph) + + +@pytest.mark.parametrize("serialized", [False, True]) +def test_shacl_validator_prepares_graph_before_validation(serialized: bool): + shape = EXAMPLE.marker_shape + target = BNode() + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.target, target)) + shapes_graph.add( + ( + target, + SH.select, + Literal( + f"SELECT ?this WHERE {{ ?this a <{SCHEMA.Person}> . " + f"FILTER EXISTS {{ ?this <{VALIDATION_CANDIDATE_PREDICATE}> true }} }}" + ), + ) + ) + shapes_graph.add((target, RDF.type, SH.SPARQLTarget)) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.name)) + shapes_graph.add((property_shape, SH.minCount, Literal(1))) + shapes_graph.add((property_shape, SH.message, Literal("A person needs a name"))) + + person = EXAMPLE.person + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.author, person)) + data_graph.add((person, RDF.type, SCHEMA.Person)) + original_triples = set(data_graph) + + source = data_graph.serialize(format="turtle").encode() if serialized else data_graph + result = SHACLValidator(shapes_graph=shapes_graph).validate(source) + + assert not result.conforms + assert any(violation.focusNode == person for violation in result.violations) + assert set(data_graph) == original_triples From 3a8d0cf054e21ed2cef2863dca62496988aa12b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 20:12:42 +0200 Subject: [PATCH 065/271] feat(shacl): :sparkles: prepare transient graph before validation Run graph transformers before pySHACL while preserving the source graph. --- .../requirements/shacl/validator.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 74eb48cf0..746d51385 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any, cast # pylint: disable=unused-import import pyshacl +from pyshacl.rdfutil.load import load_from_source from rdflib import BNode, Graph, Literal, Namespace from rdflib.term import Node, URIRef @@ -33,6 +34,7 @@ VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES, ) +from rocrate_validator.graph_transformers import prepare_data_graph from rocrate_validator.models import ( Profile, RequirementCheck, @@ -471,6 +473,25 @@ def validate( if isinstance(self._shapes_graph, Graph): _inject_default_prefixes(self._shapes_graph) + # Classify nodes before pySHACL can add ontology or inferred triples. + # Keep the source graph untouched: prepare_data_graph creates the + # transient graph that pySHACL will validate. + if isinstance(data_graph, Graph): + data_graph = prepare_data_graph(cast("Graph", data_graph)) + elif isinstance(data_graph, (str, bytes)) and not kwargs.get("sparql_mode", False): + data_graph = prepare_data_graph( + cast( + "Graph", + load_from_source( + data_graph, + rdf_format=kwargs.get("data_graph_format"), + multigraph=True, + do_owl_imports=False, + logger=cast("Any", logger), + ), + ) + ) + # validate the data graph using pyshacl.validate try: conforms, results_graph, results_text = pyshacl.validate( From 782f74c4d9e90919412ffc41fa50d6b86b5505bc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 15 Sep 2026 20:17:16 +0200 Subject: [PATCH 066/271] docs(graph): :memo: document graph transformer extension Document transformer discovery, ordering, vocabulary, and testing guidance. --- docs/0_toc.rst | 1 + docs/11_writing_a_profile.rst | 2 + docs/14_graph_transformers.rst | 313 ++++++++++++++++++ .../1.2/must/6_contextual_entity_metadata.ttl | 2 +- 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 docs/14_graph_transformers.rst diff --git a/docs/0_toc.rst b/docs/0_toc.rst index f827b61d1..ffbaa0d22 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -30,6 +30,7 @@ 12_validation_profiles 11_writing_a_profile 13_optimizing_shacl_target_selection + 14_graph_transformers 10_api genindex diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 3a226eb43..b492e10a3 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -31,6 +31,8 @@ query language. You can learn about SPARQL in the tutorial For guidance on SHACL Core targets, SPARQL target performance, inference and rule ordering, see :ref:`efficient-shacl-targets`. +For the rare case where validation must preserve information from the original +graph before an ontology mix-in or inference, see :ref:`graph-transformers`. All these tools are best learned through practice and examples, so when building a profile, it's encouraged to use the `other profiles `_ diff --git a/docs/14_graph_transformers.rst b/docs/14_graph_transformers.rst new file mode 100644 index 000000000..e0fb38349 --- /dev/null +++ b/docs/14_graph_transformers.rst @@ -0,0 +1,313 @@ +.. + Copyright (c) 2024-2026 CRS4 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +.. _graph-transformers: + +Graph transformers +================== + +Graph transformers are an internal preprocessing extension point for cases in +which a SHACL shape needs information that is available in the original +RO-Crate graph but would be lost or become ambiguous after an ontology mix-in, +inference, or SHACL rule expansion. They can also add a reusable annotation +which would otherwise have to be recomputed by several expensive SPARQL +targets. + +Do not use a transformer for an ordinary validation condition. Prefer SHACL +Core, a SPARQL constraint, or a Python check whenever those can express the +requirement directly. A transformer changes the graph seen by every SHACL +shape, so its output is global to the validation run and requires broader +regression testing. + +Why a single SHACL phase is not enough +-------------------------------------- + +Consider a crate containing a ``schema:PropertyValue`` whose +``schema:propertyID`` points to an identifier scheme. The scheme IRI is a +reference, not an entity described by the crate. The following is a complete +minimal JSON-LD input. The namespace ``https://vocab.example.org/`` is +an arbitrary external vocabulary namespace. + +.. code-block:: json + + { + "@context": { + "schema": "http://schema.org/", + "id": "@id", + "type": "@type", + "about": { "@id": "schema:about", "@type": "@id" }, + "identifier": { "@id": "schema:identifier", "@type": "@id" }, + "name": "schema:name", + "propertyID": { "@id": "schema:propertyID", "@type": "@id" }, + "value": "schema:value" + }, + "@graph": [ + { + "id": "ro-crate-metadata.json", + "type": "schema:CreativeWork", + "about": { "id": "./" } + }, + { + "id": "./", + "type": "schema:Dataset", + "name": "Example crate", + "identifier": { "id": "#identifier" } + }, + { + "id": "#identifier", + "type": "schema:PropertyValue", + "propertyID": { + "id": "https://vocab.example.org/identifier-scheme" + }, + "value": "ORCID" + } + ] + } + +After JSON-LD expansion, but **before an ontology mix-in, inference, or SHACL +rule expansion**, the relevant part of the RDF graph is: + +.. code-block:: turtle + + @prefix rdf: . + @prefix schema: . + @prefix vocab: . + + + rdf:type schema:CreativeWork ; + schema:about <./> . + + <./> + rdf:type schema:Dataset ; + schema:name "Example crate" ; + schema:identifier <#identifier> . + + <#identifier> + rdf:type schema:PropertyValue ; + schema:propertyID vocab:identifier-scheme ; + schema:value "ORCID" . + +At this point ``vocab:identifier-scheme`` occurs only as the object of +``schema:propertyID``. It has no outgoing assertion and is therefore a +cited-only reference. In particular, it is not yet a subject selected by a +target of the form ``?this a ?type``. + +A single-phase SHACL implementation can try to classify nodes from the triples +visible at the time of validation. In pySHACL, a ``CONSTRUCT`` query is used as +the body of a ``sh:SPARQLRule`` through ``sh:construct``; it is not the query +form of a ``sh:SPARQLTarget``. The following is a complete shapes-graph +fragment. Its target is deliberately selected only after the ontology makes the +term an ``owl:Class`` instance: + +.. code-block:: turtle + + @prefix ex: . + @prefix owl: . + @prefix rdf: . + @prefix sh: . + + ex:CandidateMarkerShape + a sh:NodeShape ; + sh:targetClass owl:Class ; + sh:rule [ + a sh:SPARQLRule ; + sh:construct """ + CONSTRUCT { + $this ex:validationCandidate true + } + WHERE { + $this ?predicate ?object . + FILTER(?predicate != rdf:type) + } + """ + ] . + +This is the same mechanism described in the `pySHACL Inference and Rules +documentation `_ and in +the normative `SHACL-AF SPARQL Rules specification +`_. It is enabled in pySHACL with +``advanced=True`` (or can be run explicitly with ``shacl_rules(...)``). + +On the original Turtle graph, this rule cannot classify +``vocab:identifier-scheme`` from its outgoing assertions because the IRI does +not occur as a subject. The distinction is available at this stage: the IRI is +only the object of ``schema:propertyID``. + +Now suppose the validator supplies the following ontology graph, or enables +inference which produces equivalent additional triples: + +.. code-block:: turtle + + @prefix owl: . + @prefix rdfs: . + @prefix vocab: . + + vocab:identifier-scheme + a owl:Class ; + rdfs:label "Identifier scheme" . + +The ontology mix-in adds these ontology triples to the data graph before pySHACL +applies SHACL rules. With OWL-RL inference, further triples such as +the following can also be present: + +.. code-block:: turtle + + vocab:identifier-scheme + + vocab:identifier-scheme . + +The term is now a subject with a type and with outgoing predicates that were +not asserted by the crate. The same single-phase rule now sees the injected +``rdfs:label`` or inferred ``owl:sameAs`` and marks +``vocab:identifier-scheme`` as a validation candidate. It cannot distinguish +those generated statements from assertions in the crate. The same problem can +occur without an ontology when inference or an earlier SHACL rule expands the +graph. + +Changing ``sh:order`` does not solve this problem. SHACL rule ordering applies +after the ontology mix-in and pre-inference, so it cannot restore the +provenance that was present in the first Turtle graph. The required operation +is two-phase: classify nodes using the original graph, then validate a copy of +that graph after normal ontology and inference expansion. The graph transformer +pipeline implements that separation; the private candidate marker added to +the transient copy is not inferred from ontology triples. + +How the pipeline works +---------------------- + +``prepare_data_graph`` creates one transient copy of the parsed RO-Crate graph, +discovers all modules below ``rocrate_validator.graph_transformers``, and runs +their registered transformers sequentially. The cached source graph is never +passed to a transformer and remains unchanged. + +The validator skips this preprocessing when pySHACL is called with +``sparql_mode=True``. Transformer-dependent validation therefore requires the +normal in-memory or serialized-graph validation mode. + +Lower ``order`` values run first. Transformers with the same order are sorted +by their fully qualified Python name, so their order is deterministic but +should not be used to encode an implicit dependency. Assign distinct orders +when one transformer consumes triples produced by another. + +Developing transformers +----------------------- + +The following sections describe the contract and development workflow for +adding a transformer to the bundled package. + +Transformer contract +~~~~~~~~~~~~~~~~~~~~ + +Every transformer must: + +* accept exactly one ``rdflib.Graph``; +* return an ``rdflib.Graph`` (returning ``None`` is an error); +* be deterministic for the same input graph; +* avoid network access and process-global mutable state; +* treat the graph as transient validator state, never as metadata to write back + to the RO-Crate. + +The discovery mechanism covers modules bundled in the +``rocrate_validator.graph_transformers`` package. It is not an entry-point +system for arbitrary profile-local or third-party plugins. + +Function transformers +~~~~~~~~~~~~~~~~~~~~~ + +Use ``@graph_transformer`` for a stateless transformation that is naturally +expressed as a function: + +.. code-block:: python + + from rdflib import Graph + + from rocrate_validator.graph_transformers import graph_transformer + + + @graph_transformer(order=50) + def add_example_annotations(data_graph: Graph) -> Graph: + # Add annotations to the transient graph. + ... + return data_graph + +The module does not need to be imported manually. It is imported by discovery +the first time ``prepare_data_graph`` runs. + +Class transformers +~~~~~~~~~~~~~~~~~~ + +Use a ``GraphTransformer`` subclass when the implementation benefits from +private helper methods or a clearer class-level contract: + +.. code-block:: python + + from rdflib import Graph + + from rocrate_validator.graph_transformers import GraphTransformer + + + class ExampleTransformer(GraphTransformer): + order = 60 + + def transform(self, data_graph: Graph) -> Graph: + ... + return data_graph + +Every concrete subclass is registered automatically. It must have a no-argument +constructor; a new instance is created for each pipeline execution so state +does not leak between validations. Abstract intermediate subclasses are not +registered. + +Private marker predicates and prefixes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A transformer may annotate the transient graph with private predicates. Keep +these predicates in the validator-owned namespace +``https://github.com/crs4/rocrate-validator/graph-transformers/`` and define +their RDFLib vocabulary terms in ``graph_transformers/vocabulary.py``. These triples +are implementation details: they must not be serialized into an RO-Crate or +presented as vocabulary terms owned by the crate. + +When a SHACL query consumes a marker, declare the namespace in the profile's +shared ``sh:prefixes`` block. For example: + +.. code-block:: turtle + + sh:declare [ + sh:prefix "validator-transformers" ; + sh:namespace + "https://github.com/crs4/rocrate-validator/graph-transformers/"^^xsd:anyURI ; + ] . + +The query can then select only nodes annotated before pySHACL expands the +graph: + +.. code-block:: sparql + + FILTER EXISTS { + ?this validator-transformers:validationCandidate true + } + +Testing a transformer +~~~~~~~~~~~~~~~~~~~~~ + +Add unit tests for the transformation itself and integration tests for every +SHACL target which consumes its output. At minimum, verify that: + +* the source graph is unchanged; +* consecutive transformers see their predecessors' output; +* ordering is explicit when transformers depend on each other; +* ontology and inference triples do not accidentally enter the intended scope; +* representative large graphs remain within the project's performance budget. diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl index e6fe3176f..0fce7655e 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl @@ -30,7 +30,7 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; CreativeWorkAuthorDefinition) tag referenced-but-undescribed entities with rdf:type, so they appear as subjects and are therefore captured. Vocabulary terms loaded from ontologies are excluded using provenance - captured from the original crate graph before ontology inoculation.""" ; + captured from the original crate graph before the ontology mix-in.""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; From 7d671572e6b8059920cb2729b069cec14caf2d21 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 16 Sep 2026 10:09:32 +0200 Subject: [PATCH 067/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.11.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd550dcfd..8fca8a775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.11.4] - 2026-09-16 + +Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.3...0.11.4 + +### ✨ Added + +- feat(rocrate): add validation for singleton arrays in compacted entity properties ([9d8078c7](https://github.com/crs4/rocrate-validator/commit/9d8078c75e7482c78928d9929784e6d3c6e6f71f), [675cdb27](https://github.com/crs4/rocrate-validator/commit/675cdb2788c57bc4b4216642cb146346730d8a5a), [2aa6bcba](https://github.com/crs4/rocrate-validator/commit/2aa6bcba5886e36ab92e0f5c505af479eea7e414)) + +### 🔧 Changed + +- refactor(metadata): expose raw metadata descriptor bytes for encoding checks ([e18e24ef](https://github.com/crs4/rocrate-validator/commit/e18e24ef224493dbfe6acb73a3e494b3351bcf85)) +- refactor(versioning): use a shared distribution name when resolving the package version ([e57e2ea7](https://github.com/crs4/rocrate-validator/commit/e57e2ea701651f748aa6bf139158ac0bb051493f)) + +### 🐛 Fixed + +- fix(validation): skip metadata-dependent profile checks when descriptors are missing or unreadable instead of reporting misleading errors ([b302b5cd](https://github.com/crs4/rocrate-validator/commit/b302b5cdd1dd25678e5bbc3d1a723f2ce87e7c8a), [2444e97a](https://github.com/crs4/rocrate-validator/commit/2444e97ad3d38ae41c5d3fcd8b11cfa5ee13c9ec)) +- fix(metadata): handle missing metadata descriptors without leaking `FileNotFoundError` ([e1217fda](https://github.com/crs4/rocrate-validator/commit/e1217fda31b7e8c806202350f8c17150735813d5)) +- fix(profile): skip descriptor encoding validation in metadata-only mode and narrow exception handling ([acb715bf](https://github.com/crs4/rocrate-validator/commit/acb715bfc9c5ae3f27f399aae8e429660a311ebe), [9411c86c](https://github.com/crs4/rocrate-validator/commit/9411c86c195b6aad8bdf1b4bd13738ec9995f580)) +- fix(rocrate): normalize singleton-array `mainEntity` values in workflow crates ([568b8c41](https://github.com/crs4/rocrate-validator/commit/568b8c414541de31410c513901573cc617fa2aec)) +- fix(profiles): use the canonical namespace for the RO-Crate 1.2 ontology ([d9e115cb](https://github.com/crs4/rocrate-validator/commit/d9e115cb71ee11bff7422b68ea6b6351b432360c)) +- fix(versioning): resolve the package version when installed outside a Git checkout ([856546a4](https://github.com/crs4/rocrate-validator/commit/856546a49d61771c478b0a232bc12501313a46e0)) + +### 📚 Documentation + +- docs(profiles): document SHACL target selection optimization ([14058e67](https://github.com/crs4/rocrate-validator/commit/14058e67fd96043ed5298c91c6396293e0a1d484)) + +### ⚡ Performance + +- perf(profiles): replace custom SPARQL targets with SHACL Core targets ([bb4d0317](https://github.com/crs4/rocrate-validator/commit/bb4d0317ef28c91470b9da46f5048d5d940f49aa)) +- perf(profiles): reduce duplicate and cross-product work in contextual-entity target queries ([be6aa6ff](https://github.com/crs4/rocrate-validator/commit/be6aa6ffe98cb55869c6e9b57634d08e1cc53f0d), [88ded678](https://github.com/crs4/rocrate-validator/commit/88ded678cd0009df06209d8a48bbc7346ae5fbc6)) + ## [0.11.3] - 2026-07-28 Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.2...0.11.3 From a8a2346b843afdebdc938287b70c215b057c94d9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 16 Sep 2026 10:15:56 +0200 Subject: [PATCH 068/271] =?UTF-8?q?chore(deps):=20=F0=9F=94=A7=20update=20?= =?UTF-8?q?dependencies=20in=20poetry.lock=20(v0.11.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 2144 +++++++++++++++++++++++++++------------------------ 1 file changed, 1148 insertions(+), 996 deletions(-) diff --git a/poetry.lock b/poetry.lock index eefd19f98..e8302f3ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -52,58 +52,82 @@ idna = ">=2.5" [[package]] name = "appnope" -version = "0.1.4" +version = "1.0.0" description = "Disable App Nap on macOS >= 10.9" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["dev"] markers = "platform_system == \"Darwin\"" files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, + {file = "appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e"}, + {file = "appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49"}, ] [[package]] name = "ast-serialize" -version = "0.5.0" +version = "0.11.2" description = "Python bindings for mypy AST serialization" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb"}, - {file = "ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101"}, - {file = "ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642"}, - {file = "ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6"}, + {file = "ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d"}, + {file = "ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1"}, + {file = "ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4"}, + {file = "ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27"}, + {file = "ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450"}, ] [[package]] @@ -123,14 +147,14 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "asttokens" -version = "3.0.1" +version = "3.0.2" description = "Annotate AST trees with source code positions" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a"}, - {file = "asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7"}, + {file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"}, + {file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"}, ] [package.extras] @@ -245,14 +269,14 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "cattrs" -version = "26.1.0" +version = "26.2.0" description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096"}, - {file = "cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40"}, + {file = "cattrs-26.2.0-py3-none-any.whl", hash = "sha256:c680f17cd1df3a1c038ad1db8d90a9c595568cc54c4e8098e7ceb8d7b9bd826b"}, + {file = "cattrs-26.2.0.tar.gz", hash = "sha256:3cf49f69df8326bcf17a3cb3d3d3ec4a856858fe3a7473746c9044c317d3ba55"}, ] [package.dependencies] @@ -264,7 +288,7 @@ typing-extensions = ">=4.14.0" bson = ["pymongo (>=4.4.0)"] cbor2 = ["cbor2 (>=5.4.6)"] msgpack = ["msgpack (>=1.0.5)"] -msgspec = ["msgspec (>=0.19.0) ; implementation_name == \"cpython\""] +msgspec = ["msgspec (>=0.21.1) ; implementation_name == \"cpython\""] orjson = ["orjson (>=3.11.3) ; implementation_name == \"cpython\""] pyyaml = ["pyyaml (>=6.0)"] tomlkit = ["tomlkit (>=0.11.8)"] @@ -273,109 +297,125 @@ ujson = ["ujson (>=5.10.0)"] [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, - {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev", "docs"] markers = "implementation_name == \"pypy\"" files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, ] [package.dependencies] @@ -395,158 +435,198 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.7" +version = "3.5.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, - {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, - {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, + {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, + {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, ] [[package]] name = "click" -version = "8.4.1" +version = "8.5.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, - {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -562,14 +642,14 @@ markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", [[package]] name = "colorlog" -version = "6.10.1" +version = "6.12.0" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"}, - {file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"}, + {file = "colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e"}, + {file = "colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f"}, ] [package.dependencies] @@ -595,103 +675,133 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.14.3" +version = "7.16.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "coverage-7.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:360bec1f58e7243e3405d3bdf7a1a8115aa9b448d54dc7cd6f7b7e0e9406b62e"}, - {file = "coverage-7.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed68faa5e85de2f3e400bc3f122e5c82735a58c8bb24b9f63a2215954ba17b2d"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:830c1fca669c572dec37ce9c838224ee45aac5be0f6961edf871e82e49d6537c"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a64caee2193563601dbaaa55fe2dcf597debef04a2f8f1fa8a07aa4bb7ac7a1e"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0096fd7559178f0cc9cf088f2dbd2a02ef85bacaa69732c633517286b4494610"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6197e5a00183c11a8ce7c6abd18be1a9189fd8399084ffc95196f4f0db4f2137"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7dfe427045520d6abca33687dfef767b4f635015893a1816c5decb12eb72ce18"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a3f142070eb7b82fc4085a55d887396f9c4e21250bccebe2ba22502c45b9647"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64b2055bb6e0dc945af35cdeceb3633e6ed9273475ef3af85592410fd6803803"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1551b4caac3e3ec9f2bfcec6bf3776e01c0edbdd2e240431a50ca1a1aac72c27"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:583d50d59142f8549470bd6390471d0fe8b8c8d69d6a0f28ac71e05380cef640"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0bb8a6bc7015efdf8a928753b25da1b9ca2d6f24ef04d2ee0688e486f32aae7"}, - {file = "coverage-7.14.3-cp310-cp310-win32.whl", hash = "sha256:d48400185564042287dc487c1f016a3397f18ab4f4c5d5ec36edc218f7ffa35b"}, - {file = "coverage-7.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:eadea7aba74e40adee867a8c0eec17b820b061d308a4b014f7a0e118c2b0aa61"}, - {file = "coverage-7.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3"}, - {file = "coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc"}, - {file = "coverage-7.14.3-cp311-cp311-win32.whl", hash = "sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8"}, - {file = "coverage-7.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2"}, - {file = "coverage-7.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4"}, - {file = "coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24"}, - {file = "coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd"}, - {file = "coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35"}, - {file = "coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d"}, - {file = "coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336"}, - {file = "coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c"}, - {file = "coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0"}, - {file = "coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37"}, - {file = "coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994"}, - {file = "coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150"}, - {file = "coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc"}, - {file = "coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b"}, - {file = "coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965"}, - {file = "coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3"}, - {file = "coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92"}, - {file = "coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949"}, - {file = "coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635"}, - {file = "coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc"}, - {file = "coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda"}, - {file = "coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f"}, - {file = "coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8"}, - {file = "coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f"}, + {file = "coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853"}, + {file = "coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773"}, + {file = "coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc"}, + {file = "coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90"}, + {file = "coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da"}, + {file = "coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572"}, + {file = "coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2"}, + {file = "coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7"}, + {file = "coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22"}, + {file = "coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af"}, + {file = "coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0"}, + {file = "coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d"}, + {file = "coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e"}, + {file = "coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8"}, + {file = "coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9"}, + {file = "coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0"}, + {file = "coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a"}, + {file = "coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa"}, + {file = "coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d"}, + {file = "coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15"}, + {file = "coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361"}, + {file = "coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b"}, + {file = "coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b"}, + {file = "coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f"}, + {file = "coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152"}, + {file = "coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae"}, + {file = "coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2"}, + {file = "coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d"}, + {file = "coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6"}, + {file = "coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743"}, + {file = "coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040"}, + {file = "coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827"}, + {file = "coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d"}, + {file = "coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855"}, + {file = "coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6"}, + {file = "coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0"}, + {file = "coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5"}, + {file = "coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db"}, + {file = "coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0"}, + {file = "coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4"}, + {file = "coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210"}, ] [package.dependencies] @@ -702,42 +812,42 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "debugpy" -version = "1.8.21" +version = "1.8.22" description = "An implementation of the Debug Adapter Protocol for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "debugpy-1.8.21-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9"}, - {file = "debugpy-1.8.21-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344"}, - {file = "debugpy-1.8.21-cp310-cp310-win32.whl", hash = "sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73"}, - {file = "debugpy-1.8.21-cp310-cp310-win_amd64.whl", hash = "sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5"}, - {file = "debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264"}, - {file = "debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc"}, - {file = "debugpy-1.8.21-cp311-cp311-win32.whl", hash = "sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e"}, - {file = "debugpy-1.8.21-cp311-cp311-win_amd64.whl", hash = "sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7"}, - {file = "debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e"}, - {file = "debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176"}, - {file = "debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9"}, - {file = "debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c"}, - {file = "debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88"}, - {file = "debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2"}, - {file = "debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1"}, - {file = "debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0"}, - {file = "debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782"}, - {file = "debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e"}, - {file = "debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c"}, - {file = "debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8"}, - {file = "debugpy-1.8.21-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:0042da0ecd0a8b50dc4a54395ecd870d258d73fa18776f50c91fdcabdcad2675"}, - {file = "debugpy-1.8.21-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:ffd932c6796afadab6993ec96745918a8cb2444dbd392074f769db5ea40ab440"}, - {file = "debugpy-1.8.21-cp38-cp38-win32.whl", hash = "sha256:4e7c2d784d78ad4b71a5f8cd7b59c167719ec8a7a0211dbb3eb1bfeda78bc4e2"}, - {file = "debugpy-1.8.21-cp38-cp38-win_amd64.whl", hash = "sha256:aa9d941d6dfe3d0407e4b3ca0b9ec466030e260fbf1174094f68785680f66db6"}, - {file = "debugpy-1.8.21-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:9f5171176a0084b95d2ebe55a4d1f7b2a75b74c5dbec577ebd3a85c740551c36"}, - {file = "debugpy-1.8.21-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:f15c10084f9861b5e8414a48f18f8e4aadf51a98a59e72c16aa28281ca994672"}, - {file = "debugpy-1.8.21-cp39-cp39-win32.whl", hash = "sha256:4e70cc8b5079f885cb43910924ee0aab73b8b6b2a14eff23afdd9895d86e79eb"}, - {file = "debugpy-1.8.21-cp39-cp39-win_amd64.whl", hash = "sha256:e935f9dc0501be523c8a8e1853c39432e1354e9ece717ae5998fd2371c4542c3"}, - {file = "debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92"}, - {file = "debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6"}, + {file = "debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735"}, + {file = "debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee"}, + {file = "debugpy-1.8.22-cp310-cp310-win32.whl", hash = "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f"}, + {file = "debugpy-1.8.22-cp310-cp310-win_amd64.whl", hash = "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c"}, + {file = "debugpy-1.8.22-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c"}, + {file = "debugpy-1.8.22-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35"}, + {file = "debugpy-1.8.22-cp311-cp311-win32.whl", hash = "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a"}, + {file = "debugpy-1.8.22-cp311-cp311-win_amd64.whl", hash = "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3"}, + {file = "debugpy-1.8.22-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0"}, + {file = "debugpy-1.8.22-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb"}, + {file = "debugpy-1.8.22-cp312-cp312-win32.whl", hash = "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b"}, + {file = "debugpy-1.8.22-cp312-cp312-win_amd64.whl", hash = "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3"}, + {file = "debugpy-1.8.22-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54"}, + {file = "debugpy-1.8.22-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39"}, + {file = "debugpy-1.8.22-cp313-cp313-win32.whl", hash = "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337"}, + {file = "debugpy-1.8.22-cp313-cp313-win_amd64.whl", hash = "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2"}, + {file = "debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d"}, + {file = "debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9"}, + {file = "debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294"}, + {file = "debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e"}, + {file = "debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07"}, + {file = "debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5"}, + {file = "debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0"}, + {file = "debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a"}, + {file = "debugpy-1.8.22-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:cba7b99573ee41f9510c6deeff1cdd0c333748b9dd63d5016413dd57ba9a4593"}, + {file = "debugpy-1.8.22-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:3b7c328cb47b4e2f2b40801936daf8150f88dcb4ffef5080ff119270f0d23626"}, + {file = "debugpy-1.8.22-cp39-cp39-win32.whl", hash = "sha256:ffeeaf4dfe1534375902040df32a4c48f721fd35a4d4d337ac72a44d4176075b"}, + {file = "debugpy-1.8.22-cp39-cp39-win_amd64.whl", hash = "sha256:92fc425308a08f601f3c69afb4719767f9b898276f44b97e6942a713c4c32740"}, + {file = "debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947"}, + {file = "debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e"}, ] [[package]] @@ -747,6 +857,7 @@ description = "Decorators for Humans" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c"}, {file = "decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82"}, @@ -911,14 +1022,14 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastjsonschema" -version = "2.21.2" +version = "2.22.2" description = "Fastest Python implementation of JSON schema" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, - {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, + {file = "fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4"}, + {file = "fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf"}, ] [package.extras] @@ -926,14 +1037,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.29.4" +version = "3.32.7" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767"}, - {file = "filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a"}, + {file = "filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52"}, + {file = "filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7"}, ] [[package]] @@ -987,56 +1098,42 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.18" +version = "3.19" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main", "docs"] files = [ - {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, - {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, + {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, + {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, ] [package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] [[package]] name = "imagesize" -version = "1.5.0" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -groups = ["docs"] -markers = "python_version >= \"3.15\"" -files = [ - {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, - {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, -] - -[[package]] -name = "imagesize" -version = "2.0.0" +version = "2.0.1" description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)" optional = false -python-versions = "<3.15,>=3.10" +python-versions = ">=3.10" groups = ["docs"] -markers = "python_version < \"3.15\"" files = [ - {file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"}, - {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}, + {file = "imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02"}, + {file = "imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5"}, ] [[package]] name = "importlib-metadata" -version = "9.0.0" +version = "9.0.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.10" groups = ["main"] markers = "python_version < \"3.12\"" files = [ - {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, - {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, + {file = "importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0"}, + {file = "importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99"}, ] [package.dependencies] @@ -1048,7 +1145,7 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.17)"] type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] @@ -1080,7 +1177,7 @@ pfzy = ">=0.3.1,<0.4.0" prompt-toolkit = ">=3.0.1,<4.0.0" [package.extras] -docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] +docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] [[package]] name = "ipykernel" @@ -1158,33 +1255,32 @@ test-extra = ["curio", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "n [[package]] name = "ipython" -version = "9.14.1" +version = "9.17.1" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.11" groups = ["dev"] markers = "python_version >= \"3.11\"" files = [ - {file = "ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c"}, - {file = "ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b"}, + {file = "ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509"}, + {file = "ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529"}, ] [package.dependencies] colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""} -decorator = ">=5.1.0" ipython-pygments-lexers = ">=1.0.0" jedi = ">=0.18.2" matplotlib-inline = ">=0.1.6" pexpect = {version = ">4.6", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} prompt_toolkit = ">=3.0.41,<3.1.0" -psutil = {version = ">=7", markers = "sys_platform != \"emscripten\""} +psutil = {version = ">=7", markers = "sys_platform != \"emscripten\" and sys_platform != \"cygwin\""} pygments = ">=2.14.0" stack_data = ">=0.6.0" traitlets = ">=5.13.0" typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] -all = ["argcomplete (>=3.0)", "ipython[doc,matplotlib,terminal,test,test-extra]", "types-decorator"] +all = ["argcomplete (>=3.0)", "ipython[doc,matplotlib,test,test-extra]"] black = ["black"] doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[matplotlib,test]", "setuptools (>=80.0)", "sphinx (>=8.0)", "sphinx-rtd-theme (>=0.1.8)", "sphinx_toml (==0.0.4)", "typing_extensions"] matplotlib = ["matplotlib (>3.9)"] @@ -1287,7 +1383,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.25.0" @@ -1312,14 +1408,14 @@ referencing = ">=0.31.0" [[package]] name = "jupyter-client" -version = "8.9.1" +version = "8.10.0" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81"}, - {file = "jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa"}, + {file = "jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e"}, + {file = "jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497"}, ] [package.dependencies] @@ -1369,103 +1465,150 @@ files = [ [[package]] name = "librt" -version = "0.11.0" +version = "0.15.0" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, - {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, - {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, - {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, - {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, - {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, - {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, - {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, - {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, - {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, - {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, - {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, - {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, - {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, - {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, - {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, - {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, - {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, - {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, - {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, - {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, - {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, - {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, - {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, - {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, - {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, - {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, - {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, - {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, - {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, - {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, - {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, - {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, - {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, - {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, - {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, - {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, + {file = "librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489"}, + {file = "librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702"}, + {file = "librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c"}, + {file = "librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e"}, + {file = "librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053"}, + {file = "librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22"}, + {file = "librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a"}, + {file = "librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0"}, + {file = "librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb"}, + {file = "librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db"}, + {file = "librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56"}, + {file = "librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e"}, + {file = "librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2"}, + {file = "librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1"}, + {file = "librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022"}, + {file = "librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570"}, + {file = "librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26"}, + {file = "librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2"}, + {file = "librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b"}, + {file = "librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab"}, + {file = "librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890"}, + {file = "librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8"}, + {file = "librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad"}, + {file = "librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993"}, + {file = "librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879"}, + {file = "librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65"}, + {file = "librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622"}, + {file = "librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15"}, + {file = "librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28"}, + {file = "librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc"}, + {file = "librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf"}, + {file = "librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915"}, + {file = "librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605"}, + {file = "librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca"}, + {file = "librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0"}, + {file = "librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d"}, + {file = "librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374"}, + {file = "librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9"}, + {file = "librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8"}, + {file = "librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b"}, + {file = "librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162"}, + {file = "librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1"}, + {file = "librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc"}, + {file = "librt-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d"}, + {file = "librt-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55"}, + {file = "librt-0.15.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb"}, + {file = "librt-0.15.0-cp39-cp39-win32.whl", hash = "sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455"}, + {file = "librt-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578"}, + {file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"}, ] [[package]] @@ -1656,14 +1799,14 @@ files = [ [[package]] name = "mistune" -version = "3.3.2" +version = "3.3.4" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "mistune-3.3.2-py3-none-any.whl", hash = "sha256:a678a56387d487db7368ede4647cb2ba1deff22ce61f92343e4ebe0ddfce4f2d"}, - {file = "mistune-3.3.2.tar.gz", hash = "sha256:e12ee4f1e74336e91aa1141e35f913b337c40bdf7c0cc49f21fb853a27e8b62f"}, + {file = "mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a"}, + {file = "mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe"}, ] [package.dependencies] @@ -1671,137 +1814,167 @@ typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "msgpack" -version = "1.2.1" +version = "1.2.2" description = "MessagePack serializer" optional = false python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, - {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, - {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, - {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, - {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, - {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, - {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, - {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, - {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, - {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, - {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, - {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, - {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, - {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, - {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, - {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, - {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, - {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, + {file = "msgpack-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970"}, + {file = "msgpack-1.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648"}, + {file = "msgpack-1.2.2-cp310-cp310-win32.whl", hash = "sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563"}, + {file = "msgpack-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec"}, + {file = "msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e"}, + {file = "msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f"}, + {file = "msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b"}, + {file = "msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c"}, + {file = "msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35"}, + {file = "msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc"}, + {file = "msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3"}, + {file = "msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4"}, + {file = "msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3"}, + {file = "msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640"}, + {file = "msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533"}, + {file = "msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab"}, + {file = "msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd"}, + {file = "msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f"}, + {file = "msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896"}, + {file = "msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04"}, + {file = "msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54"}, + {file = "msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248"}, + {file = "msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc"}, + {file = "msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8"}, + {file = "msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650"}, + {file = "msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7"}, + {file = "msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e"}, + {file = "msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36"}, + {file = "msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f"}, + {file = "msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5"}, + {file = "msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b"}, + {file = "msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf"}, + {file = "msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff"}, + {file = "msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808"}, + {file = "msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8"}, + {file = "msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212"}, + {file = "msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc"}, + {file = "msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d"}, + {file = "msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754"}, + {file = "msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a"}, ] [[package]] name = "mypy" -version = "2.1.0" +version = "2.3.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc"}, - {file = "mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849"}, - {file = "mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd"}, - {file = "mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166"}, - {file = "mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8"}, - {file = "mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8"}, - {file = "mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e"}, - {file = "mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41"}, - {file = "mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca"}, - {file = "mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538"}, - {file = "mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398"}, - {file = "mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563"}, - {file = "mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389"}, - {file = "mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666"}, - {file = "mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af"}, - {file = "mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6"}, - {file = "mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211"}, - {file = "mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b"}, - {file = "mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22"}, - {file = "mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b"}, - {file = "mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8"}, - {file = "mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5"}, - {file = "mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e"}, - {file = "mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e"}, - {file = "mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285"}, - {file = "mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5"}, - {file = "mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65"}, - {file = "mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d"}, - {file = "mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2"}, - {file = "mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f"}, - {file = "mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4"}, - {file = "mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef"}, - {file = "mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135"}, - {file = "mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21"}, - {file = "mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57"}, - {file = "mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e"}, - {file = "mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780"}, - {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd"}, - {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08"}, - {file = "mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081"}, - {file = "mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7"}, - {file = "mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6"}, - {file = "mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289"}, - {file = "mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633"}, -] - -[package.dependencies] -ast-serialize = ">=0.3.0,<1.0.0" -librt = {version = ">=0.11.0", markers = "platform_python_implementation != \"PyPy\""} + {file = "mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167"}, + {file = "mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13"}, + {file = "mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53"}, + {file = "mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90"}, + {file = "mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8"}, + {file = "mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01"}, + {file = "mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1"}, + {file = "mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6"}, + {file = "mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080"}, + {file = "mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355"}, + {file = "mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb"}, + {file = "mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b"}, + {file = "mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226"}, + {file = "mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74"}, + {file = "mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6"}, + {file = "mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac"}, + {file = "mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f"}, + {file = "mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82"}, + {file = "mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9"}, + {file = "mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d"}, + {file = "mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595"}, + {file = "mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045"}, + {file = "mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0"}, + {file = "mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63"}, + {file = "mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4"}, + {file = "mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561"}, + {file = "mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133"}, + {file = "mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9"}, + {file = "mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3"}, + {file = "mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523"}, + {file = "mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e"}, + {file = "mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc"}, + {file = "mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4"}, + {file = "mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29"}, + {file = "mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb"}, + {file = "mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419"}, +] + +[package.dependencies] +ast-serialize = ">=0.6.0,<1.0.0" +librt = {version = ">=0.13.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -1934,14 +2107,14 @@ webpdf = ["playwright"] [[package]] name = "nbformat" -version = "5.10.4" +version = "5.11.1" description = "The Jupyter Notebook format" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, + {file = "nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec"}, + {file = "nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d"}, ] [package.dependencies] @@ -1951,8 +2124,8 @@ jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" traitlets = ">=5.1" [package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["packaging", "pre-commit", "pytest", "testpath"] [[package]] name = "nbsphinx" @@ -2000,29 +2173,32 @@ files = [ [[package]] name = "owlrl" -version = "7.1.4" +version = "7.6.2" description = "A simple implementation of the OWL2 RL Profile, as well as a basic RDFS inference, on top of RDFLib. Based mechanical forward chaining." optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "owlrl-7.1.4-py3-none-any.whl", hash = "sha256:e78b46020169783345636da93a467d318f18700c483184dd15e885850cf64775"}, - {file = "owlrl-7.1.4.tar.gz", hash = "sha256:60bd4067e346b9111f0a2924565afe97ac6595b98b2bbe953928b5113971daf7"}, + {file = "owlrl-7.6.2-py3-none-any.whl", hash = "sha256:83347bf7f133979e87b2b18695d51d25510b99cec3f6919b5df05d4fbf058ae0"}, + {file = "owlrl-7.6.2.tar.gz", hash = "sha256:c743f35c2d908396e77823852bb1ebbce88340cd49961493983bec42c93283a8"}, ] [package.dependencies] -rdflib = ">=7.1.4" +rdflib = ">=7.6.0" + +[package.extras] +oxigraph = ["pyoxigraph (>=0.5.6)"] [[package]] name = "packaging" -version = "26.2" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev", "docs", "test"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -2099,18 +2275,18 @@ files = [ ] [package.extras] -docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] +docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.11.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs"] files = [ - {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, - {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, + {file = "platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081"}, + {file = "platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc"}, ] [[package]] @@ -2131,14 +2307,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b"}, - {file = "pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9"}, + {file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"}, + {file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"}, ] [package.dependencies] @@ -2168,18 +2344,18 @@ tests = ["pytest (>=9)", "pytest-cov", "pytest-lazy-fixtures"] [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, - {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, + {file = "prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2"}, + {file = "prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6"}, ] [package.dependencies] -wcwidth = "*" +wcwidth = ">=0.1.4" [[package]] name = "psutil" @@ -2231,14 +2407,14 @@ files = [ [[package]] name = "pure-eval" -version = "0.2.3" +version = "0.2.4" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, + {file = "pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a"}, + {file = "pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1"}, ] [package.extras] @@ -2259,14 +2435,14 @@ files = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs", "test"] files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [package.extras] @@ -2289,8 +2465,8 @@ astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=4.2.5,<5.13 || >5.13,<7" mccabe = ">=0.6,<0.8" @@ -2319,32 +2495,33 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyshacl" -version = "0.31.0" +version = "0.40.1" description = "Python SHACL Validator" optional = false -python-versions = "<4,>=3.9" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyshacl-0.31.0-py3-none-any.whl", hash = "sha256:5cae2184401d956b67deebb00e3c78ab7052784741a730e52e309e33c8a0b9a5"}, - {file = "pyshacl-0.31.0.tar.gz", hash = "sha256:327950875a5bb0d1a15c246a8a272b2dbf6bc9b96e28cfa8fdbfa4d73aadc0ba"}, + {file = "pyshacl-0.40.1-py3-none-any.whl", hash = "sha256:27dd58c8ddfa103303b4a8c40b2c666332ffc912dbcd3137f7adc7b7bc5e6bda"}, + {file = "pyshacl-0.40.1.tar.gz", hash = "sha256:011e3cf1a68b31747cb762ba3d755ae1bdcc464c8fad0dc212a9adc550719552"}, ] [package.dependencies] importlib-metadata = {version = ">6", markers = "python_version < \"3.12\""} -owlrl = ">=7.1.2,<8" +owlrl = ">=7.6.2,<8" packaging = ">=21.3" prettytable = [ {version = ">=3.5.0", markers = "python_version < \"3.12\""}, {version = ">=3.7.0", markers = "python_version >= \"3.12\""}, ] -rdflib = {version = ">=7.1.1,<7.1.2 || >7.1.2,<8.0", extras = ["html"]} +rdflib = {version = ">=7.3.0,<8.0", extras = ["html"]} [package.extras] dev-coverage = ["coverage (>6.1,!=6.1.1,<7)", "platformdirs", "pytest-cov (>=2.8.1,<3)"] dev-lint = ["platformdirs", "ruff (>=0.9.3,<0.10)"] dev-type-checking = ["mypy (>=1.13.0)", "platformdirs", "types-setuptools"] http = ["sanic (>=22.12,<23)", "sanic-cors (==2.2.0)", "sanic-ext (>=23.3,<23.6)"] -js = ["pyduktape2 (>=0.4.6,<1)"] +js = ["pyduktape2 (>=0.4.6,<1) ; python_version <= \"3.13\"", "pyduktape2 (>=0.5.0,<1) ; python_version >= \"3.13\""] +oxigraph = ["pyoxigraph (>=0.5.6)"] [[package]] name = "pytest" @@ -2427,23 +2604,18 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.4.2" +version = "1.6.0" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500"}, - {file = "python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690"}, + {file = "python_discovery-1.6.0-py3-none-any.whl", hash = "sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a"}, + {file = "python_discovery-1.6.0.tar.gz", hash = "sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c"}, ] [package.dependencies] filelock = ">=3.15.4" -platformdirs = ">=4.3.6,<5" - -[package.extras] -docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)", "sphinxcontrib-towncrier (>=0.4)", "towncrier (>=25.8)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] [[package]] name = "pyyaml" @@ -2530,104 +2702,98 @@ files = [ [[package]] name = "pyzmq" -version = "27.1.0" +version = "27.2.0" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev", "docs"] files = [ - {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, - {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, - {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, - {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, - {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, - {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, - {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, - {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, - {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, - {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, - {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, - {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, - {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, - {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, - {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, - {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, - {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, - {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, - {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, - {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, - {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, - {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, - {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, - {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, + {file = "pyzmq-27.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62"}, + {file = "pyzmq-27.2.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638"}, + {file = "pyzmq-27.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb"}, + {file = "pyzmq-27.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed"}, + {file = "pyzmq-27.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080"}, + {file = "pyzmq-27.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0"}, + {file = "pyzmq-27.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3"}, + {file = "pyzmq-27.2.0-cp310-cp310-win32.whl", hash = "sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7"}, + {file = "pyzmq-27.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033"}, + {file = "pyzmq-27.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e"}, + {file = "pyzmq-27.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872"}, + {file = "pyzmq-27.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373"}, + {file = "pyzmq-27.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e"}, + {file = "pyzmq-27.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67"}, + {file = "pyzmq-27.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff"}, + {file = "pyzmq-27.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7"}, + {file = "pyzmq-27.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42"}, + {file = "pyzmq-27.2.0-cp311-cp311-win32.whl", hash = "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c"}, + {file = "pyzmq-27.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e"}, + {file = "pyzmq-27.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b"}, + {file = "pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5"}, + {file = "pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394"}, + {file = "pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3"}, + {file = "pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42"}, + {file = "pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2"}, + {file = "pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12"}, + {file = "pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3"}, + {file = "pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52"}, + {file = "pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb"}, + {file = "pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702"}, + {file = "pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8"}, + {file = "pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7"}, + {file = "pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f"}, + {file = "pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1"}, + {file = "pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853"}, + {file = "pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd"}, + {file = "pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af"}, + {file = "pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103"}, + {file = "pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843"}, + {file = "pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a"}, + {file = "pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101"}, + {file = "pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b"}, + {file = "pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9"}, + {file = "pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f"}, + {file = "pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6"}, + {file = "pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44"}, + {file = "pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b"}, + {file = "pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d"}, + {file = "pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b"}, + {file = "pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012"}, + {file = "pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb"}, + {file = "pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35"}, + {file = "pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d"}, + {file = "pyzmq-27.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:76afba06ae698f2b8fe4fb34b32c760a650f168c2e622f370f2c528035b7f650"}, + {file = "pyzmq-27.2.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c9322f9c87b0935870516c2876e1e29497fdc50439c785ece63e3fbbab06c821"}, + {file = "pyzmq-27.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7e2579c5de82ddf4544d723c1bc8b44c3b806d157acc9fb2a2d18e10ef28e202"}, + {file = "pyzmq-27.2.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95f52b877149b06bbdeec2e8ea6230aad14950bbfbcfa16e7eb88951f07d6b28"}, + {file = "pyzmq-27.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a0ee3c49be2aa15abd12cbbd14d4ea2892f872c688e1e487af39ec1972ed549d"}, + {file = "pyzmq-27.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6eb63cc61ab93b01b9afc887a160255e2fbe703fdbacfe5feaef87214f51bd6c"}, + {file = "pyzmq-27.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f52f08101907609cc08db6a1f9f2a7a9afd54e9b2ca16178c9c38e99fb593cef"}, + {file = "pyzmq-27.2.0-cp39-cp39-win32.whl", hash = "sha256:507c0b33f95502723d325487e8e50c2cdd3b37444143f05423a3861327f69bf7"}, + {file = "pyzmq-27.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ff60f0f7ccfda0e303ac43bec7096007b7cdf2c41b3739d1ec667febe67acab3"}, + {file = "pyzmq-27.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:d61a0169ba05ab7ebc48dc793f092df12f789bf378dac8321ccd966fd93d94e8"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:97d4c6622f129b514a4f5939af1b5f434c97f47085d9311b5f7f36e24b3bd447"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:dfcd024eade5870b25f890c4df0ba9421ed8167d8d3d82334237512c1158dada"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a070a9cdad1f8f8a85ea153afcc4654f11b10895d14c0acabe10f1df0e0892ea"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:010db74a1dd67c7cd8b8b30916355735db7d633a070510bb34e41ab679ab2c0e"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab72ee77b313d0658447204c8201f9b315146e923b48c56ea7dbd005d464a91"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a5c04ad2e368142aea52d1abdf6631cb2534864e3c16ab78268ab957060b2a6"}, + {file = "pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3"}, ] [package.dependencies] @@ -2700,14 +2866,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-cache" -version = "1.3.2" +version = "1.3.3" description = "A persistent cache for python requests" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "requests_cache-1.3.2-py3-none-any.whl", hash = "sha256:c52666c76b08daa94d05a99327dd24afc46f405abc044e8c2267b540f90673d0"}, - {file = "requests_cache-1.3.2.tar.gz", hash = "sha256:bdc3680931f98a1dea509d339ea6b45cea526945b47b250ce63ffd2744ee0b14"}, + {file = "requests_cache-1.3.3-py3-none-any.whl", hash = "sha256:c8df20ff874ebfc026959e3874e6c12bd6724934cdb10925915908453d4b17e4"}, + {file = "requests_cache-1.3.3.tar.gz", hash = "sha256:79b72d5ac5143992d1836ad78f4d8e65666061dd44e220548caab3723089826b"}, ] [package.dependencies] @@ -2748,14 +2914,14 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" -version = "1.9.8" +version = "1.9.9" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "rich_click-1.9.8-py3-none-any.whl", hash = "sha256:12873865396e6927835d4eabb1cc3996edcd65b7ac9b2391a29eca4f335a2f93"}, - {file = "rich_click-1.9.8.tar.gz", hash = "sha256:4008f921da88b5d91646c134ec881c1500e5a6b3f093e90e8f29400e09608371"}, + {file = "rich_click-1.9.9-py3-none-any.whl", hash = "sha256:365e7a9d0adb42e41ea832a0a12e02c44c079a26536dee688125eb9814f97274"}, + {file = "rich_click-1.9.9.tar.gz", hash = "sha256:324cba7513cd4187ee92b2eef21f071714e45be062458c8b157bd7e0c81103e3"}, ] [package.dependencies] @@ -2764,10 +2930,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} rich = ">=12" typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} -[package.extras] -dev = ["inline-snapshot (>=0.24)", "jsonschema (>=4)", "mypy (>=1.14.1)", "nodeenv (>=1.9.1)", "packaging (>=25)", "pre-commit (>=3.5)", "pytest (>=8.3.5)", "pytest-cov (>=5)", "rich-codex (>=1.2.11)", "ruff (>=0.12.4)", "typer (>=0.15,<0.26)", "types-setuptools (>=75.8.0.20250110)"] -docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitions-plugin (>=0.1.1)", "mkdocs-glightbox (>=0.4)", "mkdocs-include-markdown-plugin (>=7.1.7) ; python_version >= \"3.9\"", "mkdocs-material-extensions (>=1.3.1)", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-redirects (>=1.2.2)", "mkdocs-rss-plugin (>=1.15)", "mkdocs[docs] (>=1.6.1)", "mkdocstrings[python] (>=0.26.1)", "rich-codex (>=1.2.11)", "typer (>=0.15,<0.26)"] - [[package]] name = "roman-numerals" version = "4.1.0" @@ -2925,143 +3087,129 @@ files = [ [[package]] name = "rpds-py" -version = "2026.5.1" +version = "2026.6.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.11" groups = ["docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036"}, - {file = "rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0"}, - {file = "rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a"}, - {file = "rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2"}, - {file = "rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2"}, - {file = "rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f"}, - {file = "rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a"}, - {file = "rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b"}, - {file = "rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d"}, - {file = "rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6"}, - {file = "rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4"}, - {file = "rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24"}, - {file = "rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732"}, - {file = "rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed"}, - {file = "rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870"}, - {file = "rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473"}, - {file = "rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d"}, - {file = "rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46"}, - {file = "rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf"}, - {file = "rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f"}, - {file = "rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89"}, - {file = "rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842"}, - {file = "rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf"}, - {file = "rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55"}, - {file = "rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9"}, - {file = "rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d"}, - {file = "rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d"}, - {file = "rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02"}, - {file = "rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0"}, - {file = "rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7"}, - {file = "rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838"}, - {file = "rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6"}, - {file = "rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb"}, - {file = "rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9"}, - {file = "rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14"}, - {file = "rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01"}, - {file = "rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d"}, - {file = "rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa"}, - {file = "rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325"}, - {file = "rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049"}, - {file = "rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, + {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, ] [[package]] @@ -3084,30 +3232,30 @@ oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\"" [[package]] name = "ruff" -version = "0.15.19" +version = "0.15.22" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86"}, - {file = "ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40"}, - {file = "ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8"}, - {file = "ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da"}, - {file = "ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0"}, - {file = "ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be"}, - {file = "ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063"}, + {file = "ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8"}, + {file = "ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697"}, + {file = "ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64"}, + {file = "ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf"}, + {file = "ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde"}, + {file = "ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661"}, + {file = "ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809"}, ] [[package]] @@ -3136,14 +3284,14 @@ files = [ [[package]] name = "soupsieve" -version = "2.8.4" +version = "2.9.2" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"}, - {file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"}, + {file = "soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823"}, + {file = "soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74"}, ] [[package]] @@ -3670,51 +3818,51 @@ files = [ [[package]] name = "tomlkit" -version = "0.15.0" +version = "0.15.1" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738"}, - {file = "tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3"}, + {file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"}, + {file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"}, ] [[package]] name = "tornado" -version = "6.5.7" +version = "6.5.10" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">=3.9" groups = ["dev", "docs"] files = [ - {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"}, - {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"}, - {file = "tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972"}, - {file = "tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b"}, - {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92"}, - {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5"}, - {file = "tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4"}, - {file = "tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4"}, - {file = "tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796"}, - {file = "tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2"}, + {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7"}, + {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1"}, + {file = "tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d"}, + {file = "tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676"}, + {file = "tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015"}, + {file = "tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828"}, + {file = "tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72"}, + {file = "tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918"}, + {file = "tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694"}, + {file = "tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687"}, ] [[package]] name = "traitlets" -version = "5.15.1" +version = "5.16.1" description = "Traitlets Python configuration system" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92"}, - {file = "traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722"}, + {file = "traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b"}, + {file = "traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.17.0,<1.19)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3) ; python_version < \"3.12\"", "argcomplete (>=3.5.2) ; python_version >= \"3.12\"", "mypy (>=2.0) ; implementation_name != \"pypy\"", "pre-commit", "pytest (>=7.0,<10.0)", "pytest-mock", "pytest-mypy-testing ; implementation_name != \"pypy\""] [[package]] name = "types-toml" @@ -3730,14 +3878,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs", "test"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] markers = {test = "python_version == \"3.10\""} @@ -3761,14 +3909,14 @@ dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-socket", "ruff"] [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" groups = ["main", "docs"] files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, ] [package.extras] @@ -3779,47 +3927,51 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.7.10" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783"}, - {file = "virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8"}, + {file = "virtualenv-21.7.10-py3-none-any.whl", hash = "sha256:d7ac9669ba19e675ffadbb97fe8e887ef92fb1743fe9a0032be62b947657327a"}, + {file = "virtualenv-21.7.10.tar.gz", hash = "sha256:a7bf10f37ecc36f1942d6e469d6b59f5fe308f60ac711f66f51ef3bd8cb2c9aa"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" -python-discovery = ">=1.4.2" +python-discovery = ">=1.6" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [[package]] name = "wcwidth" -version = "0.8.1" +version = "0.8.3" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8"}, - {file = "wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9"}, + {file = "wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4"}, + {file = "wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb"}, ] [[package]] name = "webencodings" -version = "0.5.1" +version = "0.6.1" description = "Character encoding aliases for legacy web content" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, + {file = "webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b"}, + {file = "webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910"}, ] +[package.extras] +doc = ["furo", "sphinx"] +test = ["pytest", "ruff"] + [[package]] name = "zipp" version = "4.1.0" From b0c2107f23b2b24380156bac3d90bb2bf4ff101a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 16 Sep 2026 10:19:15 +0200 Subject: [PATCH 069/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.11.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bcb27df93..d6f41c305 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.11.3" +version = "0.11.4" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From a46eb34ff08c562fbc5d3a4109cf6809eea1338e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 17 Sep 2026 05:36:18 +0200 Subject: [PATCH 070/271] refactor(shacl): :truck: move transformer core under SHACL requirements --- docs/14_graph_transformers.rst | 16 +++++++++------- .../shacl/transformers}/__init__.py | 4 ++-- .../shacl/transformers}/base.py | 0 .../transformers}/validation_candidate_marker.py | 4 ++-- .../shacl/transformers}/vocabulary.py | 0 .../requirements/shacl/validator.py | 2 +- tests/unit/graph_transformers/test_base.py | 6 +++--- .../test_validation_candidate_marker.py | 4 ++-- 8 files changed, 19 insertions(+), 17 deletions(-) rename rocrate_validator/{graph_transformers => requirements/shacl/transformers}/__init__.py (94%) rename rocrate_validator/{graph_transformers => requirements/shacl/transformers}/base.py (100%) rename rocrate_validator/{graph_transformers => requirements/shacl/transformers}/validation_candidate_marker.py (93%) rename rocrate_validator/{graph_transformers => requirements/shacl/transformers}/vocabulary.py (100%) diff --git a/docs/14_graph_transformers.rst b/docs/14_graph_transformers.rst index e0fb38349..dcd76ade5 100644 --- a/docs/14_graph_transformers.rst +++ b/docs/14_graph_transformers.rst @@ -188,7 +188,8 @@ How the pipeline works ---------------------- ``prepare_data_graph`` creates one transient copy of the parsed RO-Crate graph, -discovers all modules below ``rocrate_validator.graph_transformers``, and runs +discovers all modules below +``rocrate_validator.requirements.shacl.transformers``, and runs their registered transformers sequentially. The cached source graph is never passed to a transformer and remains unchanged. @@ -220,8 +221,8 @@ Every transformer must: to the RO-Crate. The discovery mechanism covers modules bundled in the -``rocrate_validator.graph_transformers`` package. It is not an entry-point -system for arbitrary profile-local or third-party plugins. +``rocrate_validator.requirements.shacl.transformers`` package. It is not an +entry-point system for arbitrary profile-local or third-party plugins. Function transformers ~~~~~~~~~~~~~~~~~~~~~ @@ -233,7 +234,7 @@ expressed as a function: from rdflib import Graph - from rocrate_validator.graph_transformers import graph_transformer + from rocrate_validator.requirements.shacl.transformers import graph_transformer @graph_transformer(order=50) @@ -255,7 +256,7 @@ private helper methods or a clearer class-level contract: from rdflib import Graph - from rocrate_validator.graph_transformers import GraphTransformer + from rocrate_validator.requirements.shacl.transformers import GraphTransformer class ExampleTransformer(GraphTransformer): @@ -276,8 +277,9 @@ Private marker predicates and prefixes A transformer may annotate the transient graph with private predicates. Keep these predicates in the validator-owned namespace ``https://github.com/crs4/rocrate-validator/graph-transformers/`` and define -their RDFLib vocabulary terms in ``graph_transformers/vocabulary.py``. These triples -are implementation details: they must not be serialized into an RO-Crate or +their RDFLib vocabulary terms in +``requirements/shacl/transformers/vocabulary.py``. These triples are +implementation details: they must not be serialized into an RO-Crate or presented as vocabulary terms owned by the crate. When a SHACL query consumes a marker, declare the namespace in the profile's diff --git a/rocrate_validator/graph_transformers/__init__.py b/rocrate_validator/requirements/shacl/transformers/__init__.py similarity index 94% rename from rocrate_validator/graph_transformers/__init__.py rename to rocrate_validator/requirements/shacl/transformers/__init__.py index 0316c5c56..8c570bf3f 100644 --- a/rocrate_validator/graph_transformers/__init__.py +++ b/rocrate_validator/requirements/shacl/transformers/__init__.py @@ -20,12 +20,12 @@ from rdflib import Dataset, Graph -from rocrate_validator.graph_transformers.base import ( +from rocrate_validator.requirements.shacl.transformers.base import ( GraphTransformer, graph_transformer, run_registered_transformers, ) -from rocrate_validator.graph_transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE +from rocrate_validator.requirements.shacl.transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE _DISCOVERY_LOCK = Lock() _TRANSFORMERS_LOADED = False diff --git a/rocrate_validator/graph_transformers/base.py b/rocrate_validator/requirements/shacl/transformers/base.py similarity index 100% rename from rocrate_validator/graph_transformers/base.py rename to rocrate_validator/requirements/shacl/transformers/base.py diff --git a/rocrate_validator/graph_transformers/validation_candidate_marker.py b/rocrate_validator/requirements/shacl/transformers/validation_candidate_marker.py similarity index 93% rename from rocrate_validator/graph_transformers/validation_candidate_marker.py rename to rocrate_validator/requirements/shacl/transformers/validation_candidate_marker.py index 871c8c9b4..fe070e0c7 100644 --- a/rocrate_validator/graph_transformers/validation_candidate_marker.py +++ b/rocrate_validator/requirements/shacl/transformers/validation_candidate_marker.py @@ -18,8 +18,8 @@ from rdflib import RDF, BNode, Graph, Literal, URIRef -from rocrate_validator.graph_transformers.base import graph_transformer -from rocrate_validator.graph_transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE +from rocrate_validator.requirements.shacl.transformers.base import graph_transformer +from rocrate_validator.requirements.shacl.transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE logger = logging.getLogger(__name__) diff --git a/rocrate_validator/graph_transformers/vocabulary.py b/rocrate_validator/requirements/shacl/transformers/vocabulary.py similarity index 100% rename from rocrate_validator/graph_transformers/vocabulary.py rename to rocrate_validator/requirements/shacl/transformers/vocabulary.py diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 746d51385..78dce655e 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -34,7 +34,6 @@ VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES, ) -from rocrate_validator.graph_transformers import prepare_data_graph from rocrate_validator.models import ( Profile, RequirementCheck, @@ -44,6 +43,7 @@ ) from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry +from rocrate_validator.requirements.shacl.transformers import prepare_data_graph from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging from rocrate_validator.utils.rdf import extract_base_from_jsonld diff --git a/tests/unit/graph_transformers/test_base.py b/tests/unit/graph_transformers/test_base.py index 811fcdc84..a87ae744e 100644 --- a/tests/unit/graph_transformers/test_base.py +++ b/tests/unit/graph_transformers/test_base.py @@ -17,9 +17,9 @@ import pytest from rdflib import Graph, Literal, Namespace -from rocrate_validator.graph_transformers import GraphTransformer, graph_transformer -from rocrate_validator.graph_transformers import base as transformer_base -from rocrate_validator.graph_transformers.base import ( +from rocrate_validator.requirements.shacl.transformers import GraphTransformer, graph_transformer +from rocrate_validator.requirements.shacl.transformers import base as transformer_base +from rocrate_validator.requirements.shacl.transformers.base import ( get_registered_transformers, run_registered_transformers, ) diff --git a/tests/unit/requirements/test_validation_candidate_marker.py b/tests/unit/requirements/test_validation_candidate_marker.py index f2d9a1bf2..101ec68fc 100644 --- a/tests/unit/requirements/test_validation_candidate_marker.py +++ b/tests/unit/requirements/test_validation_candidate_marker.py @@ -16,11 +16,11 @@ from rdflib import RDF, BNode, Graph, Literal, Namespace, URIRef from rdflib.namespace import SH -from rocrate_validator.graph_transformers import ( +from rocrate_validator.requirements.shacl import SHACLValidator +from rocrate_validator.requirements.shacl.transformers import ( VALIDATION_CANDIDATE_PREDICATE, prepare_data_graph, ) -from rocrate_validator.requirements.shacl import SHACLValidator SCHEMA = Namespace("http://schema.org/") CSVW = Namespace("http://www.w3.org/ns/csvw#") From 21590fe88be1511195c8d3b9051a4244a5f33bed Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 17 Sep 2026 05:37:11 +0200 Subject: [PATCH 071/271] refactor(transformers): :truck: separate implementations from SHACL core --- docs/14_graph_transformers.rst | 7 ++++--- .../requirements/shacl/transformers/__init__.py | 8 ++++---- rocrate_validator/transformers/__init__.py | 15 +++++++++++++++ .../transformers/validation_candidate_marker.py | 2 +- .../test_validation_candidate_marker.py | 0 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 rocrate_validator/transformers/__init__.py rename rocrate_validator/{requirements/shacl => }/transformers/validation_candidate_marker.py (97%) rename tests/unit/{requirements => transformers}/test_validation_candidate_marker.py (100%) diff --git a/docs/14_graph_transformers.rst b/docs/14_graph_transformers.rst index dcd76ade5..aa2aa8ab1 100644 --- a/docs/14_graph_transformers.rst +++ b/docs/14_graph_transformers.rst @@ -189,7 +189,7 @@ How the pipeline works ``prepare_data_graph`` creates one transient copy of the parsed RO-Crate graph, discovers all modules below -``rocrate_validator.requirements.shacl.transformers``, and runs +``rocrate_validator.transformers``, and runs their registered transformers sequentially. The cached source graph is never passed to a transformer and remains unchanged. @@ -220,8 +220,9 @@ Every transformer must: * treat the graph as transient validator state, never as metadata to write back to the RO-Crate. -The discovery mechanism covers modules bundled in the -``rocrate_validator.requirements.shacl.transformers`` package. It is not an +The discovery mechanism covers modules bundled in the sibling catalog package +``rocrate_validator.transformers``. The engine remains in +``rocrate_validator.requirements.shacl.transformers``. This is not an entry-point system for arbitrary profile-local or third-party plugins. Function transformers diff --git a/rocrate_validator/requirements/shacl/transformers/__init__.py b/rocrate_validator/requirements/shacl/transformers/__init__.py index 8c570bf3f..4c8a913ed 100644 --- a/rocrate_validator/requirements/shacl/transformers/__init__.py +++ b/rocrate_validator/requirements/shacl/transformers/__init__.py @@ -45,10 +45,10 @@ def _load_concrete_transformers() -> None: with _DISCOVERY_LOCK: if _TRANSFORMERS_LOADED: return - package_prefix = f"{__name__}." - for module in pkgutil.walk_packages(__path__, package_prefix): - if module.name != f"{__name__}.base": - importlib.import_module(module.name) + package = importlib.import_module("rocrate_validator.transformers") + package_prefix = f"{package.__name__}." + for module in pkgutil.walk_packages(package.__path__, package_prefix): + importlib.import_module(module.name) _TRANSFORMERS_LOADED = True diff --git a/rocrate_validator/transformers/__init__.py b/rocrate_validator/transformers/__init__.py new file mode 100644 index 000000000..e368067f1 --- /dev/null +++ b/rocrate_validator/transformers/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bundled graph-transformer implementations used by SHACL profiles.""" diff --git a/rocrate_validator/requirements/shacl/transformers/validation_candidate_marker.py b/rocrate_validator/transformers/validation_candidate_marker.py similarity index 97% rename from rocrate_validator/requirements/shacl/transformers/validation_candidate_marker.py rename to rocrate_validator/transformers/validation_candidate_marker.py index fe070e0c7..98bd907a7 100644 --- a/rocrate_validator/requirements/shacl/transformers/validation_candidate_marker.py +++ b/rocrate_validator/transformers/validation_candidate_marker.py @@ -18,7 +18,7 @@ from rdflib import RDF, BNode, Graph, Literal, URIRef -from rocrate_validator.requirements.shacl.transformers.base import graph_transformer +from rocrate_validator.requirements.shacl.transformers import graph_transformer from rocrate_validator.requirements.shacl.transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE logger = logging.getLogger(__name__) diff --git a/tests/unit/requirements/test_validation_candidate_marker.py b/tests/unit/transformers/test_validation_candidate_marker.py similarity index 100% rename from tests/unit/requirements/test_validation_candidate_marker.py rename to tests/unit/transformers/test_validation_candidate_marker.py From 822c26be8073baf015ed5e09e8759b9aaea128e6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 17 Sep 2026 05:39:56 +0200 Subject: [PATCH 072/271] feat(shacl): :sparkles: activate graph transformers from shapes --- .../1.2/must/6_contextual_entity_metadata.ttl | 32 +-- .../ro-crate/1.2/should/0_entity_metadata.ttl | 8 +- .../shacl/transformers/__init__.py | 69 +----- .../requirements/shacl/transformers/base.py | 70 ++---- .../shacl/transformers/discovery.py | 44 ++++ .../shacl/transformers/pipeline.py | 91 ++++++++ .../shacl/transformers/preparation.py | 213 ++++++++++++++++++ .../shacl/transformers/registry.py | 110 +++++++++ .../shacl/transformers/vocabulary.py | 12 +- .../requirements/shacl/validator.py | 59 +++-- .../validation_candidate_marker.py | 42 +++- .../shacl/transformers}/test_base.py | 57 ++++- .../shacl/transformers/test_preparation.py | 75 ++++++ .../test_validation_candidate_marker.py | 178 ++++++++++++++- 14 files changed, 856 insertions(+), 204 deletions(-) create mode 100644 rocrate_validator/requirements/shacl/transformers/discovery.py create mode 100644 rocrate_validator/requirements/shacl/transformers/pipeline.py create mode 100644 rocrate_validator/requirements/shacl/transformers/preparation.py create mode 100644 rocrate_validator/requirements/shacl/transformers/registry.py rename tests/unit/{graph_transformers => requirements/shacl/transformers}/test_base.py (51%) create mode 100644 tests/unit/requirements/shacl/transformers/test_preparation.py diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl index 0fce7655e..e913d6d1b 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl @@ -19,36 +19,28 @@ @prefix schema_org: . @prefix sh: . @prefix validator: . +@prefix validator-transformers: . @prefix xsd: . ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; + validator:requiresGraphTransformer validator-transformers:validationCandidateMarker ; sh:name "Identify Contextual Entities" ; sh:description """Mark entities as Contextual Entities if they are in the RO-Crate metadata but are not Data Entities, not the Root Data Entity, - and not the Metadata File Descriptor. The query uses `?this ?p ?o` - (subject of any triple) — SHACL rules from earlier shapes (e.g. - CreativeWorkAuthorDefinition) tag referenced-but-undescribed entities - with rdf:type, so they appear as subjects and are therefore captured. - Vocabulary terms loaded from ontologies are excluded using provenance - captured from the original crate graph before the ontology mix-in.""" ; + and not the Metadata File Descriptor. Validation candidates are + classified from asserted source triples before ontology inoculation, + inference, or SHACL rules. This includes type-only and claim-only + entities while excluding cited vocabulary terms.""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { - # Structural criteria: IRI or blank node, subject of some triple. - # SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) - # tag even referenced-but-undescribed entities so they become - # subjects and are correctly classified as Contextual Entities. - # - # The distinct subjects are materialised by a subquery FIRST so the - # exclusion FILTERs below run once per entity instead of once per - # triple (~2.5k subjects vs ~40k triples on a large crate). - { SELECT DISTINCT ?this WHERE { - ?this ?p ?o . - FILTER(isIRI(?this) || isBlank(?this)) - } } + # Candidates were classified from the asserted source graph, + # before ontology inoculation, inference, and SHACL rules. + ?this validator-transformers:validationCandidate true . + FILTER(isIRI(?this) || isBlank(?this)) # Structural exclusions — Metadata Descriptor, Root Data Entity, # Data Entities (File/Dataset or ro-crate:DataEntity), Ontology. @@ -59,10 +51,6 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; FILTER NOT EXISTS { ?this a schema:Dataset } FILTER NOT EXISTS { ?this a owl:Ontology } - # Restrict validation to nodes which the original crate - # describes or claims. Claim-only objects remain in scope after - # SHACL rules materialise their entity type. - FILTER EXISTS { ?this validator-transformers:validationCandidate true } } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl index 012350ea9..b56530970 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl @@ -15,8 +15,11 @@ @prefix sh: . @prefix schema: . @prefix xsd: . +@prefix validator: . +@prefix validator-transformers: . ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; + validator:requiresGraphTransformer validator-transformers:validationCandidateMarker ; sh:name "RO-Crate Metadata Entity: RECOMMENDED properties" ; sh:description """Check if the RO-Crate Metadata Entity includes at least one Schema.org type and a human-readable name""" ; sh:target [ @@ -25,6 +28,7 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + ?this validator-transformers:validationCandidate true . ?this a ?type . { SELECT DISTINCT ?root ?metadatafile WHERE { @@ -36,10 +40,6 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; FILTER (?this != ?root) # Exclude entities with non-IRI identifiers. FILTER (isIRI(?this)) - # Restrict validation to nodes which the original crate - # describes or claims, before ontology and inferred terms are - # added to the graph. - FILTER EXISTS { ?this validator-transformers:validationCandidate true } # Profile descriptors use prof:Profile rather than a Schema.org # type and have their own dedicated metadata requirements. FILTER NOT EXISTS { ?this a } diff --git a/rocrate_validator/requirements/shacl/transformers/__init__.py b/rocrate_validator/requirements/shacl/transformers/__init__.py index 4c8a913ed..8eb441a0a 100644 --- a/rocrate_validator/requirements/shacl/transformers/__init__.py +++ b/rocrate_validator/requirements/shacl/transformers/__init__.py @@ -12,72 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations +"""Core API for graph transformers used by SHACL validation.""" -import importlib -import pkgutil -from threading import Lock - -from rdflib import Dataset, Graph - -from rocrate_validator.requirements.shacl.transformers.base import ( - GraphTransformer, - graph_transformer, - run_registered_transformers, +from rocrate_validator.requirements.shacl.transformers.base import GraphTransformer +from rocrate_validator.requirements.shacl.transformers.pipeline import prepare_data_graph +from rocrate_validator.requirements.shacl.transformers.registry import graph_transformer +from rocrate_validator.requirements.shacl.transformers.vocabulary import ( + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, ) -from rocrate_validator.requirements.shacl.transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE - -_DISCOVERY_LOCK = Lock() -_TRANSFORMERS_LOADED = False - - -def _load_concrete_transformers() -> None: - """ - Import every transformer module in this package exactly once. - - Importing triggers function decorators and ``GraphTransformer`` subclass - hooks. The lock prevents concurrent validations from observing a partially - populated registry during first use. - """ - global _TRANSFORMERS_LOADED # noqa: PLW0603 - if _TRANSFORMERS_LOADED: - return - with _DISCOVERY_LOCK: - if _TRANSFORMERS_LOADED: - return - package = importlib.import_module("rocrate_validator.transformers") - package_prefix = f"{package.__name__}." - for module in pkgutil.walk_packages(package.__path__, package_prefix): - importlib.import_module(module.name) - _TRANSFORMERS_LOADED = True - - -def _copy_graph(source: Graph) -> Graph: - """Return a validation-only copy, including namespace bindings.""" - identifier = source.default_graph.identifier if isinstance(source, Dataset) else source.identifier - target = Graph(identifier=identifier) - for prefix, namespace in source.namespaces(): - target.bind(prefix, namespace, replace=True) - # Dataset iteration yields quads; copy the union view as triples so the - # transient validation graph has the same shape for Graph and Dataset input. - for triple in source.triples((None, None, None)): - target.add(triple) - return target - - -def prepare_data_graph(data_graph: Graph) -> Graph: - """Discover and run all concrete transformers on a transient graph copy. - - The copy is created once for the complete pipeline. Each transformer sees - the output of its predecessor, while the cached graph owned by the - validation context remains untouched. - """ - _load_concrete_transformers() - return run_registered_transformers(_copy_graph(data_graph)) - __all__ = [ - "VALIDATION_CANDIDATE_PREDICATE", + "REQUIRES_GRAPH_TRANSFORMER_PREDICATE", "GraphTransformer", "graph_transformer", "prepare_data_graph", diff --git a/rocrate_validator/requirements/shacl/transformers/base.py b/rocrate_validator/requirements/shacl/transformers/base.py index 62163bc75..dd9f96e3b 100644 --- a/rocrate_validator/requirements/shacl/transformers/base.py +++ b/rocrate_validator/requirements/shacl/transformers/base.py @@ -17,37 +17,13 @@ import inspect from abc import ABC, abstractmethod from collections.abc import Callable -from dataclasses import dataclass from typing import ClassVar -from rdflib import Graph +from rdflib import Graph, URIRef TransformerFunction = Callable[[Graph], Graph] -@dataclass(frozen=True) -class RegisteredTransformer: - """A transformer definition together with its execution metadata.""" - - name: str - order: int - implementation: TransformerFunction | type[GraphTransformer] - - -# Qualified names make registration idempotent when Python imports or reloads -# a transformer module more than once. A reload replaces the same definition -# instead of scheduling it twice. -_TRANSFORMERS: dict[str, RegisteredTransformer] = {} - - -def _register( - implementation: TransformerFunction | type[GraphTransformer], - order: int, -) -> None: - name = f"{implementation.__module__}.{implementation.__qualname__}" - _TRANSFORMERS[name] = RegisteredTransformer(name, order, implementation) - - class GraphTransformer(ABC): """ Base class for automatically registered graph transformers. @@ -56,46 +32,26 @@ class GraphTransformer(ABC): Abstract intermediate classes remain unregistered. """ + identifier: ClassVar[URIRef | None] = None order: ClassVar[int] = 100 + annotation_predicates: ClassVar[frozenset[URIRef]] = frozenset() def __init_subclass__(cls, **kwargs: object) -> None: super().__init_subclass__(**kwargs) if not inspect.isabstract(cls): - _register(cls, cls.order) + if cls.identifier is None: + raise TypeError(f"Graph transformer {cls.__module__}.{cls.__qualname__} has no identifier") + # Imported lazily to keep the abstract contract independent from + # the registry module which stores concrete implementations. + from rocrate_validator.requirements.shacl.transformers.registry import ( # noqa: PLC0415 + register_transformer, + ) + + register_transformer(cls, cls.identifier, cls.order, cls.annotation_predicates) @abstractmethod def transform(self, data_graph: Graph) -> Graph: """Transform and return the graph passed by the orchestration pipeline.""" -def graph_transformer(*, order: int = 100) -> Callable[[TransformerFunction], TransformerFunction]: - """Register a function as a graph transformer.""" - - def decorator(function: TransformerFunction) -> TransformerFunction: - _register(function, order) - return function - - return decorator - - -def get_registered_transformers() -> tuple[RegisteredTransformer, ...]: - """Return registered transformers in deterministic execution order.""" - # Lower values run first. The qualified name is a deterministic tie-breaker - # so execution never depends on filesystem or import iteration order. - return tuple(sorted(_TRANSFORMERS.values(), key=lambda item: (item.order, item.name))) - - -def run_registered_transformers(data_graph: Graph) -> Graph: - """Run every registered transformer sequentially on the same graph.""" - transformed_graph = data_graph - for registered in get_registered_transformers(): - # Class transformers are deliberately instantiated without arguments; - # persistent state must not leak between separate validation runs. - implementation = registered.implementation - if isinstance(implementation, type): - transformed_graph = implementation().transform(transformed_graph) - else: - transformed_graph = implementation(transformed_graph) - if not isinstance(transformed_graph, Graph): - raise TypeError(f"Graph transformer {registered.name} did not return an RDFLib Graph") - return transformed_graph +__all__ = ["GraphTransformer", "TransformerFunction"] diff --git a/rocrate_validator/requirements/shacl/transformers/discovery.py b/rocrate_validator/requirements/shacl/transformers/discovery.py new file mode 100644 index 000000000..948a9474d --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/discovery.py @@ -0,0 +1,44 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Discovery of graph transformers bundled with the validator.""" + +from __future__ import annotations + +import importlib +import pkgutil +from threading import Lock + +_DISCOVERY_LOCK = Lock() +_TRANSFORMERS_LOADED = False + + +def load_transformers() -> None: + """Import every bundled transformer module exactly once. + + Importing triggers function decorators and ``GraphTransformer`` subclass + hooks. The lock prevents concurrent validations from observing a partially + populated registry during first use. + """ + global _TRANSFORMERS_LOADED # noqa: PLW0603 + if _TRANSFORMERS_LOADED: + return + with _DISCOVERY_LOCK: + if _TRANSFORMERS_LOADED: + return + package = importlib.import_module("rocrate_validator.transformers") + package_prefix = f"{package.__name__}." + for module in pkgutil.walk_packages(package.__path__, package_prefix): + importlib.import_module(module.name) + _TRANSFORMERS_LOADED = True diff --git a/rocrate_validator/requirements/shacl/transformers/pipeline.py b/rocrate_validator/requirements/shacl/transformers/pipeline.py new file mode 100644 index 000000000..1131f1b4a --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/pipeline.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Execution pipeline for registered graph transformers.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rdflib import Dataset, Graph + +from rocrate_validator.requirements.shacl.transformers.discovery import load_transformers +from rocrate_validator.requirements.shacl.transformers.registry import get_registered_transformers + +if TYPE_CHECKING: + from collections.abc import Collection + + from rdflib import URIRef + + +def _copy_data_graph(source: Graph) -> Graph: + """Copy a data graph without flattening RDFLib dataset contexts.""" + if isinstance(source, Dataset): + target: Graph = Dataset( + default_union=source.default_union, + default_graph_base=source.default_graph.base, + ) + else: + target = Graph(identifier=source.identifier, base=source.base) + + for prefix, namespace in source.namespaces(): + target.bind(prefix, namespace, replace=True) + + if isinstance(source, Dataset): + assert isinstance(target, Dataset) + for subject, predicate, obj, context in source.quads((None, None, None, None)): + context_graph = ( + target.default_graph if context == source.default_graph.identifier else target.graph(context) + ) + context_graph.add((subject, predicate, obj)) + else: + for triple in source: + target.add(triple) + return target + + +def prepare_data_graph( + data_graph: Graph, + transformer_identifiers: Collection[URIRef] | None = None, +) -> Graph: + """Run selected transformers on one transient copy of a data graph. + + Passing ``None`` preserves the low-level helper's original behaviour and + runs every registered transformer. The caller-owned source graph is never + passed to transformer code. + """ + load_transformers() + return run_registered_transformers(_copy_data_graph(data_graph), transformer_identifiers) + + +def run_registered_transformers( + data_graph: Graph, + identifiers: Collection[URIRef] | None = None, +) -> Graph: + """Run the selected registered transformers sequentially.""" + transformed_graph = data_graph + for registered in get_registered_transformers(identifiers): + # Class transformers are deliberately instantiated without arguments; + # persistent state must not leak between separate validation runs. + implementation = registered.implementation + if isinstance(implementation, type): + transformed_graph = implementation().transform(transformed_graph) + else: + transformed_graph = implementation(transformed_graph) + if not isinstance(transformed_graph, Graph): + raise TypeError(f"Graph transformer {registered.name} did not return an RDFLib Graph") + return transformed_graph + + +__all__ = ["prepare_data_graph", "run_registered_transformers"] diff --git a/rocrate_validator/requirements/shacl/transformers/preparation.py b/rocrate_validator/requirements/shacl/transformers/preparation.py new file mode 100644 index 000000000..3157a0b14 --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/preparation.py @@ -0,0 +1,213 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""SHACL integration for graph-transformer-dependent validation.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import cast + +from pyshacl.monkey import rdflib_bool_patch, rdflib_bool_unpatch +from pyshacl.rdfutil.load import load_from_source +from rdflib import RDF, BNode, Dataset, Graph, Literal, Namespace, URIRef +from rdflib.collection import Collection + +from rocrate_validator.constants import SHACL_NS +from rocrate_validator.requirements.shacl.transformers.discovery import load_transformers +from rocrate_validator.requirements.shacl.transformers.pipeline import prepare_data_graph +from rocrate_validator.requirements.shacl.transformers.registry import get_registered_transformers +from rocrate_validator.requirements.shacl.transformers.vocabulary import ( + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, +) + +logger = logging.getLogger(__name__) + +GraphSource = Graph | str | bytes +ShapesSource = GraphSource | None + + +class GraphTransformerCompatibilityError(ValueError): + """Raised when pySHACL options cannot support requested transformers.""" + + +@dataclass(frozen=True) +class PreparedValidationGraphs: + """Data and shapes sources prepared for one pySHACL invocation.""" + + data_graph: GraphSource + shapes_graph: ShapesSource + + +def prepare_validation_graphs( + data_graph: GraphSource, + shapes_graph: ShapesSource, + *, + data_graph_format: str | None = None, + shacl_graph_format: str | None = None, + do_owl_imports: bool | int = False, + inplace: bool = False, + sparql_mode: bool = False, +) -> PreparedValidationGraphs: + """Apply graph transformers explicitly requested by SHACL shapes. + + Shapes without a transformer declaration leave caller-owned graph objects + untouched. Transformer-dependent validation receives private data and + shapes copies suitable for a single pySHACL invocation. + """ + inspected_shapes = _load_shapes_for_inspection( + shapes_graph, + rdf_format=shacl_graph_format, + do_owl_imports=do_owl_imports, + ) + if inspected_shapes is None: + return PreparedValidationGraphs(data_graph, shapes_graph) + + required_transformers = _get_required_transformers(inspected_shapes) + if not required_transformers: + # Reuse an already parsed serialized source instead of asking pySHACL + # to parse it a second time. Existing Graph inputs remain unchanged. + return PreparedValidationGraphs(data_graph, inspected_shapes) + + _validate_options(required_transformers, inplace=inplace, sparql_mode=sparql_mode) + annotation_predicates = _get_annotation_predicates(required_transformers) + prepared_shapes = _copy_shapes_graph(inspected_shapes) + _ignore_annotations_in_closed_shapes(prepared_shapes, annotation_predicates) + prepared_data = _load_data_graph(data_graph, rdf_format=data_graph_format) + prepared_data = prepare_data_graph(prepared_data, required_transformers) + return PreparedValidationGraphs(prepared_data, prepared_shapes) + + +def _load_shapes_for_inspection( + source: ShapesSource, + *, + rdf_format: str | None, + do_owl_imports: bool | int, +) -> Graph | None: + if source is None: + return None + if isinstance(source, Graph) and not do_owl_imports: + return source + + load_source: GraphSource = _copy_shapes_graph(source) if isinstance(source, Graph) else source + rdflib_bool_patch() + try: + return cast( + "Graph", + load_from_source( + load_source, + rdf_format=rdf_format, + multigraph=True, + do_owl_imports=do_owl_imports, + logger=logger, + ), + ) + finally: + rdflib_bool_unpatch() + + +def _load_data_graph(source: GraphSource, *, rdf_format: str | None) -> Graph: + if isinstance(source, Graph): + return source + return cast( + "Graph", + load_from_source( + source, + rdf_format=rdf_format, + multigraph=True, + do_owl_imports=False, + logger=logger, + ), + ) + + +def _copy_shapes_graph(source: Graph) -> Graph: + """Flatten a shapes source into an isolated graph for private changes.""" + identifier = source.default_graph.identifier if isinstance(source, Dataset) else source.identifier + target = Graph(identifier=identifier, base=source.base) + for prefix, namespace in source.namespaces(): + target.bind(prefix, namespace, replace=True) + if isinstance(source, Dataset): + for subject, predicate, obj, _ in source.quads((None, None, None, None)): + target.add((subject, predicate, obj)) + else: + for triple in source: + target.add(triple) + return target + + +def _get_required_transformers(shapes_graph: Graph) -> frozenset[URIRef]: + identifiers: set[URIRef] = set() + for identifier in shapes_graph.objects(None, REQUIRES_GRAPH_TRANSFORMER_PREDICATE): + if not isinstance(identifier, URIRef): + raise TypeError("Graph transformer identifiers declared by SHACL shapes must be IRIs") + identifiers.add(identifier) + return frozenset(identifiers) + + +def _get_annotation_predicates(transformer_identifiers: frozenset[URIRef]) -> frozenset[URIRef]: + load_transformers() + return frozenset( + predicate + for transformer in get_registered_transformers(transformer_identifiers) + for predicate in transformer.annotation_predicates + ) + + +def _validate_options( + required_transformers: frozenset[URIRef], + *, + inplace: bool, + sparql_mode: bool, +) -> None: + if not required_transformers: + return + if sparql_mode: + raise GraphTransformerCompatibilityError( + "sparql_mode cannot be used with graph-transformer-dependent SHACL shapes" + ) + if inplace: + raise GraphTransformerCompatibilityError( + "inplace=True cannot be used when SHACL shapes require graph transformers" + ) + + +def _ignore_annotations_in_closed_shapes( + shapes_graph: Graph, + annotation_predicates: frozenset[URIRef], +) -> None: + if not annotation_predicates: + return + + shacl_ns = Namespace(SHACL_NS) + for shape in shapes_graph.subjects(shacl_ns.closed, Literal(True)): + list_head = shapes_graph.value(shape, shacl_ns.ignoredProperties) + if list_head is None or list_head == RDF.nil: + if list_head == RDF.nil: + shapes_graph.remove((shape, shacl_ns.ignoredProperties, RDF.nil)) + list_head = BNode() + shapes_graph.add((shape, shacl_ns.ignoredProperties, list_head)) + ignored_properties = Collection(shapes_graph, list_head) + existing = set(ignored_properties) + for predicate in sorted(annotation_predicates, key=str): + if predicate not in existing: + ignored_properties.append(predicate) + + +__all__ = [ + "GraphTransformerCompatibilityError", + "PreparedValidationGraphs", + "prepare_validation_graphs", +] diff --git a/rocrate_validator/requirements/shacl/transformers/registry.py b/rocrate_validator/requirements/shacl/transformers/registry.py new file mode 100644 index 000000000..fe5f8942a --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/registry.py @@ -0,0 +1,110 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Registration metadata for SHACL graph transformers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable, Collection + + from rdflib import URIRef + + from rocrate_validator.requirements.shacl.transformers.base import GraphTransformer, TransformerFunction + + +@dataclass(frozen=True) +class RegisteredTransformer: + """A transformer definition together with its execution metadata.""" + + identifier: URIRef + name: str + order: int + annotation_predicates: frozenset[URIRef] + implementation: TransformerFunction | type[GraphTransformer] + + +# Qualified names make registration idempotent when Python imports or reloads +# a transformer module more than once. A reload replaces the same definition +# instead of scheduling it twice. +_TRANSFORMERS: dict[str, RegisteredTransformer] = {} + + +def register_transformer( + implementation: TransformerFunction | type[GraphTransformer], + identifier: URIRef, + order: int, + annotation_predicates: Collection[URIRef] = (), +) -> None: + """Register one implementation and reject identifier collisions.""" + name = f"{implementation.__module__}.{implementation.__qualname__}" + duplicate = next( + ( + transformer + for registered_name, transformer in _TRANSFORMERS.items() + if transformer.identifier == identifier and registered_name != name + ), + None, + ) + if duplicate is not None: + raise ValueError(f"Graph transformer identifier {identifier} is already registered by {duplicate.name}") + _TRANSFORMERS[name] = RegisteredTransformer( + identifier, + name, + order, + frozenset(annotation_predicates), + implementation, + ) + + +def graph_transformer( + *, + identifier: URIRef, + order: int = 100, + annotation_predicates: Collection[URIRef] = (), +) -> Callable[[TransformerFunction], TransformerFunction]: + """Register a function as a graph transformer.""" + + def decorator(function: TransformerFunction) -> TransformerFunction: + register_transformer(function, identifier, order, annotation_predicates) + return function + + return decorator + + +def get_registered_transformers( + identifiers: Collection[URIRef] | None = None, +) -> tuple[RegisteredTransformer, ...]: + """Return selected transformers in deterministic execution order.""" + transformers = tuple(_TRANSFORMERS.values()) + if identifiers is not None: + requested = set(identifiers) + registered = {item.identifier for item in transformers} + unknown = requested - registered + if unknown: + unknown_list = ", ".join(sorted(map(str, unknown))) + raise ValueError(f"Unknown graph transformer(s): {unknown_list}") + transformers = tuple(item for item in transformers if item.identifier in requested) + return tuple(sorted(transformers, key=lambda item: (item.order, item.name))) + + +__all__ = [ + "RegisteredTransformer", + "get_registered_transformers", + "graph_transformer", + "register_transformer", +] diff --git a/rocrate_validator/requirements/shacl/transformers/vocabulary.py b/rocrate_validator/requirements/shacl/transformers/vocabulary.py index eed89feff..d80c79674 100644 --- a/rocrate_validator/requirements/shacl/transformers/vocabulary.py +++ b/rocrate_validator/requirements/shacl/transformers/vocabulary.py @@ -14,8 +14,10 @@ from rdflib import Namespace, URIRef -# Private vocabulary for annotations that exist only in the transient graph -# passed to pySHACL. These predicates describe validator bookkeeping, not -# RO-Crate metadata, and must never be serialized back into the crate. -MARKER_NS = Namespace("https://github.com/crs4/rocrate-validator/graph-transformers/") -VALIDATION_CANDIDATE_PREDICATE: URIRef = URIRef(MARKER_NS.validationCandidate) +# Shapes request bundled transformers explicitly through this predicate. This +# keeps private annotations and graph-copying overhead out of unrelated SHACL +# validations. +VALIDATOR_NS = Namespace("https://github.com/crs4/rocrate-validator/") +REQUIRES_GRAPH_TRANSFORMER_PREDICATE: URIRef = URIRef(VALIDATOR_NS.requiresGraphTransformer) + +__all__ = ["REQUIRES_GRAPH_TRANSFORMER_PREDICATE"] diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 78dce655e..287f9a220 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any, cast # pylint: disable=unused-import import pyshacl -from pyshacl.rdfutil.load import load_from_source from rdflib import BNode, Graph, Literal, Namespace from rdflib.term import Node, URIRef @@ -43,7 +42,10 @@ ) from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry -from rocrate_validator.requirements.shacl.transformers import prepare_data_graph +from rocrate_validator.requirements.shacl.transformers.preparation import ( + GraphTransformerCompatibilityError, + prepare_validation_graphs, +) from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging from rocrate_validator.utils.rdf import extract_base_from_jsonld @@ -470,33 +472,23 @@ def validate( assert inference in (None, "rdfs", "owlrl", "both"), "Invalid inference option" - if isinstance(self._shapes_graph, Graph): - _inject_default_prefixes(self._shapes_graph) - - # Classify nodes before pySHACL can add ontology or inferred triples. - # Keep the source graph untouched: prepare_data_graph creates the - # transient graph that pySHACL will validate. - if isinstance(data_graph, Graph): - data_graph = prepare_data_graph(cast("Graph", data_graph)) - elif isinstance(data_graph, (str, bytes)) and not kwargs.get("sparql_mode", False): - data_graph = prepare_data_graph( - cast( - "Graph", - load_from_source( - data_graph, - rdf_format=kwargs.get("data_graph_format"), - multigraph=True, - do_owl_imports=False, - logger=cast("Any", logger), - ), - ) - ) - # validate the data graph using pyshacl.validate try: - conforms, results_graph, results_text = pyshacl.validate( + prepared_graphs = prepare_validation_graphs( data_graph, - shacl_graph=self.shapes_graph, + self._shapes_graph, + data_graph_format=kwargs.get("data_graph_format"), + shacl_graph_format=kwargs.get("shacl_graph_format"), + do_owl_imports=kwargs.get("do_owl_imports", False), + inplace=bool(inplace), + sparql_mode=bool(kwargs.get("sparql_mode", False)), + ) + if isinstance(prepared_graphs.shapes_graph, Graph): + _inject_default_prefixes(prepared_graphs.shapes_graph) + + conforms, results_graph, results_text = pyshacl.validate( + prepared_graphs.data_graph, + shacl_graph=prepared_graphs.shapes_graph, ont_graph=self.ont_graph, inference=inference or ("owlrl" if self.ont_graph else None), inplace=inplace, @@ -510,11 +502,10 @@ def validate( debug=False, **kwargs, ) + except GraphTransformerCompatibilityError: + raise except Exception as e: - version = getattr(pyshacl, "__version__", "unknown") - raise SHACLValidationError( - message=(f"SHACL validation could not be executed by pySHACL {version}: {type(e).__name__}: {e}") - ) from e + raise _to_shacl_validation_error(e) from e # log the validation results logger.debug("pyshacl.validate result: Conforms: %r", conforms) logger.debug("pyshacl.validate result: Results Graph: %r", results_graph) @@ -575,4 +566,12 @@ def _inject_default_prefixes(shapes_graph: Graph) -> None: shapes_graph.set((subject, shacl_ns.select, Literal(updated_query))) +def _to_shacl_validation_error(error: Exception) -> SHACLValidationError: + """Wrap graph preparation and engine failures in the public error type.""" + version = getattr(pyshacl, "__version__", "unknown") + return SHACLValidationError( + message=(f"SHACL validation could not be executed by pySHACL {version}: {type(error).__name__}: {error}") + ) + + __all__ = ["SHACLValidationResult", "SHACLValidator", "SHACLViolation"] diff --git a/rocrate_validator/transformers/validation_candidate_marker.py b/rocrate_validator/transformers/validation_candidate_marker.py index 98bd907a7..127c0f55d 100644 --- a/rocrate_validator/transformers/validation_candidate_marker.py +++ b/rocrate_validator/transformers/validation_candidate_marker.py @@ -16,13 +16,18 @@ import logging -from rdflib import RDF, BNode, Graph, Literal, URIRef +from rdflib import RDF, BNode, Dataset, Graph, Literal, Namespace, URIRef from rocrate_validator.requirements.shacl.transformers import graph_transformer -from rocrate_validator.requirements.shacl.transformers.vocabulary import VALIDATION_CANDIDATE_PREDICATE logger = logging.getLogger(__name__) +# Private terms owned by this implementation. They exist only in the transient +# graph passed to pySHACL and are never serialized back into the RO-Crate. +MARKER_NS = Namespace("https://github.com/crs4/rocrate-validator/graph-transformers/") +VALIDATION_CANDIDATE_PREDICATE: URIRef = URIRef(MARKER_NS.validationCandidate) +VALIDATION_CANDIDATE_TRANSFORMER: URIRef = URIRef(MARKER_NS.validationCandidateMarker) + # These predicates point to an identifier, format, or vocabulary term without # claiming that the object is an entity which the crate needs to describe. REFERENCE_PREDICATES: frozenset[URIRef] = frozenset( @@ -39,16 +44,20 @@ # The default-stage order leaves lower values available for transformers that # must normalize or annotate the graph before candidate classification. -@graph_transformer(order=100) +@graph_transformer( + identifier=VALIDATION_CANDIDATE_TRANSFORMER, + order=100, + annotation_predicates=(VALIDATION_CANDIDATE_PREDICATE,), +) def mark_validation_candidates(data_graph: Graph) -> Graph: """ Mark validation candidates identified from the current data graph. Classification is based exclusively on assertions from the original - RO-Crate graph, before pySHACL adds ontology and inferred triples. An IRI is - kept in validation scope when the crate describes it with a predicate other - than ``rdf:type``, or when another entity points to it through a predicate - that claims it as an entity (for example ``schema:author`` or + RO-Crate graph, before pySHACL adds ontology and inferred triples. A node is + kept in validation scope when the crate asserts any outgoing statement + about it, or when another entity points to it through a predicate that + claims it as an entity (for example ``schema:author`` or ``schema:hasPart``). A positive marker keeps the validation scope stable when pySHACL later adds @@ -58,18 +67,31 @@ def mark_validation_candidates(data_graph: Graph) -> Graph: """ described_or_claimed: set[URIRef | BNode] = set() - for subject, predicate, obj in data_graph: - if isinstance(subject, (URIRef, BNode)) and predicate != RDF.type: + triples = ( + ((subject, predicate, obj) for subject, predicate, obj, _ in data_graph.quads()) + if isinstance(data_graph, Dataset) + else iter(data_graph) + ) + for subject, predicate, obj in triples: + if isinstance(subject, (URIRef, BNode)): described_or_claimed.add(subject) if isinstance(obj, (URIRef, BNode)) and predicate not in REFERENCE_PREDICATES: described_or_claimed.add(obj) marker_value = Literal(True) + target_graph = data_graph.default_graph if isinstance(data_graph, Dataset) else data_graph for node in described_or_claimed: - data_graph.add((node, VALIDATION_CANDIDATE_PREDICATE, marker_value)) + target_graph.add((node, VALIDATION_CANDIDATE_PREDICATE, marker_value)) logger.debug( "Prepared validation graph with %d validation-candidate markers", len(described_or_claimed), ) return data_graph + + +__all__ = [ + "VALIDATION_CANDIDATE_PREDICATE", + "VALIDATION_CANDIDATE_TRANSFORMER", + "mark_validation_candidates", +] diff --git a/tests/unit/graph_transformers/test_base.py b/tests/unit/requirements/shacl/transformers/test_base.py similarity index 51% rename from tests/unit/graph_transformers/test_base.py rename to tests/unit/requirements/shacl/transformers/test_base.py index a87ae744e..b49a16c85 100644 --- a/tests/unit/graph_transformers/test_base.py +++ b/tests/unit/requirements/shacl/transformers/test_base.py @@ -17,39 +17,39 @@ import pytest from rdflib import Graph, Literal, Namespace -from rocrate_validator.requirements.shacl.transformers import GraphTransformer, graph_transformer -from rocrate_validator.requirements.shacl.transformers import base as transformer_base -from rocrate_validator.requirements.shacl.transformers.base import ( +from rocrate_validator.requirements.shacl.transformers import GraphTransformer, graph_transformer, registry +from rocrate_validator.requirements.shacl.transformers.pipeline import run_registered_transformers +from rocrate_validator.requirements.shacl.transformers.registry import ( get_registered_transformers, - run_registered_transformers, ) EXAMPLE = Namespace("https://example.org/") def test_abstract_transformers_are_not_registered(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(transformer_base, "_TRANSFORMERS", {}) + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) - class AbstractTransformer(GraphTransformer): + class _AbstractTransformer(GraphTransformer): @abstractmethod def transform(self, data_graph: Graph) -> Graph: raise NotImplementedError - assert get_registered_transformers() == () + assert not get_registered_transformers() def test_registered_transformers_run_in_order(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(transformer_base, "_TRANSFORMERS", {}) + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) trace = EXAMPLE.trace step = EXAMPLE.step - @graph_transformer(order=20) + @graph_transformer(identifier=EXAMPLE.function_transformer, order=20) def function_transformer(data_graph: Graph) -> Graph: assert (trace, step, Literal("class")) in data_graph data_graph.add((trace, step, Literal("function"))) return data_graph - class ClassTransformer(GraphTransformer): + class _ClassTransformer(GraphTransformer): + identifier = EXAMPLE.class_transformer order = 10 def transform(self, data_graph: Graph) -> Graph: @@ -65,12 +65,45 @@ def transform(self, data_graph: Graph) -> Graph: def test_transformer_must_return_an_rdflib_graph(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(transformer_base, "_TRANSFORMERS", {}) + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) - @graph_transformer() + @graph_transformer(identifier=EXAMPLE.invalid_transformer) def invalid_transformer(data_graph: Graph) -> Graph: del data_graph return "not a graph" # type: ignore[return-value] with pytest.raises(TypeError, match="did not return an RDFLib Graph"): run_registered_transformers(Graph()) + + +def test_only_requested_transformers_run(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + + @graph_transformer(identifier=EXAMPLE.first_transformer, order=10) + def first_transformer(data_graph: Graph) -> Graph: + data_graph.add((EXAMPLE.trace, EXAMPLE.step, Literal("first"))) + return data_graph + + @graph_transformer(identifier=EXAMPLE.second_transformer, order=20) + def second_transformer(data_graph: Graph) -> Graph: + data_graph.add((EXAMPLE.trace, EXAMPLE.step, Literal("second"))) + return data_graph + + transformed = run_registered_transformers(Graph(), {EXAMPLE.second_transformer}) + + assert (EXAMPLE.trace, EXAMPLE.step, Literal("first")) not in transformed + assert (EXAMPLE.trace, EXAMPLE.step, Literal("second")) in transformed + + +def test_transformer_identifiers_must_be_unique(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + + @graph_transformer(identifier=EXAMPLE.duplicate) + def first_transformer(data_graph: Graph) -> Graph: + return data_graph + + with pytest.raises(ValueError, match="already registered"): + + @graph_transformer(identifier=EXAMPLE.duplicate) + def second_transformer(data_graph: Graph) -> Graph: + return data_graph diff --git a/tests/unit/requirements/shacl/transformers/test_preparation.py b/tests/unit/requirements/shacl/transformers/test_preparation.py new file mode 100644 index 000000000..2aed569f4 --- /dev/null +++ b/tests/unit/requirements/shacl/transformers/test_preparation.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rdflib import RDF, BNode, Graph, Literal, Namespace +from rdflib.collection import Collection +from rdflib.namespace import SH + +from rocrate_validator.requirements.shacl.transformers import REQUIRES_GRAPH_TRANSFORMER_PREDICATE +from rocrate_validator.requirements.shacl.transformers.preparation import prepare_validation_graphs +from rocrate_validator.transformers.validation_candidate_marker import ( + VALIDATION_CANDIDATE_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, +) + +EXAMPLE = Namespace("https://example.org/") +SCHEMA = Namespace("http://schema.org/") + + +def test_unrequested_transformers_do_not_copy_graph_objects(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + shapes_graph = Graph() + shapes_graph.add((EXAMPLE.shape, RDF.type, SH.NodeShape)) + + prepared = prepare_validation_graphs(data_graph, shapes_graph) + + assert prepared.data_graph is data_graph + assert prepared.shapes_graph is shapes_graph + + +def test_requested_transformers_use_isolated_data_and_shapes_graphs(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + shapes_graph = Graph() + shapes_graph.add((EXAMPLE.shape, RDF.type, SH.NodeShape)) + shapes_graph.add((EXAMPLE.shape, SH.closed, Literal(True))) + target = BNode() + shapes_graph.add((EXAMPLE.shape, SH.target, target)) + shapes_graph.add((target, RDF.type, SH.SPARQLTarget)) + shapes_graph.add((target, SH.select, Literal("SELECT ?this WHERE { ?this schema:name ?name }"))) + shapes_graph.add( + ( + EXAMPLE.shape, + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, + ) + ) + original_data = set(data_graph) + original_shapes = set(shapes_graph) + + prepared = prepare_validation_graphs(data_graph, shapes_graph) + + assert prepared.data_graph is not data_graph + assert prepared.shapes_graph is not shapes_graph + assert (EXAMPLE.entity, VALIDATION_CANDIDATE_PREDICATE, Literal(True)) in prepared.data_graph + assert (EXAMPLE.entity, VALIDATION_CANDIDATE_PREDICATE, None) not in data_graph + assert (EXAMPLE.shape, SH.ignoredProperties, None) not in shapes_graph + assert set(data_graph) == original_data + assert set(shapes_graph) == original_shapes + + assert isinstance(prepared.shapes_graph, Graph) + ignored_head = prepared.shapes_graph.value(EXAMPLE.shape, SH.ignoredProperties) + assert isinstance(ignored_head, BNode) + assert VALIDATION_CANDIDATE_PREDICATE in Collection(prepared.shapes_graph, ignored_head) diff --git a/tests/unit/transformers/test_validation_candidate_marker.py b/tests/unit/transformers/test_validation_candidate_marker.py index 101ec68fc..4da207581 100644 --- a/tests/unit/transformers/test_validation_candidate_marker.py +++ b/tests/unit/transformers/test_validation_candidate_marker.py @@ -13,14 +13,19 @@ # limitations under the License. import pytest -from rdflib import RDF, BNode, Graph, Literal, Namespace, URIRef +from rdflib import RDF, BNode, Dataset, Graph, Literal, Namespace, URIRef from rdflib.namespace import SH from rocrate_validator.requirements.shacl import SHACLValidator +from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.transformers import ( - VALIDATION_CANDIDATE_PREDICATE, + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, prepare_data_graph, ) +from rocrate_validator.transformers.validation_candidate_marker import ( + VALIDATION_CANDIDATE_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, +) SCHEMA = Namespace("http://schema.org/") CSVW = Namespace("http://www.w3.org/ns/csvw#") @@ -74,6 +79,17 @@ def test_described_subject_remains_in_validation_scope(): assert not _is_validation_candidate(validation_graph, SCHEMA.Person) +@pytest.mark.parametrize("subject", [EXAMPLE.type_only, BNode("type-only")]) +def test_type_only_subject_remains_in_validation_scope(subject: URIRef | BNode): + data_graph = Graph() + data_graph.add((subject, RDF.type, SCHEMA.Person)) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, subject) + assert not _is_validation_candidate(validation_graph, SCHEMA.Person) + + def test_claimed_vocabulary_term_is_a_validation_candidate(): data_graph = Graph() data_graph.add((EXAMPLE.entity, SCHEMA.author, EXAMPLE.ontology_term)) @@ -111,6 +127,7 @@ def test_shacl_validator_prepares_graph_before_validation(serialized: bool): property_shape = BNode() shapes_graph = Graph() shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) shapes_graph.add((shape, SH.target, target)) shapes_graph.add( ( @@ -140,3 +157,160 @@ def test_shacl_validator_prepares_graph_before_validation(serialized: bool): assert not result.conforms assert any(violation.focusNode == person for violation in result.violations) assert set(data_graph) == original_triples + + +def test_serialized_shapes_can_request_graph_transformers(): + shape = EXAMPLE.marker_shape + target = BNode() + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + shapes_graph.add((shape, SH.target, target)) + shapes_graph.add((target, RDF.type, SH.SPARQLTarget)) + shapes_graph.add( + ( + target, + SH.select, + Literal(f"SELECT ?this WHERE {{ ?this <{VALIDATION_CANDIDATE_PREDICATE}> true }}"), + ) + ) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.name)) + shapes_graph.add((property_shape, SH.minCount, Literal(1))) + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.author, EXAMPLE.person)) + + result = SHACLValidator(shapes_graph=shapes_graph.serialize(format="turtle").encode()).validate( + data_graph, + shacl_graph_format="turtle", + ) + + assert not result.conforms + assert any(violation.focusNode in {EXAMPLE.entity, EXAMPLE.person} for violation in result.violations) + + +def _closed_shape() -> Graph: + shape = EXAMPLE.closed_shape + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.targetNode, EXAMPLE.entity)) + shapes_graph.add((shape, SH.closed, Literal(True))) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.name)) + return shapes_graph + + +def test_unrequested_transformers_do_not_change_closed_shapes(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + + result = SHACLValidator(shapes_graph=_closed_shape()).validate(data_graph) + + assert result.conforms + + +def test_transformer_annotations_are_ignored_by_closed_shapes(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + shapes_graph = _closed_shape() + shapes_graph.add( + ( + EXAMPLE.closed_shape, + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, + ) + ) + + result = SHACLValidator(shapes_graph=shapes_graph).validate(data_graph) + + assert result.conforms + + +def test_dataset_named_graphs_are_preserved_during_preparation(): + data_graph = Dataset() + data_graph.graph(EXAMPLE.named_graph).add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + original_quads = set(data_graph.quads((None, None, None, None))) + shape = EXAMPLE.dataset_shape + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.targetSubjectsOf, SCHEMA.name)) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.description)) + shapes_graph.add((property_shape, SH.minCount, Literal(1))) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + result = SHACLValidator(shapes_graph=shapes_graph).validate(data_graph) + + assert not result.conforms + assert any(violation.focusNode == EXAMPLE.entity for violation in result.violations) + assert set(data_graph.quads((None, None, None, None))) == original_quads + assert not data_graph.default_union + + +def test_serialized_parse_errors_keep_shacl_error_contract(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + with pytest.raises(SHACLValidationError, match="BadSyntax") as exc_info: + SHACLValidator(shapes_graph=shapes_graph).validate( + b"@prefix broken", + data_graph_format="turtle", + ) + + assert exc_info.value.__cause__ is not None + + +def test_inplace_remains_effective_without_requested_transformers(): + shape = EXAMPLE.rule_shape + rule = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.targetNode, EXAMPLE.entity)) + shapes_graph.add((shape, SH.rule, rule)) + shapes_graph.add((rule, RDF.type, SH.TripleRule)) + shapes_graph.add((rule, SH.subject, SH.this)) + shapes_graph.add((rule, SH.predicate, EXAMPLE.generated)) + shapes_graph.add((rule, SH.object, Literal("yes"))) + data_graph = Graph() + + SHACLValidator(shapes_graph=shapes_graph).validate(data_graph, inplace=True) + + assert (EXAMPLE.entity, EXAMPLE.generated, Literal("yes")) in data_graph + + +def test_inplace_is_rejected_for_transformer_dependent_shapes(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + with pytest.raises(ValueError, match="inplace=True"): + SHACLValidator(shapes_graph=shapes_graph).validate(Graph(), inplace=True) + + +def test_sparql_mode_is_rejected_for_transformer_dependent_shapes(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + with pytest.raises(ValueError, match="sparql_mode"): + SHACLValidator(shapes_graph=shapes_graph).validate( + "https://example.org/sparql", + sparql_mode=True, + ) + + +def test_unknown_requested_transformer_fails_validation(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, EXAMPLE.unknown_transformer)) + + with pytest.raises(SHACLValidationError, match="Unknown graph transformer"): + SHACLValidator(shapes_graph=shapes_graph).validate(Graph()) From e78b4fd41e7c88c7adadc11b854d8081694a32eb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 17 Sep 2026 05:40:36 +0200 Subject: [PATCH 073/271] test(profiles): :white_check_mark: cover preprocessing candidate scope --- .../invalid/ro-crate-metadata.json | 113 ++++++++++++++++ .../valid/ro-crate-metadata.json | 124 ++++++++++++++++++ .../ro-crate-1.2/test_metadata_entities.py | 93 +++++++++++++ tests/ro_crates_v1_2.py | 8 ++ 4 files changed, 338 insertions(+) create mode 100644 tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json diff --git a/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..c321cda85 --- /dev/null +++ b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.2/context", + { + "propertyUrl": "http://www.w3.org/ns/csvw#propertyUrl" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid preprocessing candidate fixture", + "description": "A crate with claimed and type-only entities missing required metadata.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0009-0000-5074-6239" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://vocab.example.org/identifier-scheme" + }, + "inDefinedTermSet": { + "@id": "https://vocab.example.org/identifier-vocabulary" + }, + "encodingFormat": { + "@id": "https://vocab.example.org/formats/uuid" + }, + "conformsTo": { + "@id": "https://vocab.example.org/profiles/property-value" + }, + "propertyUrl": { + "@id": "https://vocab.example.org/properties/uuid" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://orcid.org/0009-0000-5074-6239", + "@type": "Person", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#type-only-person", + "@type": "Person" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org/research", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "Example Funding Agency", + "url": "https://example.org/funding", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example contact", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Funding contact", + "email": "mailto:funding@example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon the work." + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json new file mode 100644 index 000000000..18fb33168 --- /dev/null +++ b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.2/context", + { + "customPredicate": "https://vocab.example.org/property/customPredicate", + "propertyUrl": "http://www.w3.org/ns/csvw#propertyUrl" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Preprocessing candidate fixture", + "description": "A valid crate covering cited-only and claim-referenced identifiers.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0009-0000-5074-6239" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "customPredicate": { + "@id": "https://vocab.example.org/entities/described" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://vocab.example.org/identifier-scheme" + }, + "inDefinedTermSet": { + "@id": "https://vocab.example.org/identifier-vocabulary" + }, + "encodingFormat": { + "@id": "https://vocab.example.org/formats/uuid" + }, + "conformsTo": { + "@id": "https://vocab.example.org/profiles/property-value" + }, + "propertyUrl": { + "@id": "https://vocab.example.org/properties/uuid" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://vocab.example.org/entities/described", + "@type": [ + "Thing", + "https://vocab.example.org/types/CustomEntity" + ], + "name": "Described vocabulary entity", + "description": "This entity belongs to an unlisted namespace but is described by the crate.", + "customPredicate": "custom value" + }, + { + "@id": "https://orcid.org/0009-0000-5074-6239", + "@type": "Person", + "name": "Example Author", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org/research", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "Example Funding Agency", + "url": "https://example.org/funding", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example contact", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Funding contact", + "email": "mailto:funding@example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon the work." + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py index 3637c2d0c..57bce1c1d 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py @@ -15,7 +15,11 @@ import json import logging +from rdflib import Graph, Literal, URIRef + from rocrate_validator import models, services +from rocrate_validator.requirements.shacl.transformers import prepare_data_graph +from rocrate_validator.transformers.validation_candidate_marker import VALIDATION_CANDIDATE_PREDICATE from tests.ro_crates_v1_2 import MetadataEntities from tests.shared import do_entity_test @@ -128,3 +132,92 @@ def test_orcid_person_claimed_as_author_remains_in_validation_scope(): issue.violatingEntity == person_id and issue.message == "Entities SHOULD have a human-readable name" for issue in result.get_issues(models.Severity.RECOMMENDED) ) + + +def test_type_only_blank_node_remains_in_contextual_entity_scope(): + """A blank node asserted by the crate is not an external vocabulary term.""" + rocrate_path = __metadata_entities__.valid_recommended_schema_type + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + metadata["@graph"].append({"@id": "_:type-only-person", "@type": "Person"}) + + result = _validate_metadata(rocrate_path, metadata) + + assert any( + issue.violatingEntity == "type-only-person" + and issue.message == "A Contextual Entity MUST have a `name` property" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + assert any( + issue.violatingEntity == "type-only-person" + and issue.message == "Contextual entities SHOULD be referenced by other entities." + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + + +def test_valid_preprocessing_candidates_fixture_keeps_citations_out_of_scope(): + """Cited-only vocabulary IRIs are ignored while described claims remain valid.""" + rocrate_path = __metadata_entities__.valid_preprocessing_candidates + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + + result = _validate_metadata(rocrate_path, metadata) + + assert result.passed() + cited_only_ids = { + "https://vocab.example.org/identifier-scheme", + "https://vocab.example.org/identifier-vocabulary", + "https://vocab.example.org/formats/uuid", + "https://vocab.example.org/profiles/property-value", + "https://vocab.example.org/properties/uuid", + "https://vocab.example.org/types/CustomEntity", + } + assert not any(issue.violatingEntity in cited_only_ids for issue in result.issues) + + +def test_preprocessing_candidates_fixture_marks_claims_and_descriptions_only(): + """The fixture exercises both positive and cited-only preprocessing paths.""" + rocrate_path = __metadata_entities__.valid_preprocessing_candidates + metadata = (rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8") + graph = Graph().parse( + data=metadata, + format="json-ld", + publicID="https://example.org/preprocessing-candidates/ro-crate-metadata.json", + ) + prepared_graph = prepare_data_graph(graph) + + def is_marked(identifier: str) -> bool: + return ( + URIRef(identifier), + VALIDATION_CANDIDATE_PREDICATE, + Literal(True), + ) in prepared_graph + + assert is_marked("https://orcid.org/0009-0000-5074-6239") + assert is_marked("https://vocab.example.org/entities/described") + assert not is_marked("https://vocab.example.org/identifier-scheme") + assert not is_marked("https://vocab.example.org/identifier-vocabulary") + assert not is_marked("https://vocab.example.org/formats/uuid") + assert not is_marked("https://vocab.example.org/profiles/property-value") + assert not is_marked("https://vocab.example.org/properties/uuid") + assert not is_marked("https://vocab.example.org/types/CustomEntity") + assert not is_marked("https://vocab.example.org/property/customPredicate") + + +def test_invalid_preprocessing_candidates_fixture_keeps_incomplete_entities_in_scope(): + """Claimed and type-only entities remain subject to metadata checks.""" + rocrate_path = __metadata_entities__.invalid_preprocessing_candidates + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + author_id = "https://orcid.org/0009-0000-5074-6239" + + result = _validate_metadata(rocrate_path, metadata) + + assert not result.passed() + assert any( + issue.violatingEntity == author_id and issue.message == "Entities SHOULD have a human-readable name" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + assert any( + issue.violatingEntity.endswith("#type-only-person") + and issue.message == "Entities SHOULD have a human-readable name" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + assert not any(issue.violatingEntity == "https://vocab.example.org/identifier-scheme" for issue in result.issues) diff --git a/tests/ro_crates_v1_2.py b/tests/ro_crates_v1_2.py index 18e5491d0..083021a87 100644 --- a/tests/ro_crates_v1_2.py +++ b/tests/ro_crates_v1_2.py @@ -192,6 +192,14 @@ def valid_recommended_name(self) -> Path: def invalid_recommended_name(self) -> Path: return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "invalid" + @property + def valid_preprocessing_candidates(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "preprocessing_candidates" / "valid" + + @property + def invalid_preprocessing_candidates(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "preprocessing_candidates" / "invalid" + class MetadataDescriptor: METADATA_DESCRIPTOR_CRATES_PATH = BASE_PATH / "6_metadata_descriptor" From fc455598486d4288ce39f81abb82ea6da0813ed7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 17 Sep 2026 05:40:54 +0200 Subject: [PATCH 074/271] docs(transformers): :memo: describe SHACL engine and implementation catalog --- docs/14_graph_transformers.rst | 134 +++++++++++++++--- .../ro-crate/1.2/should/0_entity_metadata.ttl | 4 +- 2 files changed, 113 insertions(+), 25 deletions(-) diff --git a/docs/14_graph_transformers.rst b/docs/14_graph_transformers.rst index aa2aa8ab1..d7f002931 100644 --- a/docs/14_graph_transformers.rst +++ b/docs/14_graph_transformers.rst @@ -187,21 +187,61 @@ the transient copy is not inferred from ontology triples. How the pipeline works ---------------------- -``prepare_data_graph`` creates one transient copy of the parsed RO-Crate graph, -discovers all modules below -``rocrate_validator.transformers``, and runs -their registered transformers sequentially. The cached source graph is never -passed to a transformer and remains unchanged. - -The validator skips this preprocessing when pySHACL is called with -``sparql_mode=True``. Transformer-dependent validation therefore requires the -normal in-memory or serialized-graph validation mode. +Shapes explicitly declare the transformers they require with +``validator:requiresGraphTransformer``. If there are no such declarations, the +validator delegates the original graph to pySHACL without copying or annotating +it. This keeps ordinary SHACL profiles isolated from the preprocessing +extension. + +When at least one transformer is requested, ``prepare_data_graph`` creates one +transient copy of the parsed RO-Crate graph, discovers the bundled modules below +``rocrate_validator.transformers``, and runs only the requested +transformers. The cached source graph is never passed to a transformer and +remains unchanged. RDFLib datasets are copied as datasets, including their +default and named graphs. + +Transformer-dependent validation rejects ``sparql_mode=True`` because a remote +endpoint cannot receive transient local annotations. It also rejects +``inplace=True``: mutating a caller-owned graph would leak private annotations. +Both options retain their normal pySHACL behaviour when no transformer is +requested. Lower ``order`` values run first. Transformers with the same order are sorted by their fully qualified Python name, so their order is deterministic but should not be used to encode an implicit dependency. Assign distinct orders when one transformer consumes triples produced by another. +Package structure +~~~~~~~~~~~~~~~~~ + +The engine belongs to the SHACL validation subsystem and is contained in +``rocrate_validator.requirements.shacl.transformers``: + +* ``base.py`` defines the abstract transformer contract; +* ``registry.py`` owns registration metadata and deterministic ordering; +* ``discovery.py`` imports bundled transformer modules once, with locking for + concurrent first use; +* ``pipeline.py`` copies data graphs without flattening datasets and executes + the requested transformer subset; +* ``preparation.py`` is the integration facade used by ``SHACLValidator``. It + reads shape dependencies, validates incompatible pySHACL options, prepares + private data and shapes graphs, and protects closed shapes; +* ``vocabulary.py`` owns the RDF predicate through which shapes request a + transformer. + +Concrete implementations live directly in the sibling catalog package +``rocrate_validator.transformers``. This mirrors the visibility of the bundled +``profiles`` directory while keeping executable transformer code separate from +profile resources and from the SHACL engine. + +``SHACLValidator`` calls ``prepare_validation_graphs`` and does not implement +transformer discovery or transformer-specific graph mutation itself. This +boundary keeps the feature reusable internally without turning it into a +pySHACL rule language or a third-party plugin API. + +Generic SHACL concerns, such as injecting the validator's default SPARQL +prefixes and wrapping pySHACL failures, remain in ``SHACLValidator``. + Developing transformers ----------------------- @@ -215,15 +255,15 @@ Every transformer must: * accept exactly one ``rdflib.Graph``; * return an ``rdflib.Graph`` (returning ``None`` is an error); +* define a stable, globally unique RDF identifier; * be deterministic for the same input graph; * avoid network access and process-global mutable state; * treat the graph as transient validator state, never as metadata to write back to the RO-Crate. -The discovery mechanism covers modules bundled in the sibling catalog package -``rocrate_validator.transformers``. The engine remains in -``rocrate_validator.requirements.shacl.transformers``. This is not an -entry-point system for arbitrary profile-local or third-party plugins. +The discovery mechanism covers modules bundled in +``rocrate_validator.transformers``. It is not an entry-point system for +arbitrary profile-local or third-party plugins. Function transformers ~~~~~~~~~~~~~~~~~~~~~ @@ -233,12 +273,23 @@ expressed as a function: .. code-block:: python - from rdflib import Graph + from rdflib import Graph, URIRef from rocrate_validator.requirements.shacl.transformers import graph_transformer + EXAMPLE_TRANSFORMER = URIRef( + "https://github.com/crs4/rocrate-validator/graph-transformers/example" + ) + EXAMPLE_ANNOTATION = URIRef( + "https://github.com/crs4/rocrate-validator/graph-transformers/exampleAnnotation" + ) - @graph_transformer(order=50) + + @graph_transformer( + identifier=EXAMPLE_TRANSFORMER, + order=50, + annotation_predicates=(EXAMPLE_ANNOTATION,), + ) def add_example_annotations(data_graph: Graph) -> Graph: # Add annotations to the transient graph. ... @@ -255,12 +306,15 @@ private helper methods or a clearer class-level contract: .. code-block:: python - from rdflib import Graph + from rdflib import Graph, URIRef from rocrate_validator.requirements.shacl.transformers import GraphTransformer class ExampleTransformer(GraphTransformer): + identifier = URIRef( + "https://github.com/crs4/rocrate-validator/graph-transformers/example" + ) order = 60 def transform(self, data_graph: Graph) -> Graph: @@ -272,16 +326,49 @@ constructor; a new instance is created for each pipeline execution so state does not leak between validations. Abstract intermediate subclasses are not registered. +Requesting a transformer from shapes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A registered transformer is not executed merely because its module exists. +Every shape which relies on it declares the dependency explicitly: + +.. code-block:: turtle + + @prefix ex: . + @prefix sh: . + @prefix validator: . + @prefix validator-transformers: + . + + ex:ExampleShape + a sh:NodeShape ; + validator:requiresGraphTransformer + validator-transformers:example ; + # normal targets and constraints follow + . + +Dependencies are collected from the complete merged shapes graph. Repeating a +declaration is harmless, and inherited or additional profiles participate in +the same request set. An unknown identifier fails validation instead of being +silently ignored. + Private marker predicates and prefixes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A transformer may annotate the transient graph with private predicates. Keep these predicates in the validator-owned namespace ``https://github.com/crs4/rocrate-validator/graph-transformers/`` and define -their RDFLib vocabulary terms in -``requirements/shacl/transformers/vocabulary.py``. These triples are -implementation details: they must not be serialized into an RO-Crate or -presented as vocabulary terms owned by the crate. +their RDFLib vocabulary terms next to the concrete implementation in +``rocrate_validator.transformers``. These triples are implementation details: +they must not be serialized into an RO-Crate or presented as vocabulary terms +owned by the crate. + +Declare every produced predicate through the transformer's +``annotation_predicates`` metadata. Before validation, the orchestrator adds +those predicates to ``sh:ignoredProperties`` on a transient copy of every +``sh:closed true`` shape. This preserves closed-shape semantics without +mutating the source shapes graph. SPARQL constraints which enumerate arbitrary +predicates should still exclude the validator-owned namespace explicitly. When a SHACL query consumes a marker, declare the namespace in the profile's shared ``sh:prefixes`` block. For example: @@ -299,9 +386,7 @@ graph: .. code-block:: sparql - FILTER EXISTS { - ?this validator-transformers:validationCandidate true - } + ?this validator-transformers:validationCandidate true . Testing a transformer ~~~~~~~~~~~~~~~~~~~~~ @@ -310,7 +395,10 @@ Add unit tests for the transformation itself and integration tests for every SHACL target which consumes its output. At minimum, verify that: * the source graph is unchanged; +* profiles which do not request the transformer are unchanged; * consecutive transformers see their predecessors' output; * ordering is explicit when transformers depend on each other; * ontology and inference triples do not accidentally enter the intended scope; +* private predicates do not invalidate closed shapes; +* RDFLib datasets preserve default and named graph data; * representative large graphs remain within the project's performance budget. diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl index b56530970..f92c1e9c0 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl @@ -40,8 +40,8 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; FILTER (?this != ?root) # Exclude entities with non-IRI identifiers. FILTER (isIRI(?this)) - # Profile descriptors use prof:Profile rather than a Schema.org - # type and have their own dedicated metadata requirements. + # W3C Profile Vocabulary descriptors are not ordinary Schema.org + # metadata entities; they have dedicated profile-specific rules. FILTER NOT EXISTS { ?this a } } """ ; From 72bcc55b5a08eff5fe18b63634942598ddc4e7ee Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 17 Sep 2026 14:56:08 +0200 Subject: [PATCH 075/271] =?UTF-8?q?test:=20=F0=9F=A7=AA=20update=20RO-Crat?= =?UTF-8?q?e=201.2=20context=20check=20identifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/shared.py | 2 +- tests/unit/test_default_profile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/shared.py b/tests/shared.py index 257cdc950..89b50beb6 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -111,7 +111,7 @@ def _context_uri_for_profile(profile_identifier: str) -> str: Return the JSON-LD context matching the profile under test. Keeping the context aligned with the profile matters: re-serialising a 1.1 - context while validating against 1.2 trips the `ro-crate-1.2_2.2` check and + context while validating against 1.2 trips the `ro-crate-1.2_4.2` check and the test would fail for a reason unrelated to what it is asserting. """ return _PROFILE_CONTEXT_URIS.get(profile_identifier, _DEFAULT_CONTEXT_URI) diff --git a/tests/unit/test_default_profile.py b/tests/unit/test_default_profile.py index 1e23c5989..3e27e3f7e 100644 --- a/tests/unit/test_default_profile.py +++ b/tests/unit/test_default_profile.py @@ -106,7 +106,7 @@ def test_default_profile_rejects_a_1_1_context(): result = validate(settings) assert not result.passed() failed_check_ids = {issue.check.identifier for issue in result.get_issues()} - assert "ro-crate-1.2_2.2" in failed_check_ids, ( + assert "ro-crate-1.2_4.2" in failed_check_ids, ( f"Expected the 1.2 context check to fail, got: {sorted(failed_check_ids)}" ) From 5f2f9dc5d22e8f7b6b9b5582a02a623ef66c9767 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 17 Sep 2026 15:15:45 +0200 Subject: [PATCH 076/271] port updates from #207 and #208 to ro-crate 1.3 --- .../1.3/must/0_file_descriptor_format.py | 98 ++++++++++++------- .../ro-crate/1.3/must/1_ro_crate_preview.py | 8 +- .../1.3/must/2_root_data_entity_cite_as.py | 8 +- .../1.3/must/2_root_data_entity_identifier.py | 8 +- .../1.3/must/2_root_identifier_property.py | 6 +- .../1.3/must/4_data_entity_metadata.py | 42 ++++---- .../1.3/should/0_entity_identifier_format.py | 14 +-- .../1.3/should/1_file_descriptor_name.py | 6 +- .../should/2_root_data_entity_identifier.py | 26 ++--- .../1.3/should/4_data_entity_license.py | 8 +- .../1.3/should/4_data_entity_metadata.py | 24 ++--- .../1.3/should/4_missing_file_local_path.py | 10 +- .../1.3/should/4_referenced_rocrate.py | 10 +- .../1.3/should/4_web_dataset_distribution.py | 8 +- 14 files changed, 150 insertions(+), 126 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py index 1f5c673ca..2ce458cd7 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py @@ -19,7 +19,7 @@ from urllib.parse import urljoin from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester @@ -59,27 +59,27 @@ class FileDescriptorExistence(PyFunctionCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Descriptor Existence") - def test_existence(self, context: ValidationContext) -> bool: + def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) return False return True - @check(name="File Descriptor size check") - def test_size(self, context: ValidationContext) -> bool: + @check(name="File Descriptor size check", depends_on=("File Descriptor Existence",)) + def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False @@ -92,14 +92,14 @@ class FileDescriptorEncodingCheck(PyFunctionCheck): The file descriptor MUST be UTF-8 encoded """ - @check(name="File Descriptor UTF-8 encoding") - def check(self, context: ValidationContext) -> bool: + @check(name="File Descriptor UTF-8 encoding", depends_on=("File Descriptor Existence",)) + def check(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is UTF-8 encoded """ if context.settings.metadata_only: logger.debug("Skipping file descriptor encoding check in metadata-only mode") - return True + return CheckResult.SKIPPED try: raw_data = context.ro_crate.metadata.as_bytes() if isinstance(raw_data, str): @@ -113,7 +113,7 @@ def check(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED @requirement(name="File Descriptor JSON format") @@ -122,8 +122,8 @@ class FileDescriptorJsonFormat(PyFunctionCheck): The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor JSON format") - def check(self, context: ValidationContext) -> bool: + @check(name="File Descriptor JSON format", depends_on=("File Descriptor UTF-8 encoding",)) + def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) @@ -142,7 +142,7 @@ def check(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self @@ -251,8 +251,11 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) # return if the context is valid return is_valid - @check(name="File Descriptor @context property validation") - def check_context(self, context: ValidationContext) -> bool: + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) + def check_context(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor contains the @context property and it is a valid JSON-LD context @@ -287,7 +290,7 @@ def has_expected_context(ctx: object) -> bool: return self.__check_contexts__(context, json_dict["@context"]) except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -369,8 +372,11 @@ def _is_entity_flat(self, context: ValidationContext, entity: Any, is_first: boo return False return result - @check(name="File Descriptor JSON-LD must be flattened") - def check_flattened(self, context: ValidationContext) -> bool: + @check( + name="File Descriptor JSON-LD must be flattened", + depends_on=("File Descriptor JSON format",), + ) + def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" try: fail_fast = bool(context.settings.abort_on_first) @@ -389,14 +395,17 @@ def check_flattened(self, context: ValidationContext) -> bool: return result except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of the @id property of the file descriptor entities") - def check_identifiers(self, context: ValidationContext) -> bool: + @check( + name="Validation of the @id property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -412,14 +421,17 @@ def check_identifiers(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of the @type property of the file descriptor entities") - def check_types(self, context: ValidationContext) -> bool: + @check( + name="Validation of the @type property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) + def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -435,14 +447,17 @@ def check_types(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor type check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of unique @id values") - def check_unique_identifiers(self, context: ValidationContext) -> bool: + @check( + name="Validation of unique @id values", + depends_on=("File Descriptor JSON format",), + ) + def check_unique_identifiers(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() identifiers = [entity.get("@id") for entity in json_dict.get("@graph", [])] @@ -455,7 +470,7 @@ def check_unique_identifiers(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping unique identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -485,8 +500,11 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke return message return None - @check(name="Validation of entity references") - def check_entity_references(self, context: ValidationContext) -> bool: + @check( + name="Validation of entity references", + depends_on=("File Descriptor JSON format",), + ) + def check_entity_references(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() graph = json_dict.get("@graph", []) @@ -504,14 +522,17 @@ def check_entity_references(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping entity reference check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of subject and keyword properties") - def check_subject_keywords(self, context: ValidationContext) -> bool: + @check( + name="Validation of subject and keyword properties", + depends_on=("File Descriptor JSON format",), + ) + def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() graph = json_dict.get("@graph", []) @@ -526,7 +547,7 @@ def check_subject_keywords(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping subject and keyword check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") @@ -599,8 +620,11 @@ def add_unexpected_key(k: str, u_keys: dict) -> None: return unexpected_keys - @check(name="Validation of the compaction format of the file descriptor") - def check_compaction(self, context: ValidationContext) -> bool: + @check( + name="Validation of the compaction format of the file descriptor", + depends_on=("File Descriptor JSON format",), + ) + def check_compaction(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -646,7 +670,7 @@ def check_compaction(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py index 1c7c0b79f..baa7274d6 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py @@ -15,7 +15,7 @@ from pathlib import Path from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,13 +30,13 @@ class ROCrateWebsiteChecker(PyFunctionCheck): """ @check(name="RO-Crate Website HTML5 doctype") - def check_preview_html(self, context: ValidationContext) -> bool: + def check_preview_html(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED except ROCrateMetadataNotFoundError: logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): return True diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py index 8625a6fa3..22e13f4f5 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -51,13 +51,13 @@ def _resolve_cite_as_url(cite_as_raw: object) -> str | None: return cite_as_url @check(name="Root Data Entity: `cite-as` MUST reference a downloadable item") - def check_cite_as_downloadable(self, context: ValidationContext) -> bool: + def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -84,7 +84,7 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> bool: except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py index cfc84fa3d..354b9ee11 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py @@ -15,7 +15,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,10 +29,10 @@ class RootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: REQUIRED value") - def check_identifier(self, context: ValidationContext) -> bool: + def check_identifier(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if root_entity.id == "./": return True @@ -45,7 +45,7 @@ def check_identifier(self, context: ValidationContext) -> bool: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py index ace24b4d0..d89f405eb 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,7 +28,7 @@ class RootIdentifierPropertyChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier PropertyValue value") - def check_identifier_values(self, context: ValidationContext) -> bool: + def check_identifier_values(self, context: ValidationContext) -> CheckResultValue: try: root = context.ro_crate.metadata.get_root_data_entity() identifiers = root.get_property("identifier") @@ -46,7 +46,7 @@ def check_identifier_values(self, context: ValidationContext) -> bool: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py index 85207aade..5b2daa42b 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py @@ -16,7 +16,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -32,7 +32,7 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: # noqa: C901 + def check_availability(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """ Check the presence of the Data Entity in the RO-Crate """ @@ -40,21 +40,21 @@ def check_availability(self, context: ValidationContext) -> bool: # noqa: C901 is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if is_detached: logger.debug("Skipping data entity payload checks for detached RO-Crate") - return True + return CheckResult.SKIPPED # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED # Perform the check result = True try: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) @@ -104,14 +104,14 @@ class DetachedDataEntityChecker(PyFunctionCheck): """ @check(name="Detached RO-Crate: data entities MUST be web-based") - def check_detached_entities(self, context: ValidationContext) -> bool: + def check_detached_entities(self, context: ValidationContext) -> CheckResultValue: try: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if not is_detached: - return True + return CheckResult.SKIPPED result = True root_entity_id = None with contextlib.suppress(Exception): @@ -120,7 +120,7 @@ def check_detached_entities(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: continue @@ -144,7 +144,7 @@ class DataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 result = True root_entity_id = None root_entity_is_local = False @@ -160,7 +160,7 @@ def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: continue @@ -197,20 +197,20 @@ def check_identifiers(self, context: ValidationContext) -> bool: # noqa: C901 return result @check(name="Data Entity: relative @id for payload files") - def check_relative_paths(self, context: ValidationContext) -> bool: + def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: try: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if is_detached: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if entity.has_local_identifier() or entity.is_remote(): continue @@ -233,13 +233,13 @@ class DataEntityCitationChecker(PyFunctionCheck): """ @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> bool: + def check_citation(self, context: ValidationContext) -> CheckResultValue: result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") if citations is None: @@ -289,19 +289,19 @@ def _not_downloadable_message(entity_id: str, dl) -> str: return msg @check(name="Web-based Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip directory URIs: assumed available, not directly downloadable by spec diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py index 276cf9fc0..3aa9875ec 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py @@ -23,7 +23,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -47,7 +47,7 @@ class EntityIdentifierFormatChecker(PyFunctionCheck): """ @check(name="Entity identifiers SHOULD NOT use ../", severity=Severity.RECOMMENDED) - def check_no_parent_traversal(self, context: ValidationContext) -> bool: + def check_no_parent_traversal(self, context: ValidationContext) -> CheckResultValue: """ @id paths SHOULD NOT use `../` to climb out of the RO-Crate Root (RO-Crate 1.3, JSON-LD appendix). @@ -57,7 +57,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping parent traversal check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") if "../" in entity_id: @@ -75,7 +75,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: name="Entity identifiers SHOULD use native UTF-8, not percent-encoding", severity=Severity.RECOMMENDED, ) - def check_utf8_identifiers(self, context: ValidationContext) -> bool: + def check_utf8_identifiers(self, context: ValidationContext) -> CheckResultValue: """ International characters in @id values SHOULD be written in native UTF-8 rather than percent-encoded (RO-Crate 1.3, JSON-LD appendix). @@ -85,7 +85,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") if _PCT_NON_ASCII_RE.search(entity_id): @@ -104,7 +104,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: name="Contextual entity @id SHOULD be absolute URI or '#'-prefixed", severity=Severity.RECOMMENDED, ) - def check_named_entity_id_format(self, context: ValidationContext) -> bool: + def check_named_entity_id_format(self, context: ValidationContext) -> CheckResultValue: """ Any Contextual Entity (Person, Organization, ContactPoint, PropertyValue, Place, etc.) SHOULD use an @id that is an absolute URI (permalink), @@ -125,7 +125,7 @@ def check_named_entity_id_format(self, context: ValidationContext) -> bool: entities = ro_crate_metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping named entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") if entity_id in non_contextual_ids: diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py index e7034f3d1..d4ebc2b8c 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py @@ -13,7 +13,7 @@ # limitations under the License. -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,7 +29,7 @@ class FileDescriptorExistence(PyFunctionCheck): """ @check(name="Detached RO-Crate file descriptor RECOMMENDED naming convention") - def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: + def test_detached_descriptor_filename(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor of a Detached RO-Crate exists and is named according to the convention. In a Detached RO-Crate, the file descriptor SHOULD be named `{prefix}-ro-crate-metadata.json`, @@ -37,7 +37,7 @@ def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py index 1f0fce210..f1249b2d7 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -29,16 +29,16 @@ class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED identifier") - def check_identifier(self, context: ValidationContext) -> bool: # noqa: PLR0911 + def check_identifier(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 """ In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. """ try: if not context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): - return True + return CheckResult.SKIPPED if root_entity.id == "./": context.result.add_issue( @@ -54,7 +54,7 @@ def check_identifier(self, context: ValidationContext) -> bool: # noqa: PLR0911 return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False @@ -68,17 +68,17 @@ class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: use cite-as for resolvable identifiers") - def check_cite_as_reference(self, context: ValidationContext) -> bool: # noqa: PLR0911 + def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 """ If the Root Data Entity has a resolvable identifier, it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. """ try: if not context.ro_crate.is_detached(): - return True + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): - return True + return CheckResult.SKIPPED # Check if the `cite-as` property is present and references the Root Data Entity cite_as = root_entity.get_property("cite-as") @@ -102,7 +102,7 @@ def check_cite_as_reference(self, context: ValidationContext) -> bool: # noqa: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) return False @@ -147,11 +147,11 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) - def check_identifier_resolvable(self, context: ValidationContext) -> bool: # noqa: PLR0911 + def check_identifier_resolvable(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 if context.settings.skip_availability_check: - return True + return CheckResult.SKIPPED if context.settings.metadata_only: - return True + return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() identifier_raw = root_entity.get_property("identifier") @@ -178,7 +178,7 @@ def check_identifier_resolvable(self, context: ValidationContext) -> bool: # no return result except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED except Exception as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py index 42b86616d..67edde9da 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py @@ -47,7 +47,7 @@ # valid crates. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -101,12 +101,12 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> bool: # noqa: C901 + def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() except Exception: - return True + return CheckResult.SKIPPED if root_entity is None: return True @@ -122,7 +122,7 @@ def check_license_divergence(self, context: ValidationContext) -> bool: # noqa: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity license check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if entity.id == root_entity.id: continue diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py index ab2124b61..3b0825ab6 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py @@ -15,7 +15,7 @@ import re from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -31,13 +31,13 @@ class DataEntityCitationChecker(PyFunctionCheck): """ @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> bool: + def check_citation(self, context: ValidationContext) -> CheckResultValue: result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") if citations is None: @@ -87,20 +87,20 @@ def _not_downloadable_message(entity_id: str, dl) -> str: return msg @check(name="Web-based Data Entity: RECOMMENDED resource availability", severity=Severity.RECOMMENDED) - def check_availability_warning(self, context: ValidationContext) -> bool: + def check_availability_warning(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or context.settings.creation_time or context.settings.enforce_availability or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.id.endswith("/"): @@ -118,15 +118,15 @@ def check_availability_warning(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) - def check_content_size(self, context: ValidationContext) -> bool: # noqa: C901 + def check_content_size(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.is_available(): @@ -159,15 +159,15 @@ def check_content_size(self, context: ValidationContext) -> bool: # noqa: C901 return result @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) - def check_content_url(self, context: ValidationContext) -> bool: # noqa: C901 + def check_content_url(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: content_url = entity.get_property("contentUrl") if not content_url: diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py index 50d54dbb9..c63240ea9 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py @@ -33,7 +33,7 @@ import contextlib from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,14 +100,14 @@ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: return True @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) - def check_missing_file_local_path(self, context: ValidationContext) -> bool: + def check_missing_file_local_path(self, context: ValidationContext) -> CheckResultValue: try: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED if is_detached or context.settings.metadata_only: - return True + return CheckResult.SKIPPED root_entity_id = None with contextlib.suppress(Exception): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id @@ -116,7 +116,7 @@ def check_missing_file_local_path(self, context: ValidationContext) -> bool: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: continue diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py index 17200ccf1..2ee3673f7 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py @@ -24,7 +24,7 @@ """ from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import has_signposting_cite_as @@ -62,7 +62,7 @@ def _needs_sddatepublished_check(entity, root_id) -> bool: name="Referenced RO-Crate: `sdDatePublished` SHOULD be present when Signposting cite-as is not declared", severity=Severity.RECOMMENDED, ) - def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: + def check_sddatepublished_signposting(self, context: ValidationContext) -> CheckResultValue: """ For each referenced RO-Crate data entity whose @id is an absolute URI with no declared `identifier` and no `sdDatePublished`, verify whether @@ -74,19 +74,19 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + return CheckResult.SKIPPED result = True try: root = context.ro_crate.metadata.get_root_data_entity() except Exception: - return True + return CheckResult.SKIPPED try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): continue diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py index 5f9ae888b..b71dd2656 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py @@ -13,7 +13,7 @@ # limitations under the License. from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -80,15 +80,15 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo return True @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) - def check_distribution_downloadable(self, context: ValidationContext) -> bool: + def check_distribution_downloadable(self, context: ValidationContext) -> CheckResultValue: if context.settings.skip_availability_check or context.settings.metadata_only: - return True + return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") - return True + return CheckResult.SKIPPED for entity in entities: distribution_raw = entity.get_property("distribution") if not distribution_raw: From 7f6e8032a8cc2354dfb3a49f35eba81c091c0828 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 18 Sep 2026 11:38:48 +0200 Subject: [PATCH 077/271] feat(profiles): validate Schema.org IRI protocols with SHACL --- .../1.2/must/9_schema_org_iri_protocol.ttl | 45 ++++++++++++++ .../should/9_schema_org_id_compatibility.ttl | 58 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl new file mode 100644 index 000000000..c1c54460c --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl @@ -0,0 +1,45 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro-crate: . +@prefix sh: . + +ro-crate:SchemaOrgTypeProtocol a sh:NodeShape ; + sh:name "Schema.org @type protocol" ; + sh:description "Schema.org @type values MUST use the http://schema.org/ namespace defined by the RO-Crate context." ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + ?this a ?schemaType . + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?schemaType ?term ?httpType + WHERE { + $this a ?schemaType . + FILTER(STRSTARTS(STR(?schemaType), "https://schema.org/")) + BIND(SUBSTR(STR(?schemaType), STRLEN("https://schema.org/") + 1) AS ?term) + BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpType) + } + """ ; + sh:message "Entity {?this} declares the @type '{?schemaType}', which is not a term of the RO-Crate @context: the context maps Schema.org terms to 'http://schema.org/', so the entity MUST be typed '{?term}' (i.e. '{?httpType}')" ; + sh:severity sh:Violation + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl new file mode 100644 index 000000000..6e0460103 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl @@ -0,0 +1,58 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro-crate: . +@prefix sh: . + +ro-crate:SchemaOrgIdCompatibility a sh:NodeShape ; + sh:name "Schema.org @id compatibility" ; + sh:description "Nested Schema.org @id references SHOULD use the http://schema.org/ namespace for compatibility with the RO-Crate context." ; + # OWL-RL adds reflexive owl:sameAs triples for IRIs; these are not nested + # @id references and must not produce compatibility recommendations. + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + ?this ?predicate ?schemaId . + FILTER(?predicate NOT IN ( + , + + )) + FILTER(isIRI(?schemaId)) + FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?predicate ?schemaId ?term ?httpId + WHERE { + $this ?predicate ?schemaId . + FILTER(?predicate NOT IN ( + , + + )) + FILTER(isIRI(?schemaId)) + FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) + BIND(SUBSTR(STR(?schemaId), STRLEN("https://schema.org/") + 1) AS ?term) + BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpId) + } + """ ; + sh:message "Entity {?this} references the Schema.org IRI '{?schemaId}' through a nested @id. For compatibility with the RO-Crate @context, the reference SHOULD use '{?term}' (i.e. '{?httpId}')" ; + sh:severity sh:Warning + ] . From 17f4f9de855c7c854b55465dc9e544202cbb0f39 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 18 Sep 2026 11:38:54 +0200 Subject: [PATCH 078/271] test(profiles): cover Schema.org IRI protocol validation --- .../invalid/ro-crate-metadata.json | 65 ++++++++ .../valid/ro-crate-metadata.json | 63 ++++++++ .../ro-crate-1.2/test_detached_rocrates.py | 8 +- .../test_metadata_contextualEntities.py | 8 +- .../ro-crate-1.2/test_metadata_document.py | 139 ++++++++++++++++++ .../test_metadata_rootDataEntity.py | 16 +- .../ro-crate-1.2/test_referenced_rocrate.py | 6 +- .../ro-crate-1.2/test_workflows_scripts.py | 14 +- tests/ro_crates_v1_2.py | 8 + 9 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..3f3f682f5 --- /dev/null +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json @@ -0,0 +1,65 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol — invalid", + "description": "RO-Crate that types an entity with an https Schema.org IRI and references Schema.org terms through https @id values.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://schema.org/identifier" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "https://schema.org/Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "https://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a1dec1a8d --- /dev/null +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol — valid", + "description": "RO-Crate whose Schema.org IRIs all use the http namespace that the RO-Crate @context defines.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "http://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py b/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py index b340809c4..f12723036 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py +++ b/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py @@ -91,13 +91,13 @@ def test_invalid_root_data_entity_identifier_when_online_available(): False, profile_identifier="ro-crate-1.2", skip_checks=[ - "ro-crate-1.2_41.1", - "ro-crate-1.2_41.2", - "ro-crate-1.2_41.3", "ro-crate-1.2_42.1", + "ro-crate-1.2_42.2", + "ro-crate-1.2_42.3", "ro-crate-1.2_43.1", - "ro-crate-1.2_43.2", "ro-crate-1.2_44.1", + "ro-crate-1.2_44.2", + "ro-crate-1.2_45.1", ], expected_triggered_requirements=["Root Data Entity: RECOMMENDED identifier"], expected_triggered_issues=["In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL"], diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py index 3c16f8361..c9d7f64e4 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py @@ -25,14 +25,14 @@ # Generic RECOMMENDED checks that fire on minimal test crates regardless of the # contextual-entity-specific property being tested. _GENERIC_RECOMMENDED_SKIP = [ - "ro-crate-1.2_48.1", # Root Data Entity: RECOMMENDED funder - "ro-crate-1.2_55.1", # Root Data Entity: RECOMMENDED publisher + "ro-crate-1.2_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.2_56.1", # Root Data Entity: RECOMMENDED publisher ] # Correct IDs for funder/publisher checks (used in person entity tests). _PERSON_VALID_SKIP = [ - "ro-crate-1.2_48.1", # Root Data Entity: RECOMMENDED funder - "ro-crate-1.2_55.1", # Root Data Entity: RECOMMENDED publisher + "ro-crate-1.2_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.2_56.1", # Root Data Entity: RECOMMENDED publisher ] diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 189397d5d..193b5321c 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -357,3 +357,142 @@ def test_invalid_named_entity_id_format(): expected_triggered_requirements=["Entity identifier: format recommendations"], expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], ) + + +# --------------------------------------------------------------------------- +# Schema.org IRIs: http namespace, not https (MUST) — see #188 +# --------------------------------------------------------------------------- + + +def test_valid_schema_org_iri_protocol(): + """ + Crate whose Schema.org IRIs all use the http namespace passes. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + ) + + +def test_invalid_schema_org_type_protocol(): + """ + An entity typed with an https Schema.org IRI is reported, naming the http term. + """ + do_entity_test( + __metadata_document_crates__.invalid_schema_org_iri_protocol, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Schema.org @type protocol"], + expected_triggered_issues=[ + "declares the @type 'https://schema.org/Organization'", + "MUST be typed 'Organization' (i.e. 'http://schema.org/Organization')", + ], + ) + + +def test_schema_org_type_presence_check_accepts_https_namespace(): + """ + The existing Schema.org type recommendation remains a presence check and + must not duplicate the dedicated protocol check. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + ) + ) + assert any(detail.check.requirement.name == "Schema.org @type protocol" for detail in result.get_issues()) + assert not any( + issue.message == "RO-Crate Metadata Entity SHOULD include at least one Schema.org type" + for issue in result.get_issues() + ) + + +def test_https_schema_org_id_is_allowed(): + """ + An https Schema.org IRI used as an @id remains a valid resource identifier. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + ) + + +def test_https_entity_id_is_not_recommended(): + """ + An entity's own HTTPS Schema.org @id is not a compatibility issue. + """ + with (__metadata_document_crates__.valid_schema_org_iri_protocol / "ro-crate-metadata.json").open( + encoding="utf-8" + ) as stream: + metadata_dict = json.load(stream) + metadata_dict["@graph"].append( + { + "@id": "https://schema.org/StandaloneResource", + "@type": "Thing", + "name": "Standalone Schema.org resource", + } + ) + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.2", + metadata_dict=metadata_dict, + metadata_only=True, + ) + + +def test_https_nested_schema_org_id_is_recommended_for_compatibility(): + """ + A nested HTTPS Schema.org @id remains valid but emits a compatibility recommendation. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.2", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + expected_triggered_requirements=["Schema.org @id compatibility"], + expected_triggered_issues=[ + "references the Schema.org IRI 'https://schema.org/identifier' through a nested @id", + "SHOULD use 'identifier' (i.e. 'http://schema.org/identifier')", + ], + ) + + +def test_schema_org_protocol_shapes_run_after_context_check(): + """ + The context check runs before the SHACL protocol shapes. A context failure + is reported, while the protocol shapes do not emit unrelated violations. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_context_reference), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + ) + ) + + protocol_issues = { + issue.check.requirement.name + for issue in result.get_issues() + if issue.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + } + assert protocol_issues == set() + assert any( + issue.message + and 'does not reference the required context "https://w3id.org/ro/crate/1.2/context"' in issue.message + for issue in result.get_issues() + ) + assert not any( + detail.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + for detail in result.skipped_check_details + ) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py index 71bbe5b1a..36dfbd3fa 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py @@ -126,7 +126,7 @@ def test_valid_recommended_citeAs_for_resolvable_id(): profile_identifier="ro-crate-1.2", # The test crate focuses on cite-as, not identifier format; # skip the identifier-presence and PropertyValue-approach checks. - skip_checks=["ro-crate-1.2_51.1", "ro-crate-1.2_52.1"], + skip_checks=["ro-crate-1.2_52.1", "ro-crate-1.2_53.1"], ) @@ -215,7 +215,7 @@ def test_valid_recommended_identifier_resolution(monkeypatch): profile_identifier="ro-crate-1.2", # The identifier uses a plain URL string rather than a PropertyValue entity; # that format check is not the focus of this test (resolution is). - skip_checks=["ro-crate-1.2_52.0"], + skip_checks=["ro-crate-1.2_53.0"], ) @@ -381,7 +381,7 @@ def test_valid_required_hasPart_all_data_entities(): models.Severity.REQUIRED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_46.1"], + skip_checks=["ro-crate-1.2_47.1"], ) @@ -397,7 +397,7 @@ def test_invalid_required_hasPart_all_data_entities(): profile_identifier="ro-crate-1.2", expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], expected_triggered_issues=["MUST reference all Data Entities via hasPart"], - skip_checks=["ro-crate-1.2_46.1"], + skip_checks=["ro-crate-1.2_47.1"], ) @@ -464,13 +464,13 @@ def test_valid_recommended_identifier_if_pid(): skip_checks=[ "ro-crate-1.2_35.1", "ro-crate-1.2_38.1", - "ro-crate-1.2_41.1", - "ro-crate-1.2_41.2", - "ro-crate-1.2_41.3", "ro-crate-1.2_42.1", + "ro-crate-1.2_42.2", + "ro-crate-1.2_42.3", "ro-crate-1.2_43.1", - "ro-crate-1.2_43.2", "ro-crate-1.2_44.1", + "ro-crate-1.2_44.2", + "ro-crate-1.2_45.1", "Root Data Entity: use cite-as for resolvable identifiers", "Root Data Entity: persistent identifier resolution", "Root Data Entity: identifier SHOULD be present if PID exists", diff --git a/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py b/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py index 8a86cb0f5..58f3f02f7 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py +++ b/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py @@ -34,7 +34,7 @@ def test_valid_referenced_rocrate(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_71.1", "ro-crate-1.2_74.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_46.1", "ro-crate-1.2_72.1", "ro-crate-1.2_75.1"], ) @@ -155,7 +155,7 @@ def test_valid_referenced_rocrate_with_identifier(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_71.1", "ro-crate-1.2_74.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_46.1", "ro-crate-1.2_72.1", "ro-crate-1.2_75.1"], ) @@ -170,7 +170,7 @@ def test_valid_referenced_rocrate_with_relative_path(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_71.1", "ro-crate-1.2_74.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_46.1", "ro-crate-1.2_72.1", "ro-crate-1.2_75.1"], ) diff --git a/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py b/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py index a16394f0e..bb6e0db0d 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py +++ b/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py @@ -29,13 +29,13 @@ _GENERIC_RECOMMENDED_SKIP = [ "ro-crate-1.2_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) "ro-crate-1.2_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) - "ro-crate-1.2_48.1", # Root Data Entity: recommended funder - "ro-crate-1.2_55.1", # Root Data Entity: recommended publisher - "ro-crate-1.2_62.1", # File Data Entity: RECOMMENDED contentSize - "ro-crate-1.2_63.0", # File: RECOMMENDED conformsTo profile - "ro-crate-1.2_75.1", # Contextual Entity Properties - "ro-crate-1.2_76.1", # Contextual Entity RECOMMENDED description - "ro-crate-1.2_81.2", # License entity: RECOMMENDED properties + "ro-crate-1.2_49.1", # Root Data Entity: recommended funder + "ro-crate-1.2_56.1", # Root Data Entity: recommended publisher + "ro-crate-1.2_63.1", # File Data Entity: RECOMMENDED contentSize + "ro-crate-1.2_64.0", # File: RECOMMENDED conformsTo profile + "ro-crate-1.2_76.1", # Contextual Entity Properties + "ro-crate-1.2_77.1", # Contextual Entity RECOMMENDED description + "ro-crate-1.2_82.2", # License entity: RECOMMENDED properties ] diff --git a/tests/ro_crates_v1_2.py b/tests/ro_crates_v1_2.py index 295f22297..18e5491d0 100644 --- a/tests/ro_crates_v1_2.py +++ b/tests/ro_crates_v1_2.py @@ -82,6 +82,14 @@ def valid_named_entity_id_format(self) -> Path: def invalid_named_entity_id_format(self) -> Path: return self.METADATA_DOCUMENT_CRATES_PATH / "named_entity_id_format" / "invalid" + @property + def valid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "valid" + + @property + def invalid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "invalid" + class MetadataDocumentFormat: METADATA_DOCUMENT_FORMAT_CRATES_PATH = MetadataDocument.METADATA_DOCUMENT_CRATES_PATH / "format" From a99d83d03e96ae8c64f507a37a26eab8f9404922 Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 18 Sep 2026 14:33:57 +0200 Subject: [PATCH 079/271] port updates from #209 to ro-crate 1.3 --- .../1.3/must/0_file_descriptor_format.py | 75 +++++++++++++++---- .../ro-crate/1.3/must/1_ro_crate_preview.py | 6 +- .../1.3/must/2_root_data_entity_cite_as.py | 11 ++- .../1.3/must/2_root_data_entity_identifier.py | 4 +- .../1.3/must/2_root_identifier_property.py | 3 +- .../1.3/must/4_data_entity_metadata.py | 22 +++++- .../1.3/should/0_entity_identifier_format.py | 9 ++- .../1.3/should/1_file_descriptor_name.py | 1 + .../should/2_root_data_entity_identifier.py | 15 +++- .../1.3/should/4_data_entity_license.py | 10 ++- .../1.3/should/4_data_entity_metadata.py | 13 +++- .../1.3/should/4_missing_file_local_path.py | 5 +- .../1.3/should/4_referenced_rocrate.py | 9 ++- .../1.3/should/4_web_dataset_distribution.py | 4 +- 14 files changed, 149 insertions(+), 38 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py index 2ce458cd7..bba7b3888 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py @@ -22,11 +22,14 @@ from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError # set up logging logger = logging.getLogger(__name__) +_HANDLED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_HANDLED_REMOTE_CONTEXT_ERRORS = (*_HANDLED_METADATA_ERRORS, OSError, RuntimeError) + # Property keys whose string values are literals (e.g. names, dates, URLs) rather # than references to other entities, so a matching @id must not be flagged. _LITERAL_KEYS = frozenset( @@ -65,6 +68,7 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' @@ -79,6 +83,7 @@ def test_size(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) @@ -99,6 +104,7 @@ def check(self, context: ValidationContext) -> CheckResultValue: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor encoding check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED try: raw_data = context.ro_crate.metadata.as_bytes() @@ -108,11 +114,13 @@ def check(self, context: ValidationContext) -> CheckResultValue: return True except UnicodeDecodeError: context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) + context.abort_validation("file descriptor is not UTF-8 encoded") if logger.isEnabledFor(logging.DEBUG): logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED @@ -142,8 +150,12 @@ def check(self, context: ValidationContext) -> CheckResultValue: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _HANDLED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -226,7 +238,9 @@ def __check_remote_context__(self, context_uri: str) -> bool: jsonLD = self.__get_remote_context__(context_uri) assert isinstance(jsonLD, dict) return True - except Exception: + except OfflineCacheMissError: + raise + except _HANDLED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -290,8 +304,12 @@ def has_expected_context(ctx: object) -> bool: return self.__check_contexts__(context, json_dict["@context"]) except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -395,8 +413,12 @@ def check_flattened(self, context: ValidationContext) -> CheckResultValue: return result except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -417,12 +439,17 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: "file descriptor does not contain the @id attribute", self, ) + context.abort_validation("file descriptor entity does not contain the @id attribute") return False return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -447,8 +474,12 @@ def check_types(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -470,8 +501,12 @@ def check_unique_identifiers(self, context: ValidationContext) -> CheckResultVal return True except ROCrateMetadataNotFoundError: logger.debug("Skipping unique identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -522,8 +557,12 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu return True except ROCrateMetadataNotFoundError: logger.debug("Skipping entity reference check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception: + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -547,8 +586,12 @@ def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue return True except ROCrateMetadataNotFoundError: logger.debug("Skipping subject and keyword check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception: + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -624,7 +667,7 @@ def add_unexpected_key(k: str, u_keys: dict) -> None: name="Validation of the compaction format of the file descriptor", depends_on=("File Descriptor JSON format",), ) - def check_compaction(self, context: ValidationContext) -> CheckResultValue: + def check_compaction(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -637,7 +680,9 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: try: context_keys = self.__get_context_keys__(jsonld_context) logger.debug(f"{context_keys}") - except Exception as e: + except OfflineCacheMissError: + raise + except _HANDLED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(str(e), self) @@ -670,8 +715,12 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: return True except ROCrateMetadataNotFoundError: logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except Exception as e: + except _HANDLED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py index baa7274d6..5bf475afd 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py @@ -33,19 +33,23 @@ class ROCrateWebsiteChecker(PyFunctionCheck): def check_preview_html(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED except ROCrateMetadataNotFoundError: logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): return True try: content = context.ro_crate.get_file_content(preview_path, binary_mode=False) + if isinstance(content, bytes): + content = content.decode("utf-8", errors="strict") if " str | None: # cite-as can be a plain string literal or an entity reference {"@id": "..."} if isinstance(cite_as_raw, str): cite_as_url = cite_as_raw - elif hasattr(cite_as_raw, "id"): - cite_as_url = cite_as_raw.id else: - return None + entity_id = getattr(cite_as_raw, "id", None) + if not isinstance(entity_id, str): + return None + cite_as_url = entity_id if not cite_as_url or not cite_as_url.startswith("http"): return None return cite_as_url @@ -57,6 +58,7 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultV or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not applicable", "configured") return CheckResult.SKIPPED try: @@ -84,7 +86,8 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultV except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py index 354b9ee11..50f32b2e2 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py @@ -32,6 +32,7 @@ class RootDataEntityIdentifierChecker(PyFunctionCheck): def check_identifier(self, context: ValidationContext) -> CheckResultValue: try: if context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if root_entity.id == "./": @@ -45,7 +46,8 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: return False except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py index d89f405eb..501ac7eb2 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py @@ -46,7 +46,8 @@ def check_identifier_values(self, context: ValidationContext) -> CheckResultValu return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py index 5b2daa42b..a054e8e69 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py @@ -40,13 +40,16 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if is_detached: logger.debug("Skipping data entity payload checks for detached RO-Crate") + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED # Perform the check result = True @@ -54,6 +57,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -84,7 +88,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self ) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue( f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self ) @@ -109,17 +113,20 @@ def check_detached_entities(self, context: ValidationContext) -> CheckResultValu is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if not is_detached: + context.record_skip(self, "RO-Crate is attached", "returned") return CheckResult.SKIPPED result = True root_entity_id = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: @@ -149,7 +156,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # root_entity_id = None root_entity_is_local = False root_entity_absolute_path = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_data_entity = context.ro_crate.metadata.get_root_data_entity() root_entity_id = root_data_entity.id root_entity_is_local = ( @@ -160,6 +167,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: @@ -202,14 +210,17 @@ def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if is_detached: + context.record_skip(self, "RO-Crate is detached", "returned") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if entity.has_local_identifier() or entity.is_remote(): @@ -239,6 +250,7 @@ def check_citation(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") @@ -295,12 +307,14 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not requested", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -313,7 +327,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) result = False if not result and context.fail_fast: diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py index 3aa9875ec..2b3b377f8 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py @@ -57,6 +57,7 @@ def check_no_parent_traversal(self, context: ValidationContext) -> CheckResultVa entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping parent traversal check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") @@ -85,6 +86,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> CheckResultValue entities = context.ro_crate.metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") @@ -114,17 +116,18 @@ def check_named_entity_id_format(self, context: ValidationContext) -> CheckResul result = True ro_crate_metadata = context.ro_crate.metadata non_contextual_ids = set() - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.add(ro_crate_metadata.get_file_descriptor_entity().id) - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.add(ro_crate_metadata.get_root_data_entity().id) - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) try: entities = ro_crate_metadata.as_dict().get("@graph", []) except ROCrateMetadataNotFoundError: logger.debug("Skipping named entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: entity_id = entity.get("@id", "") diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py index d4ebc2b8c..4261a1ed2 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py @@ -37,6 +37,7 @@ def test_detached_descriptor_filename(self, context: ValidationContext) -> Check """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py index f1249b2d7..53984453d 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py @@ -35,9 +35,11 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: # n """ try: if not context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is attached", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): + context.record_skip(self, "root data entity is not remote", "returned") return CheckResult.SKIPPED if root_entity.id == "./": @@ -54,8 +56,9 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: # n return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False @@ -75,9 +78,11 @@ def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValu """ try: if not context.ro_crate.is_detached(): + context.record_skip(self, "RO-Crate is attached", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): + context.record_skip(self, "root data entity is not remote", "returned") return CheckResult.SKIPPED # Check if the `cite-as` property is present and references the Root Data Entity @@ -102,8 +107,9 @@ def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValu return True except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) return False @@ -149,8 +155,10 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) def check_identifier_resolvable(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 if context.settings.skip_availability_check: + context.record_skip(self, "availability checks are disabled", "configured") return CheckResult.SKIPPED if context.settings.metadata_only: + context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -178,7 +186,8 @@ def check_identifier_resolvable(self, context: ValidationContext) -> CheckResult return result except ROCrateMetadataNotFoundError: logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py index 67edde9da..234166a57 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py @@ -101,11 +101,16 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 + def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901, PLR0911 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except ValueError: + context.record_skip(self, "root data entity is not available", "exception") return CheckResult.SKIPPED if root_entity is None: return True @@ -122,6 +127,7 @@ def check_license_divergence(self, context: ValidationContext) -> CheckResultVal entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if entity.id == root_entity.id: diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py index 3b0825ab6..ca5b5e1c7 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py @@ -37,6 +37,7 @@ def check_citation(self, context: ValidationContext) -> CheckResultValue: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: citations = entity.get_property("citation") @@ -94,12 +95,14 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV or context.settings.enforce_availability or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not applicable", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -110,7 +113,7 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) result = False if not result and context.fail_fast: @@ -120,12 +123,14 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) def check_content_size(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: + context.record_skip(self, "availability checks are disabled", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: assert entity.id is not None, "Entity has no @id" @@ -140,7 +145,7 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: # content_value = str(content_size) try: content_int = int(str(content_value)) - except Exception: + except (TypeError, ValueError, OverflowError): content_int = None external_size = context.ro_crate.get_external_file_size(entity.id) if external_size is not None and content_int is not None and content_int != external_size: @@ -161,12 +166,14 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: # @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) def check_content_url(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: + context.record_skip(self, "availability checks are disabled", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_web_data_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: content_url = entity.get_property("contentUrl") @@ -186,7 +193,7 @@ def check_content_url(self, context: ValidationContext) -> CheckResultValue: # msg += f": {dl.reason}" context.result.add_issue(msg, self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue( f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}' " f"availability check failed: {e}", diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py index c63240ea9..f6c67cd70 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py @@ -105,17 +105,20 @@ def check_missing_file_local_path(self, context: ValidationContext) -> CheckResu is_detached = context.ro_crate.is_detached() except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED if is_detached or context.settings.metadata_only: + context.record_skip(self, "RO-Crate is detached or metadata-only mode is active", "configured") return CheckResult.SKIPPED root_entity_id = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id result = True try: entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) except ROCrateMetadataNotFoundError: logger.debug("Skipping missing local file check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if root_entity_id and entity.id == root_entity_id: diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py index 2ee3673f7..32db7f50b 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py @@ -74,18 +74,25 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> Check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): + context.record_skip(self, "availability check is disabled or not applicable", "configured") return CheckResult.SKIPPED result = True try: root = context.ro_crate.metadata.get_root_data_entity() - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except ValueError: + context.record_skip(self, "root data entity is not available", "exception") return CheckResult.SKIPPED try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py index b71dd2656..23955a844 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py @@ -72,7 +72,7 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo msg += f": {dl.reason}" context.result.add_issue(msg, self) return False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue( f"Error checking downloadability of distribution '{url}' for Dataset '{entity_id}': {e}", self ) @@ -82,12 +82,14 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) def check_distribution_downloadable(self, context: ValidationContext) -> CheckResultValue: if context.settings.skip_availability_check or context.settings.metadata_only: + context.record_skip(self, "availability checks are disabled or metadata-only mode is active", "configured") return CheckResult.SKIPPED result = True try: entities = context.ro_crate.metadata.get_dataset_entities() except ROCrateMetadataNotFoundError: logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: distribution_raw = entity.get_property("distribution") From bc2f76e46d1e047a5c36a09e417d59146143b17a Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 21 Sep 2026 11:30:30 +0200 Subject: [PATCH 080/271] port updates from #210 and #211 to ro-crate 1.3 --- .../ro-crate/1.3/must/1_ro_crate_preview.py | 2 +- .../1.3/must/9_schema_org_iri_protocol.ttl | 45 ++++++++++++++ .../should/9_schema_org_id_compatibility.ttl | 58 +++++++++++++++++++ .../test_workflows_scripts_v1_3.py | 14 ++--- 4 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/9_schema_org_iri_protocol.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py index 5bf475afd..f03f09ffc 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py @@ -48,7 +48,7 @@ def check_preview_html(self, context: ValidationContext) -> CheckResultValue: content = content.decode("utf-8", errors="strict") if " . +@prefix sh: . + +ro-crate:SchemaOrgTypeProtocol a sh:NodeShape ; + sh:name "Schema.org @type protocol" ; + sh:description "Schema.org @type values MUST use the http://schema.org/ namespace defined by the RO-Crate context." ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + ?this a ?schemaType . + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?schemaType ?term ?httpType + WHERE { + $this a ?schemaType . + FILTER(STRSTARTS(STR(?schemaType), "https://schema.org/")) + BIND(SUBSTR(STR(?schemaType), STRLEN("https://schema.org/") + 1) AS ?term) + BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpType) + } + """ ; + sh:message "Entity {?this} declares the @type '{?schemaType}', which is not a term of the RO-Crate @context: the context maps Schema.org terms to 'http://schema.org/', so the entity MUST be typed '{?term}' (i.e. '{?httpType}')" ; + sh:severity sh:Violation + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl new file mode 100644 index 000000000..fce00879e --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl @@ -0,0 +1,58 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro-crate: . +@prefix sh: . + +ro-crate:SchemaOrgIdCompatibility a sh:NodeShape ; + sh:name "Schema.org @id compatibility" ; + sh:description "Nested Schema.org @id references SHOULD use the http://schema.org/ namespace for compatibility with the RO-Crate context." ; + # OWL-RL adds reflexive owl:sameAs triples for IRIs; these are not nested + # @id references and must not produce compatibility recommendations. + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + ?this ?predicate ?schemaId . + FILTER(?predicate NOT IN ( + , + + )) + FILTER(isIRI(?schemaId)) + FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?predicate ?schemaId ?term ?httpId + WHERE { + $this ?predicate ?schemaId . + FILTER(?predicate NOT IN ( + , + + )) + FILTER(isIRI(?schemaId)) + FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) + BIND(SUBSTR(STR(?schemaId), STRLEN("https://schema.org/") + 1) AS ?term) + BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpId) + } + """ ; + sh:message "Entity {?this} references the Schema.org IRI '{?schemaId}' through a nested @id. For compatibility with the RO-Crate @context, the reference SHOULD use '{?term}' (i.e. '{?httpId}')" ; + sh:severity sh:Warning + ] . diff --git a/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py index b286cbf46..5acb753a0 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py @@ -29,13 +29,13 @@ _GENERIC_RECOMMENDED_SKIP = [ "ro-crate-1.3_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) "ro-crate-1.3_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) - "ro-crate-1.3_48.1", # Root Data Entity: recommended funder - "ro-crate-1.3_55.1", # Root Data Entity: recommended publisher - "ro-crate-1.3_62.1", # File Data Entity: RECOMMENDED contentSize - "ro-crate-1.3_63.0", # File: RECOMMENDED conformsTo profile - "ro-crate-1.3_75.1", # Contextual Entity Properties - "ro-crate-1.3_76.1", # Contextual Entity RECOMMENDED description - "ro-crate-1.3_81.2", # License entity: RECOMMENDED properties + "ro-crate-1.3_49.1", # Root Data Entity: recommended funder + "ro-crate-1.3_56.1", # Root Data Entity: recommended publisher + "ro-crate-1.3_63.1", # File Data Entity: RECOMMENDED contentSize + "ro-crate-1.3_64.0", # File: RECOMMENDED conformsTo profile + "ro-crate-1.3_76.1", # Contextual Entity Properties + "ro-crate-1.3_77.1", # Contextual Entity RECOMMENDED description + "ro-crate-1.3_82.2", # License entity: RECOMMENDED properties ] From b6199c608df4b1d20e987a4946d06005b04fe795 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 16:00:14 +0200 Subject: [PATCH 081/271] complete changes to make 1.3 the default ro-crate profile # Conflicts: # tests/test_cli.py --- CHANGELOG.md | 4 +- README.md | 9 +- docs/5_offline_mode.rst | 6 +- rocrate_validator/cli/commands/profiles.py | 2 +- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/constants.py | 2 +- rocrate_validator/models/profile.py | 2 +- .../dataset-ro-crate-metadata.json | 0 .../test-ro-crate-metadata.json | 0 .../dataset-ro-crate-metadata.json | 43 ++++++ .../detached-1.3/test-ro-crate-metadata.json | 37 ++++++ .../prefix-ro-crate-metadata.json | 125 ++++++++++++++++++ .../valid/ro-crate-1.3-attached/data.txt | 1 + .../ro-crate-metadata.json | 38 ++++++ .../ro-crate-1.2/test_ro_crate_1_2.py | 2 +- tests/integration/test_offline_mode.py | 1 + tests/ro_crates_v1_2.py | 4 +- tests/ro_crates_v1_3.py | 20 +++ tests/shared.py | 7 +- tests/test_cli.py | 14 +- .../requirements/test_load_requirements.py | 2 +- tests/unit/test_default_profile.py | 24 ++-- 22 files changed, 306 insertions(+), 39 deletions(-) rename tests/data/crates/valid/{detached => detached-1.2}/dataset-ro-crate-metadata.json (100%) rename tests/data/crates/valid/{detached => detached-1.2}/test-ro-crate-metadata.json (100%) create mode 100644 tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json create mode 100644 tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json create mode 100644 tests/data/crates/valid/ro-crate-1.3-absolute-root/prefix-ro-crate-metadata.json create mode 100644 tests/data/crates/valid/ro-crate-1.3-attached/data.txt create mode 100644 tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5adc09209..90a3226d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### ⚠️ Breaking Changes -- **The default validation profile is now `ro-crate-1.2`** (previously `ro-crate-1.1`). Since 1.2 +- **The default validation profile is now `ro-crate-1.3`** (previously `ro-crate-1.1`). Since 1.3 is self-contained, validation now applies 95 requirements instead of 30; crates declaring the - 1.1 context fail the `ro-crate-1.2_2.2` check under the default profile. + 1.1 context fail the `ro-crate-1.3_2.2` check under the default profile. - This primarily affects API users when `profile_identifier` is omitted. CLI auto-detection is unchanged unless no profile is detected or `--no-auto-profile` is used. - To keep the previous behaviour, explicitly select `ro-crate-1.1` with `profile_identifier` or diff --git a/README.md b/README.md index 8bd2e0890..a0d0502fe 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ against different profiles, including the base RO-Crate profile and various exte - Validates RO-Crates against the profiles they declare to conform to. Currently, validation for the following profiles is implemented: - - [RO-Crate 1.2](https://w3id.org/ro/crate/1.2) *(base profile, used by default)* + - [RO-Crate 1.3](https://w3id.org/ro/crate/1.3) *(base profile, used by default)* + - [RO-Crate 1.2](https://w3id.org/ro/crate/1.2) *(base profile)* - [RO-Crate 1.1](https://w3id.org/ro/crate/1.1) *(base profile)* - [Workflow RO-Crate](https://w3id.org/workflowhub/workflow-ro-crate/1.0) - [Workflow Testing RO-Crate](https://w3id.org/ro/wftest) @@ -135,10 +136,10 @@ settings = services.ValidationSettings( # Set the path to the RO-Crate root directory rocrate_uri='/path/to/ro-crate', # Set the identifier of the RO-Crate profile to use for validation. - # If not set, the base RO-Crate profile is used, which defaults to `ro-crate-1.2`. + # If not set, the base RO-Crate profile is used, which defaults to `ro-crate-1.3`. # Use `services.detect_profiles(settings)` to determine the profiles a crate declares # conformance to, and pass one of them here to validate against it instead. - profile_identifier='ro-crate-1.2', + profile_identifier='ro-crate-1.3', # Set the requirement level for the validation requirement_severity=models.Severity.REQUIRED, ) @@ -161,7 +162,7 @@ The following is a possible output: ```bash RO-Crate is invalid! -Detected issue of severity REQUIRED with check "ro-crate-1.2:root_entity_exists: The RO-Crate must contain a root entity. +Detected issue of severity REQUIRED with check "ro-crate-1.3:root_entity_exists: The RO-Crate must contain a root entity. ``` ## Running the tests diff --git a/docs/5_offline_mode.rst b/docs/5_offline_mode.rst index 7bc08ec50..afbbd591a 100644 --- a/docs/5_offline_mode.rst +++ b/docs/5_offline_mode.rst @@ -100,11 +100,11 @@ Before going offline you can pre-fetch everything you will need with rocrate-validator cache warm --all-profiles # Warm only specific profiles - rocrate-validator cache warm -p ro-crate-1.2 -p workflow-ro-crate-1.0 + rocrate-validator cache warm -p ro-crate-1.3 -p workflow-ro-crate-1.0 # Also fetch and cache remote RO-Crates or arbitrary URLs rocrate-validator cache warm --crate https://example.org/crate.zip - rocrate-validator cache warm -u https://w3id.org/ro/crate/1.2/context + rocrate-validator cache warm -u https://w3id.org/ro/crate/1.3/context When invoked without any source option, ``cache warm`` defaults to warming all installed profiles. A summary table reports which URLs were cached, skipped or @@ -125,7 +125,7 @@ The same offline behaviour can be enabled programmatically through settings = services.ValidationSettings( rocrate_uri='/path/to/ro-crate', - profile_identifier='ro-crate-1.2', + profile_identifier='ro-crate-1.3', # Serve every HTTP request from the cache; uncached resources fail. offline=True, # Optional: use a dedicated cache directory (defaults to the user cache). diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 29c577ec6..21927335d 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -249,7 +249,7 @@ def describe_profile( \b The check identifier accepts either form: * relative: . (e.g. "1.2") - * full: _. (e.g. "ro-crate-1.2_1.2") + * full: _. (e.g. "ro-crate-1.3_1.2") With -v on a single check, the source code of the check is shown. """ diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index fc3178381..084bb841f 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -174,7 +174,7 @@ def validate_uri(ctx, param, value): help=( "[bold yellow]Fully-Qualified-Check-IDs[/bold yellow] is a comma-separated list of checks to skip " "(may be specified multiple times). Each check must be specified by its " - "Fully Qualified Identifier, e.g., [bold cyan]ro-crate-1.2_12.1[/bold cyan]. The fully qualified " + "Fully Qualified Identifier, e.g., [bold cyan]ro-crate-1.3_11.1[/bold cyan]. The fully qualified " "check identifier has the format _., " "where is the position number of the Requirement in the profile, " "and is the position number of the RequirementCheck within that Requirement. " diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index e487370c1..cadc87461 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -42,7 +42,7 @@ ROCRATE_METADATA_FILE = "ro-crate-metadata.json" # Define the default profiles name -DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.2" +DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.3" # Define the default profiles path DEFAULT_PROFILES_PATH = "profiles" diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 10a234f26..3c678caeb 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -942,7 +942,7 @@ def resolve_in_list(cls, profiles: Collection[Profile], profile_identifier: str) """ Resolve the given identifier against the given list of profiles. - An exact match on the profile identifier (e.g. `ro-crate-1.2`) always wins. + An exact match on the profile identifier (e.g. `ro-crate-1.3`) always wins. Failing that, the identifier is treated as a bare token (e.g. `ro-crate`) and resolves to the **highest available version** of that token. diff --git a/tests/data/crates/valid/detached/dataset-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.2/dataset-ro-crate-metadata.json similarity index 100% rename from tests/data/crates/valid/detached/dataset-ro-crate-metadata.json rename to tests/data/crates/valid/detached-1.2/dataset-ro-crate-metadata.json diff --git a/tests/data/crates/valid/detached/test-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.2/test-ro-crate-metadata.json similarity index 100% rename from tests/data/crates/valid/detached/test-ro-crate-metadata.json rename to tests/data/crates/valid/detached-1.2/test-ro-crate-metadata.json diff --git a/tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json new file mode 100644 index 000000000..05f8fa26a --- /dev/null +++ b/tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json @@ -0,0 +1,43 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached" + } + }, + { + "@id": "https://example.org/ro-crate/detached", + "@type": "Dataset", + "name": "Detached RO-Crate 1.3", + "description": "Detached RO-Crate metadata-only file.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "https://example.org/data/file1.txt" + } + ] + }, + { + "@id": "https://example.org/data/file1.txt", + "@type": "File", + "name": "file1.txt", + "description": "Remote file in detached RO-Crate.", + "encodingFormat": "text/plain" + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "name": "Creative Commons Zero 1.0 Universal", + "description": "A Creative Commons license that allows others to copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. This is the most permissive of the Creative Commons licenses, effectively placing the work in the public domain." + } + ] +} diff --git a/tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json new file mode 100644 index 000000000..dcfa146e9 --- /dev/null +++ b/tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "test-ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/prefixed" + } + }, + { + "@id": "https://example.org/ro-crate/detached/prefixed", + "@type": "Dataset", + "name": "Detached RO-Crate (prefixed metadata)", + "description": "Detached RO-Crate with prefixed metadata filename.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "https://example.org/data/file1.txt" + } + ] + }, + { + "@id": "https://example.org/data/file1.txt", + "@type": "File", + "name": "file1.txt", + "description": "Remote file in detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/valid/ro-crate-1.3-absolute-root/prefix-ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.3-absolute-root/prefix-ro-crate-metadata.json new file mode 100644 index 000000000..14e769c19 --- /dev/null +++ b/tests/data/crates/valid/ro-crate-1.3-absolute-root/prefix-ro-crate-metadata.json @@ -0,0 +1,125 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://github.com/crs4/pydoop/tree/develop/" + } + }, + { + "@id": "https://github.com/crs4/pydoop/tree/develop/", + "@type": "Dataset", + "name": "Example RO-Crate 1.3 (absolute root)", + "description": "Attached RO-Crate using an absolute root @id.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "publisher": { + "@id": "https://orcid.org/0000-0001-2345-6789" + }, + "funder": { + "@id": "https://ror.org/123456789" + }, + "datePublished": "2024-01-01", + "hasPart": { + "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md" + }, + "cite-as": { + "@id": "https://github.com/crs4/pydoop/tree/develop/" + }, + "url": "https://github.com/crs4/pydoop/tree/develop/", + "identifier": { + "@id": "#pydoop-pid" + } + }, + { + "@id": "#pydoop-pid", + "@type": "PropertyValue", + "name": "Pydoop repository URL", + "propertyID": "URL", + "value": "https://github.com/crs4/pydoop/tree/develop/" + }, + { + "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md", + "@type": "File", + "name": "README.md", + "description": "Example data file.", + "encodingFormat": "text/plain", + "contentSize": "221", + "contentLocation": { + "@id": "#bologna" + }, + "conformsTo": { + "@id": "#csv-profile" + }, + "dateCreated": "2025-06-15", + "dateModified": "2025-12-01", + "author": { + "@id": "https://orcid.org/0000-0001-2345-6789" + }, + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "sdDatePublished": "2026-04-15" + }, + { + "@id": "#csv-profile", + "@type": "CreativeWork", + "name": "Example CSV Profile", + "description": "An example profile describing CSV files." + }, + { + "@id": "#bologna", + "@type": "Place", + "name": "Bologna, Italy", + "description": "The city of Bologna in Italy." + }, + { + "@id": "https://orcid.org/0000-0001-2345-6789", + "@type": "Person", + "name": "Jane Doe", + "description": "An example author with an ORCID identifier.", + "affiliation": { + "@id": "https://ror.org/123456789" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/123456789", + "@type": "Organization", + "name": "Example University", + "description": "An example university affiliated with the author.", + "url": "https://exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact", + "contactType": "customer support", + "email": "info@exampleuniversity.edu", + "url": "https://exampleuniversity.edu/contact" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "name": "Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", + "description": "A Creative Commons license that allows creators to waive all their copyright and related rights in their works, effectively placing them in the public domain." + } + ] +} diff --git a/tests/data/crates/valid/ro-crate-1.3-attached/data.txt b/tests/data/crates/valid/ro-crate-1.3-attached/data.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/tests/data/crates/valid/ro-crate-1.3-attached/data.txt @@ -0,0 +1 @@ +hello diff --git a/tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json new file mode 100644 index 000000000..96cd2732c --- /dev/null +++ b/tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example RO-Crate 1.3", + "description": "Minimal attached RO-Crate for profile 1.3 tests.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Example data file.", + "encodingFormat": "text/plain", + "contentSize": "5" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py b/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py index 9675b822f..9f2440f94 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py +++ b/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py @@ -15,7 +15,7 @@ import logging from rocrate_validator import models -from tests.ro_crates import InvalidROCrate12, ValidROCrate12 +from tests.ro_crates_v1_2 import InvalidROCrate12, ValidROCrate12 from tests.shared import do_entity_test logger = logging.getLogger(__name__) diff --git a/tests/integration/test_offline_mode.py b/tests/integration/test_offline_mode.py index 127370400..7c7f6810d 100644 --- a/tests/integration/test_offline_mode.py +++ b/tests/integration/test_offline_mode.py @@ -306,6 +306,7 @@ def test_cli_cache_reset_yes_clears_entries(cli_runner, tmp_path, network_interc [ ("ro-crate-1.1", "https://w3id.org/ro/crate/1.1/context"), ("ro-crate-1.2", "https://w3id.org/ro/crate/1.2/context"), + ("ro-crate-1.3", "https://w3id.org/ro/crate/1.3/context"), ], ) def test_cli_cache_warm_populates_profile_urls( diff --git a/tests/ro_crates_v1_2.py b/tests/ro_crates_v1_2.py index 083021a87..9273f368f 100644 --- a/tests/ro_crates_v1_2.py +++ b/tests/ro_crates_v1_2.py @@ -783,11 +783,11 @@ def attached_absolute_root(self) -> Path: @property def detached(self) -> Path: - return self.base_path / "detached" / "dataset-ro-crate-metadata.json" + return self.base_path / "detached-1.2" / "dataset-ro-crate-metadata.json" @property def detached_prefixed(self) -> Path: - return self.base_path / "detached" / "test-ro-crate-metadata.json" + return self.base_path / "detached-1.2" / "test-ro-crate-metadata.json" class InvalidROCrate12: diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 3d9d9c9c2..35981ec47 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -109,3 +109,23 @@ def valid_image_about(self) -> Path: @property def invalid_image_about(self) -> Path: return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_about" / "invalid" + + +class ValidROCrate13: + base_path = VALID_CRATES_DATA_PATH + + @property + def attached(self) -> Path: + return self.base_path / "ro-crate-1.3-attached" + + @property + def attached_absolute_root(self) -> Path: + return self.base_path / "ro-crate-1.3-absolute-root" + + @property + def detached(self) -> Path: + return self.base_path / "detached-1.3" / "dataset-ro-crate-metadata.json" + + @property + def detached_prefixed(self) -> Path: + return self.base_path / "detached-1.3" / "test-ro-crate-metadata.json" diff --git a/tests/shared.py b/tests/shared.py index 89b50beb6..fb9932d7c 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -38,11 +38,11 @@ # Identifier of the RO-Crate 1.1 profile. It is the profile assumed by the tests # that exercise 1.1 semantics on the 1.1 crate corpus, and the default used by -# `do_entity_test()`; tests that target another profile (e.g. 1.2) pass its +# `do_entity_test()`; tests that target another profile (e.g. 1.3) pass its # identifier explicitly. # # This is a hard-coded literal on purpose, NOT `DEFAULT_PROFILE_IDENTIFIER`: the -# validator's default profile changes over time (it is 1.2 today), while the tests +# validator's default profile changes over time (it is 1.3 today), while the tests # that use this constant must keep targeting 1.1 even when the default moves on. RO_CRATE_1_1_PROFILE_IDENTIFIER = "ro-crate-1.1" @@ -51,6 +51,7 @@ _PROFILE_CONTEXT_URIS = { "ro-crate-1.1": "https://w3id.org/ro/crate/1.1/context", "ro-crate-1.2": "https://w3id.org/ro/crate/1.2/context", + "ro-crate-1.3": "https://w3id.org/ro/crate/1.3/context", } _DEFAULT_CONTEXT_URI = _PROFILE_CONTEXT_URIS[RO_CRATE_1_1_PROFILE_IDENTIFIER] @@ -111,7 +112,7 @@ def _context_uri_for_profile(profile_identifier: str) -> str: Return the JSON-LD context matching the profile under test. Keeping the context aligned with the profile matters: re-serialising a 1.1 - context while validating against 1.2 trips the `ro-crate-1.2_4.2` check and + context while validating against 1.3 trips the `ro-crate-1.3_4.2` check and the test would fail for a reason unrelated to what it is asserting. """ return _PROFILE_CONTEXT_URIS.get(profile_identifier, _DEFAULT_CONTEXT_URI) diff --git a/tests/test_cli.py b/tests/test_cli.py index 49d14891a..b3db37ee2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -31,7 +31,7 @@ from rocrate_validator.utils.versioning import get_version from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import InvalidFileDescriptor, InvalidFileDescriptorEntity, ValidROC -from tests.ro_crates_v1_2 import ValidROCrate12 +from tests.ro_crates_v1_3 import ValidROCrate13 # set up logging logger = logging.getLogger(__name__) @@ -389,17 +389,17 @@ def test_profiles_check_reports_invalid_profile(cli_runner: CliRunner, fake_prof assert "FAIL" in result.output -def test_validate_no_auto_profile_falls_back_to_1_2(cli_runner: CliRunner): +def test_validate_no_auto_profile_falls_back_to_1_3(cli_runner: CliRunner): """ With auto-detection disabled the base `ro-crate` profile is used, and that - now resolves to 1.2. + now resolves to 1.3. """ result = cli_runner.invoke( cli, - ["validate", str(ValidROCrate12().attached), "--no-auto-profile", "--no-paging"], + ["validate", str(ValidROCrate13().attached), "--no-auto-profile", "--no-paging"], ) assert result.exit_code == 0 - assert "ro-crate-1.2" in result.output + assert "ro-crate-1.3" in result.output def test_validate_auto_profile_still_uses_the_declared_profile(cli_runner: CliRunner): @@ -412,11 +412,11 @@ def test_validate_auto_profile_still_uses_the_declared_profile(cli_runner: CliRu assert "ro-crate-1.1" in result.output -def test_profiles_describe_defaults_to_1_2(cli_runner: CliRunner): +def test_profiles_describe_defaults_to_1_3(cli_runner: CliRunner): """`profiles describe` with no argument describes the default profile.""" result = cli_runner.invoke(cli, ["profiles", "describe", "--no-paging"]) assert result.exit_code == 0 - assert "Profile: ro-crate-1.2" in result.output + assert "Profile: ro-crate-1.3" in result.output def test_extra_profiles_list(cli_runner: CliRunner, fake_profiles_path: Path): diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index 854aef099..7f7c46d36 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -162,7 +162,7 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): def test_check_dependencies_are_ordered_and_closed(profiles_path: str): profiles = Profile.load_profiles(profiles_path=profiles_path, severity=Severity.REQUIRED) - profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.2") + profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.3") requirements = {requirement.name: requirement for requirement in profile.requirements} existence = requirements["File Descriptor existence"] diff --git a/tests/unit/test_default_profile.py b/tests/unit/test_default_profile.py index 3e27e3f7e..86e91a956 100644 --- a/tests/unit/test_default_profile.py +++ b/tests/unit/test_default_profile.py @@ -23,16 +23,16 @@ from rocrate_validator.models import URI, Profile, Severity, ValidationSettings from rocrate_validator.services import get_profile, get_profiles, validate from tests.ro_crates import ValidROC -from tests.ro_crates_v1_2 import ValidROCrate12 +from tests.ro_crates_v1_3 import ValidROCrate13 from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER logger = logging.getLogger(__name__) -EXPECTED_DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.2" +EXPECTED_DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.3" -def test_default_profile_identifier_is_ro_crate_1_2(): - """The base RO-Crate profile used by default is 1.2.""" +def test_default_profile_identifier_is_ro_crate_1_3(): + """The base RO-Crate profile used by default is 1.3.""" assert DEFAULT_PROFILE_IDENTIFIER == EXPECTED_DEFAULT_PROFILE_IDENTIFIER @@ -50,7 +50,7 @@ def test_bare_token_resolves_to_highest_version_via_services(): def test_explicit_identifier_wins_over_token_resolution(): """An exact identifier is never overridden by the highest-version rule.""" assert get_profile(RO_CRATE_1_1_PROFILE_IDENTIFIER).identifier == RO_CRATE_1_1_PROFILE_IDENTIFIER - assert get_profile("ro-crate-1.2").identifier == "ro-crate-1.2" + assert get_profile("ro-crate-1.3").identifier == "ro-crate-1.3" def test_bare_token_resolves_to_highest_version_in_validation_context(): @@ -82,14 +82,14 @@ def test_version_sort_key_orders_versions_numerically(): assert Profile.version_sort_key(None) < Profile.version_sort_key("0.1") -def test_default_profile_validates_a_1_2_crate(): - """A 1.2 crate validates against the default profile without pinning it.""" +def test_default_profile_validates_a_1_3_crate(): + """A 1.3 crate validates against the default profile without pinning it.""" settings = ValidationSettings( - rocrate_uri=URI(str(ValidROCrate12().attached)), + rocrate_uri=URI(str(ValidROCrate13().attached)), requirement_severity=Severity.REQUIRED, ) result = validate(settings) - assert result.passed(), "A valid RO-Crate 1.2 should pass under the default profile" + assert result.passed(), "A valid RO-Crate 1.3 should pass under the default profile" def test_default_profile_rejects_a_1_1_context(): @@ -97,7 +97,7 @@ def test_default_profile_rejects_a_1_1_context(): A 1.1-era crate no longer passes under the default profile. This documents the breaking part of the default switch: the crate declares the - 1.1 JSON-LD context, which 1.2 requires to be the 1.2 one. + 1.1 JSON-LD context, which 1.3 requires to be the 1.3 one. """ settings = ValidationSettings( rocrate_uri=URI(str(ValidROC().wrroc_paper)), @@ -106,8 +106,8 @@ def test_default_profile_rejects_a_1_1_context(): result = validate(settings) assert not result.passed() failed_check_ids = {issue.check.identifier for issue in result.get_issues()} - assert "ro-crate-1.2_4.2" in failed_check_ids, ( - f"Expected the 1.2 context check to fail, got: {sorted(failed_check_ids)}" + assert "ro-crate-1.3_4.2" in failed_check_ids, ( + f"Expected the 1.3 context check to fail, got: {sorted(failed_check_ids)}" ) # ...and it still passes when explicitly pinned to 1.1 From 8844dbdcd2325a2f4e24c3694baf354de1f53354 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 21 Sep 2026 16:30:58 +0200 Subject: [PATCH 082/271] rocrate-1.3: add contextual entities tests --- .../ro-crate-metadata.json | 47 ++ .../ro-crate-metadata.json | 40 ++ .../valid/ro-crate-metadata.json | 91 ++++ .../ro-crate-metadata.json | 55 ++ .../no_fragment/ro-crate-metadata.json | 55 ++ .../valid/ro-crate-metadata.json | 55 ++ .../ro-crate-metadata.json | 24 + .../invalid_no_name/ro-crate-metadata.json | 24 + .../invalid_no_type/ro-crate-metadata.json | 25 + .../invalid_no_url/ro-crate-metadata.json | 25 + .../valid/ro-crate-metadata.json | 53 ++ .../ro-crate-metadata.json | 51 ++ .../ro-crate-metadata.json | 38 ++ .../invalid_no_ror_id/ro-crate-metadata.json | 52 ++ .../ro-crate-metadata.json | 45 ++ .../valid/ro-crate-metadata.json | 74 +++ .../ro-crate-metadata.json | 57 ++ .../invalid_no_orcid/ro-crate-metadata.json | 67 +++ .../valid/ro-crate-metadata.json | 68 +++ .../invalid_no_name/ro-crate-metadata.json | 49 ++ .../invalid_no_url/ro-crate-metadata.json | 49 ++ .../invalid_no_version/ro-crate-metadata.json | 49 ++ .../valid/ro-crate-metadata.json | 71 +++ .../ro-crate-metadata.json | 76 +++ .../valid_computer_language/script.sh | 0 .../ro-crate-metadata.json | 60 +++ .../test_metadata_contextualEntities_v1_3.py | 509 ++++++++++++++++++ tests/ro_crates_v1_3.py | 112 ++++ 28 files changed, 1921 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/script.sh create mode 100644 tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json new file mode 100644 index 000000000..4e2558a72 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json @@ -0,0 +1,47 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Contextual entity @id format — invalid (bare ContactPoint id)", + "description": "RO-Crate where a ContactPoint contextual entity has a bare relative @id (no '#' prefix, not an absolute URI).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0001-0000-0001" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://orcid.org/0000-0001-0000-0001", + "@type": "Person", + "name": "Alice Researcher", + "contactPoint": { + "@id": "alice-contact" + } + }, + { + "@id": "alice-contact", + "@type": "ContactPoint", + "name": "Alice's contact point", + "email": "alice@example.edu" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json new file mode 100644 index 000000000..d8c267e93 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Contextual entity @id format — invalid (bare PropertyValue id)", + "description": "RO-Crate where a PropertyValue contextual entity has a bare relative @id (no '#' prefix, not an absolute URI).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "identifier": { + "@id": "uuid-property" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "uuid-property", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..f6bb4929e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json @@ -0,0 +1,91 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Contextual entity @id format — valid", + "description": "RO-Crate whose contextual entities all use '#'-prefixed local identifiers or absolute URI permalinks.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0001-0000-0001" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + }, + "contentLocation": { + "@id": "#lab-place" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://orcid.org/0000-0001-0000-0001", + "@type": "Person", + "name": "Alice Researcher", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#alice-contact" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#alice-contact", + "@type": "ContactPoint", + "name": "Alice's contact point", + "email": "alice@example.edu" + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example University contact", + "email": "contact@example.edu" + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#lab-place", + "@type": "Place", + "name": "Example Research Lab", + "description": "The physical location where the research took place." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json new file mode 100644 index 000000000..6cbc836ca --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Encoding format entity — info (no WebPageElement)", + "description": "RO-Crate whose encoding format entity has an `@id` pointing to a section of a webpage (fragment identifier) but whose `@type` does NOT include `WebPageElement`. The MAY check emits an `sh:Info` suggestion.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Tabular data", + "description": "A CSV file.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://example.org/encoding-formats.html#csv" + } + ], + "contentSize": "1024" + }, + { + "@id": "https://example.org/encoding-formats.html#csv", + "@type": "WebPage", + "name": "CSV encoding format section", + "description": "Section of a webpage that describes the CSV encoding format (but not typed as WebPageElement)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json new file mode 100644 index 000000000..829ba2969 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Encoding format entity — no fragment (MAY does not apply)", + "description": "RO-Crate whose encoding format entity has an `@id` that is NOT a fragment identifier (does not point to a section of a webpage). The MAY shape should not apply and no `sh:Info` is emitted.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Tabular data", + "description": "A CSV file.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://example.org/csv-format.html" + } + ], + "contentSize": "1024" + }, + { + "@id": "https://example.org/csv-format.html", + "@type": "WebPage", + "name": "CSV encoding format page", + "description": "A full webpage describing the CSV encoding format (no fragment in @id)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..7f21847a6 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Encoding format entity — valid (WebPageElement)", + "description": "RO-Crate whose File references an encoding format entity whose `@id` points to a section of a webpage and whose `@type` includes `WebPageElement` (MAY condition satisfied).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Tabular data", + "description": "A CSV file.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://example.org/encoding-formats.html#csv" + } + ], + "contentSize": "1024" + }, + { + "@id": "https://example.org/encoding-formats.html#csv", + "@type": ["WebPage", "WebPageElement"], + "name": "CSV encoding format section", + "description": "Section of a webpage that describes the CSV encoding format." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json new file mode 100644 index 000000000..5a5d0edcd --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json @@ -0,0 +1,24 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity — missing description", + "description": "RO-Crate whose license entity is missing the `description` property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" } + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json new file mode 100644 index 000000000..a5984a717 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json @@ -0,0 +1,24 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity — missing name", + "description": "RO-Crate whose license entity is missing the `name` property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" } + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json new file mode 100644 index 000000000..29ef7b495 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json @@ -0,0 +1,25 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity — missing CreativeWork type", + "description": "RO-Crate whose license entity has an absolute URL @id but is missing CreativeWork in @type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" } + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "Thing", + "name": "MIT License", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json new file mode 100644 index 000000000..f06f605c5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json @@ -0,0 +1,25 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity — non-absolute URL @id", + "description": "RO-Crate whose license entity is identified by a fragment URI, not an absolute HTTP(S) URL.", + "datePublished": "2024-01-01", + "license": { "@id": "#local-license" } + }, + { + "@id": "#local-license", + "@type": "CreativeWork", + "name": "Local License", + "description": "A locally-defined license without an absolute URL identifier." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json new file mode 100644 index 000000000..b5ff18319 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json @@ -0,0 +1,53 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity — valid", + "description": "RO-Crate with a well-formed license entity: absolute HTTPS URL @id, CreativeWork type, name and description.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "funder": { + "@id": "https://ror.org/03yrm5c26" + }, + "publisher": { + "@id": "https://ror.org/03yrm5c26" + } + }, + { + "@id": "https://ror.org/03yrm5c26", + "@type": "Organization", + "name": "Wellcome Trust", + "description": "The Wellcome Trust is a global charitable foundation dedicated to improving health by supporting scientific research and innovation.", + "url": "https://wellcome.org/", + "contactPoint": { + "@id": "mailto:info@wellcome.org" + } + }, + { + "@id": "mailto:info@wellcome.org", + "@type": "ContactPoint", + "contactType": "general inquiries", + "name": "Wellcome Trust Contact" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json new file mode 100644 index 000000000..0a17e92e3 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity — contactPoint not ContactPoint type", + "description": "RO-Crate with an Organization whose contactPoint does not reference a ContactPoint entity.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041", + "contactPoint": { "@id": "#not-a-contactpoint" } + }, + { + "@id": "#not-a-contactpoint", + "@type": "Thing", + "name": "Not a ContactPoint" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { "@id": "mailto:tim.luckett@uts.edu.au" } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json new file mode 100644 index 000000000..446b7a5d8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity — no author/publisher contactPoint", + "description": "RO-Crate where neither author nor publisher has a contactPoint property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json new file mode 100644 index 000000000..d4ea0fe27 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json @@ -0,0 +1,52 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity — non-ROR @id", + "description": "RO-Crate with an Organization entity whose @id is not a ROR identifier.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://example.com/org" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://example.com/org", + "@type": "Organization", + "name": "Example Organization", + "url": "https://example.com/org", + "contactPoint": { "@id": "mailto:contact@example.com" } + }, + { + "@id": "mailto:contact@example.com", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@example.com" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { "@id": "mailto:tim.luckett@uts.edu.au" } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json new file mode 100644 index 000000000..34536d50d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json @@ -0,0 +1,45 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity — no contactPoint", + "description": "RO-Crate with an Organization entity that has a ROR @id but no contactPoint property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { "@id": "mailto:tim.luckett@uts.edu.au" } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json new file mode 100644 index 000000000..f9ce66138 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json @@ -0,0 +1,74 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity — valid", + "description": "RO-Crate with a well-formed Organization entity: ROR @id, contactPoint referencing ContactPoint, and author with contactPoint.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "publisher": { + "@id": "https://ror.org/03f0f6041" + }, + "author": { + "@id": "https://orcid.org/0000-0001-6121-5409" + }, + "funder": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041", + "contactPoint": { + "@id": "mailto:contact@uts.edu.au" + } + }, + { + "@id": "mailto:contact@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@uts.edu.au", + "name": "UTS contact point" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "affiliation": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "name": "Tim Luckett's contact point" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json new file mode 100644 index 000000000..231cf10f2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Person entity — invalid (affiliation not Organization)", + "description": "RO-Crate with a Person entity whose affiliation references a Thing instead of an Organization.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "author": { + "@id": "https://orcid.org/0000-0001-6121-5409" + } + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "affiliation": { + "@id": "https://example.org/groups/research-lab" + } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "name": "Tim Luckett's contact point" + }, + { + "@id": "https://example.org/groups/research-lab", + "@type": "Thing", + "name": "Research Lab (not an Organization)" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json new file mode 100644 index 000000000..6a3895c46 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json @@ -0,0 +1,67 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Person entity — invalid (no ORCID @id)", + "description": "RO-Crate with a Person entity whose @id is not an ORCID identifier.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "author": { + "@id": "https://example.org/persons/john-doe" + } + }, + { + "@id": "https://example.org/persons/john-doe", + "@type": "Person", + "name": "John Doe", + "contactPoint": { + "@id": "mailto:john.doe@example.org" + }, + "affiliation": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "mailto:john.doe@example.org", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "john.doe@example.org", + "name": "John Doe's contact point" + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "contactPoint": { + "@id": "mailto:contact@uts.edu.au" + } + }, + { + "@id": "mailto:contact@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@uts.edu.au", + "name": "UTS contact point" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0181aba78 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json @@ -0,0 +1,68 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Person entity — valid", + "description": "RO-Crate with a well-formed Person entity: ORCID @id, contactPoint referencing ContactPoint, and affiliation referencing Organization.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "author": { + "@id": "https://orcid.org/0000-0001-6121-5409" + } + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "affiliation": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "name": "Tim Luckett's contact point" + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://www.uts.edu.au/", + "contactPoint": { + "@id": "mailto:contact@uts.edu.au" + } + }, + { + "@id": "mailto:contact@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@uts.edu.au", + "name": "UTS contact point" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json new file mode 100644 index 000000000..86e06da74 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity — invalid (no name)", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that does NOT declare a `name` property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "version": "1.4.2", + "url": "https://example.org/analysis-tool", + "description": "Software used to perform the data analysis (missing `name`)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json new file mode 100644 index 000000000..9fb04a670 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity — invalid (no url)", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that does NOT declare a `url` property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "name": "Analysis Tool", + "version": "1.4.2", + "description": "Software used to perform the data analysis (missing `url`)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json new file mode 100644 index 000000000..ec8133140 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity — invalid (no version)", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that does NOT declare a version property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "name": "Analysis Tool", + "url": "https://example.org/analysis-tool", + "description": "Software used to perform the data analysis (missing `version`)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dd9796a57 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json @@ -0,0 +1,71 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity — valid", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that declares a version (used as instrument of a CreateAction).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example Research Institute Contact", + "email": "contact@example.org" + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "name": "Analysis Tool", + "version": "1.4.2", + "url": "https://example.org/analysis-tool", + "description": "Software used to perform the data analysis." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json new file mode 100644 index 000000000..d1cd32346 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json @@ -0,0 +1,76 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "ComputerLanguage entity — valid", + "description": "RO-Crate referencing a ComputerLanguage contextual entity (used as `programmingLanguage` of a Script) that declares name, url, version, and an optional alternateName.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example Research Institute Contact", + "email": "contact@example.org" + }, + { + "@id": "script.sh", + "@type": ["File", "SoftwareSourceCode"], + "name": "Analysis script", + "description": "A Bash analysis script.", + "encodingFormat": "application/x-sh", + "contentSize": "256", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "alternateName": "GNU Bourne-Again Shell", + "url": "https://www.gnu.org/software/bash/", + "version": "5.2" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/script.sh b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json new file mode 100644 index 000000000..f2a452552 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json @@ -0,0 +1,60 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "ComputerLanguage with alternateName — fully valid", + "description": "RO-Crate referencing a ComputerLanguage contextual entity that declares all MUST properties (`name`, `url`, `version`) AND the MAY `alternateName`. The crate is minimal — it contains no File/Directory data entities — so no OPTIONAL-level INFO checks should fire.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "mentions": { + "@id": "#bash" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example Research Institute Contact", + "email": "contact@example.org" + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "alternateName": "GNU Bourne-Again Shell", + "url": "https://www.gnu.org/software/bash/", + "version": "5.2" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py new file mode 100644 index 000000000..3fa58964a --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py @@ -0,0 +1,509 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import ContextualEntities +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__contextual_entities_crates__ = ContextualEntities() + +# Generic RECOMMENDED checks that fire on minimal test crates regardless of the +# contextual-entity-specific property being tested. +_GENERIC_RECOMMENDED_SKIP = [ + "ro-crate-1.3_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.3_56.1", # Root Data Entity: RECOMMENDED publisher +] + +# Correct IDs for funder/publisher checks (used in person entity tests). +_PERSON_VALID_SKIP = [ + "ro-crate-1.3_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.3_56.1", # Root Data Entity: RECOMMENDED publisher +] + + +# --------------------------------------------------------------------------- +# License entity: SHOULD be typed as CreativeWork (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_license_entity(): + """ + A license entity with an absolute URL @id, CreativeWork type, name and + description SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_license_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_license_entity_no_type(): + """ + A license entity missing `CreativeWork` in its @type SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_type, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: SHOULD be typed as CreativeWork"], + expected_triggered_issues=["A License entity SHOULD have `CreativeWork` in its `@type`"], + ) + + +def test_invalid_license_entity_no_url(): + """ + A license entity whose @id is not an absolute HTTP(S) URL SHOULD trigger + a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_url, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: SHOULD have absolute URL @id"], + expected_triggered_issues=["A License entity SHOULD have an absolute HTTP(S) URL as its @id"], + ) + + +# --------------------------------------------------------------------------- +# License entity: name and description SHOULD be present (SHOULD) +# --------------------------------------------------------------------------- + + +def test_invalid_license_entity_no_name(): + """ + A license entity missing the `name` property SHOULD trigger a RECOMMENDED + warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_name, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: RECOMMENDED properties"], + expected_triggered_issues=["License entities SHOULD have a name"], + ) + + +def test_invalid_license_entity_no_description(): + """ + A license entity missing the `description` property SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_description, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: RECOMMENDED properties"], + expected_triggered_issues=["License entities SHOULD have a description"], + ) + + +# --------------------------------------------------------------------------- +# Organization entity: SHOULD have ROR identifier as @id (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_organization_entity(): + """ + An Organization entity with a ROR @id, contactPoint referencing ContactPoint, + and author with contactPoint SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_organization_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_organization_no_ror_id(): + """ + An Organization entity whose @id is not a ROR identifier SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_organization_no_ror_id, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Organization: SHOULD have ROR identifier"], + expected_triggered_issues=["An Organization entity SHOULD have a ROR identifier as its @id"], + ) + + +# --------------------------------------------------------------------------- +# Organization/Person contactPoint: SHOULD reference ContactPoint entity (SHOULD) +# --------------------------------------------------------------------------- + + +def test_invalid_organization_contactpoint_no_entity(): + """ + An Organization whose contactPoint does not reference a ContactPoint entity + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_organization_contactpoint_no_entity, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Organization: SHOULD have contactPoint referencing ContactPoint"], + expected_triggered_issues=["An Organization's contactPoint SHOULD reference a ContactPoint contextual entity"], + ) + + +def test_invalid_organization_no_contactpoint(): + """ + An Organization without a contactPoint property (where the author still has + contactPoint so only the Organization-specific check fires). + """ + do_entity_test( + __contextual_entities_crates__.invalid_organization_no_contactpoint, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Organization: SHOULD have contactPoint referencing ContactPoint"], + expected_triggered_issues=["An Organization's contactPoint SHOULD reference a ContactPoint contextual entity"], + ) + + +# --------------------------------------------------------------------------- +# Author/Publisher: at least one SHOULD have contactPoint (SHOULD) +# --------------------------------------------------------------------------- + + +def test_invalid_no_author_publisher_contactpoint(): + """ + When neither the author nor the publisher Person/Organization has a + contactPoint property, the Root Data Entity SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_no_author_publisher_contactpoint, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: at least one author/publisher SHOULD have contactPoint"], + expected_triggered_issues=[ + "At least one author or publisher Person/Organization SHOULD have a contactPoint property" + ], + ) + + +# --------------------------------------------------------------------------- +# Person entity: SHOULD have ORCID identifier as @id (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_person_entity(): + """ + A Person entity with an ORCID @id, contactPoint referencing ContactPoint, + and affiliation referencing Organization SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_person_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_PERSON_VALID_SKIP, + ) + + +def test_invalid_person_no_orcid(): + """ + A Person entity whose @id is not an ORCID identifier SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_person_no_orcid, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Person: SHOULD have ORCID identifier"], + expected_triggered_issues=["A Person entity SHOULD have an ORCID identifier as its @id"], + ) + + +def test_invalid_person_affiliation_not_org(): + """ + A Person entity whose affiliation does not reference an Organization + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_person_affiliation_not_org, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Person: RECOMMENDED affiliation"], + expected_triggered_issues=["Persons SHOULD reference an Organization for affiliation"], + ) + + +# --------------------------------------------------------------------------- +# Any Contextual Entity: @id SHOULD be absolute URI or '#'-prefixed (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_contextual_entity_id_format(): + """ + A crate whose contextual entities all use absolute URI permalinks or + '#'-prefixed local identifiers SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_contextual_entity_id_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_PERSON_VALID_SKIP, + ) + + +def test_invalid_bare_contactpoint_id(): + """ + A ContactPoint contextual entity with a bare relative @id (no '#', not an + absolute URI) SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_contextual_entity_bare_contactpoint, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], + ) + + +def test_invalid_bare_propertyvalue_id(): + """ + A PropertyValue contextual entity with a bare relative @id (no '#', not an + absolute URI) SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_contextual_entity_bare_propertyvalue, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], + ) + + +# --------------------------------------------------------------------------- +# SoftwareApplication / ComputerLanguage: MUST have name, url, version (REQUIRED) +# --------------------------------------------------------------------------- + + +def test_valid_software_application(): + """ + A SoftwareApplication contextual entity that declares `name`, `url`, and + `version` MUST pass REQUIRED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_software_application, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_software_application_no_version(): + """ + A SoftwareApplication contextual entity missing the `version` property + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __contextual_entities_crates__.invalid_software_application_no_version, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`"], + expected_triggered_issues=["A SoftwareApplication or ComputerLanguage MUST have a `version` property"], + ) + + +def test_invalid_software_application_no_name(): + """ + A SoftwareApplication contextual entity missing the `name` property MUST + trigger a REQUIRED violation. + """ + do_entity_test( + __contextual_entities_crates__.invalid_software_application_no_name, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`"], + expected_triggered_issues=["A SoftwareApplication or ComputerLanguage MUST have a `name` property"], + ) + + +def test_invalid_software_application_no_url(): + """ + A SoftwareApplication contextual entity missing the `url` property MUST + trigger a REQUIRED violation. + """ + do_entity_test( + __contextual_entities_crates__.invalid_software_application_no_url, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`"], + expected_triggered_issues=["A SoftwareApplication or ComputerLanguage MUST have a `url` property"], + ) + + +def test_valid_computer_language(): + """ + A ComputerLanguage contextual entity (referenced as `programmingLanguage` + of a Script) that declares `name`, `url`, `version`, and an optional + `alternateName` MUST pass REQUIRED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_computer_language, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +# --------------------------------------------------------------------------- +# Encoding format entity: MAY include `WebPageElement` when @id is a section +# of a webpage (sh:Info — optional suggestion) [5.8] +# --------------------------------------------------------------------------- + +_ENCODING_FORMAT_MAY_REQUIREMENT = "Encoding format: OPTIONAL `WebPageElement` type for section references" + + +def test_info_encoding_format_no_webpageelement(): + """ + An encoding format entity whose `@id` contains a fragment identifier + (section of a webpage) but whose `@type` does NOT include `WebPageElement` + triggers an `sh:Info` suggestion at OPTIONAL severity (RO-Crate 1.3, § 5.8). + """ + do_entity_test( + __contextual_entities_crates__.info_encoding_format_no_webpageelement, + models.Severity.OPTIONAL, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=[_ENCODING_FORMAT_MAY_REQUIREMENT], + expected_triggered_issues=["MAY include `WebPageElement` in its `@type`"], + ) + + +def test_valid_encoding_format_with_webpageelement(): + """ + An encoding format entity whose `@id` contains a fragment identifier AND + whose `@type` already includes `WebPageElement` does NOT trigger the MAY + suggestion (the optional recommendation is satisfied). + """ + from rocrate_validator import services + + result = services.validate( + { + "rocrate_uri": str(__contextual_entities_crates__.valid_encoding_format_webpageelement), + "profile_identifier": "ro-crate-1.3", + "requirement_severity": models.Severity.OPTIONAL, + } + ) + failed_requirement_names = {issue.check.requirement.name for issue in result.get_issues()} + assert _ENCODING_FORMAT_MAY_REQUIREMENT not in failed_requirement_names, ( + f"The MAY requirement {_ENCODING_FORMAT_MAY_REQUIREMENT!r} should NOT fire " + f"when the encoding format entity already includes `WebPageElement` in its @type" + ) + + +def test_encoding_format_no_fragment_not_triggered(): + """ + An encoding format entity whose `@id` does NOT contain a fragment + identifier is outside the target of the MAY shape; the suggestion must + not fire. + """ + from rocrate_validator import services + + result = services.validate( + { + "rocrate_uri": str(__contextual_entities_crates__.encoding_format_no_fragment), + "profile_identifier": "ro-crate-1.3", + "requirement_severity": models.Severity.OPTIONAL, + } + ) + failed_requirement_names = {issue.check.requirement.name for issue in result.get_issues()} + assert _ENCODING_FORMAT_MAY_REQUIREMENT not in failed_requirement_names, ( + f"The MAY requirement {_ENCODING_FORMAT_MAY_REQUIREMENT!r} should NOT fire " + f"when the encoding format entity @id does not contain a fragment identifier" + ) + + +# --------------------------------------------------------------------------- +# SoftwareApplication / ComputerLanguage: MAY have `alternateName` [5.7] +# --------------------------------------------------------------------------- + +_SOFTWAREAPP_COMPUTERLANG_ALTERNATENAME_REQ = "SoftwareApplication or ComputerLanguage: OPTIONAL `alternateName`" + + +def test_info_software_application_no_alternatename(): + """ + A SoftwareApplication contextual entity without an `alternateName` + property triggers an `sh:Info` suggestion at OPTIONAL severity + (RO-Crate 1.3, § 5.7). The existing `valid` SoftwareApplication crate + (used by `test_valid_software_application` at REQUIRED severity) is + reused here: at REQUIRED it passes, at OPTIONAL the MAY suggestion fires. + """ + do_entity_test( + __contextual_entities_crates__.valid_software_application, + models.Severity.OPTIONAL, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=[_SOFTWAREAPP_COMPUTERLANG_ALTERNATENAME_REQ], + expected_triggered_issues=["MAY declare an `alternateName` property"], + ) + + +def test_valid_computer_language_with_alternatename_no_info(): + """ + A ComputerLanguage contextual entity that already declares an + `alternateName` property does NOT trigger the MAY suggestion — the + optional recommendation is satisfied. + """ + do_entity_test( + __contextual_entities_crates__.valid_computer_language_with_alternatename, + models.Severity.OPTIONAL, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_computer_language_alternatename_passes_at_optional(): + """ + A minimal, fully-specified crate with a + ComputerLanguage declaring `name`, `url`, `version`, and the MAY + `alternateName` passes ALL validation checks including OPTIONAL-level + INFO suggestions. + """ + do_entity_test( + __contextual_entities_crates__.valid_computer_language_with_alternatename, + models.Severity.OPTIONAL, + True, + profile_identifier="ro-crate-1.3", + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 35981ec47..2b6e28d1f 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -111,6 +111,118 @@ def invalid_image_about(self) -> Path: return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_about" / "invalid" +class ContextualEntities: + CONTEXTUAL_ENTITIES_CRATES_PATH = BASE_PATH / "10_metadata_contextualEntities" + + # --- License entity: SHOULD be typed as CreativeWork --- + @property + def valid_license_entity(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "valid" + + @property + def invalid_license_entity_no_type(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_type" + + @property + def invalid_license_entity_no_url(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_url" + + @property + def invalid_license_entity_no_name(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_name" + + @property + def invalid_license_entity_no_description(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_description" + + # --- Organization entity: SHOULD have ROR @id and contactPoint --- + @property + def valid_organization_entity(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "valid" + + @property + def invalid_organization_no_ror_id(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_no_ror_id" + + @property + def invalid_organization_no_contactpoint(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_org_no_contactpoint" + + @property + def invalid_organization_contactpoint_no_entity(self) -> Path: + return ( + self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_contactpoint_no_contactpoint_entity" + ) + + @property + def invalid_no_author_publisher_contactpoint(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_no_author_publisher_contactpoint" + + # --- Person entity: SHOULD have ORCID @id and valid affiliation --- + @property + def valid_person_entity(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "person_entity" / "valid" + + @property + def invalid_person_no_orcid(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "person_entity" / "invalid_no_orcid" + + @property + def invalid_person_affiliation_not_org(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "person_entity" / "invalid_affiliation_not_org" + + # --- Any Contextual Entity: SHOULD have absolute URI or '#'-prefixed @id --- + @property + def valid_contextual_entity_id_format(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "contextual_entity_id_format" / "valid" + + @property + def invalid_contextual_entity_bare_contactpoint(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "contextual_entity_id_format" / "invalid_bare_contactpoint" + + @property + def invalid_contextual_entity_bare_propertyvalue(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "contextual_entity_id_format" / "invalid_bare_propertyvalue" + + # --- SoftwareApplication / ComputerLanguage: MUST have name, url, version (5.7) --- + @property + def valid_software_application(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "valid" + + @property + def invalid_software_application_no_version(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "invalid_no_version" + + @property + def invalid_software_application_no_name(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "invalid_no_name" + + @property + def invalid_software_application_no_url(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "invalid_no_url" + + @property + def valid_computer_language(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "valid_computer_language" + + @property + def valid_computer_language_with_alternatename(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "valid_with_alternatename" + + # --- Encoding Format entity: MAY include `WebPageElement` when @id has a fragment (5.8) --- + @property + def valid_encoding_format_webpageelement(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "valid" + + @property + def info_encoding_format_no_webpageelement(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "info_no_webpageelement" + + @property + def encoding_format_no_fragment(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "no_fragment" + + class ValidROCrate13: base_path = VALID_CRATES_DATA_PATH From ce4856c93bf869370bc07530a120c87a2a91c6a5 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 21 Sep 2026 17:21:38 +0200 Subject: [PATCH 083/271] rocrate-1.3: add metadata document tests --- .../context_reference/invalid/data.csv | 2 + .../invalid/ro-crate-metadata.json | 52 ++ .../context_reference/valid/data.csv | 2 + .../valid/ro-crate-metadata.json | 52 ++ .../invalid/data.csv | 2 + .../invalid/ro-crate-metadata.json | 41 ++ .../valid/data.csv | 2 + .../valid/ro-crate-metadata.json | 94 ++++ .../format/compacted/data set/.gitkeep | 0 .../format/compacted/data set2/.gitkeep | 0 .../format/compacted/data set3/.gitkeep | 0 .../compacted/pics/2017-06-11%2012.56.14.jpg | 0 .../compacted/pics/2018-06-11 12.56.14.jpg | 0 .../compacted/pics/2019-06-11 12.56.14.jpg | 0 .../format/compacted/pics/sepia_fence.jpg | 0 .../format/compacted/ro-crate-metadata.json | 216 ++++++++ .../format/flattened/ro-crate-metadata.json | 31 ++ .../format/jsonld/ro-crate-metadata.json | 19 + .../format/utf8/ro-crate-metadata.json | 25 + .../invalid/ro-crate-metadata.json | 38 ++ .../valid/ro-crate-metadata.json | 93 ++++ .../invalid/ro-crate-metadata.json | 40 ++ .../valid/ro-crate-metadata.json | 79 +++ .../invalid/data.csv | 2 + .../invalid/ro-crate-metadata.json | 49 ++ .../valid/data.csv | 2 + .../valid/ro-crate-metadata.json | 94 ++++ .../invalid/ro-crate-metadata.json | 65 +++ .../valid/ro-crate-metadata.json | 63 +++ .../invalid/ro-crate-metadata.json | 39 ++ .../valid/ro-crate-metadata.json | 79 +++ .../test_metadata_document_v1_3.py | 498 ++++++++++++++++++ tests/ro_crates_v1_3.py | 80 +++ 33 files changed, 1759 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set/.gitkeep create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set2/.gitkeep create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set3/.gitkeep create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2017-06-11%2012.56.14.jpg create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2018-06-11 12.56.14.jpg create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2019-06-11 12.56.14.jpg create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/sepia_fence.jpg create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a9fd3afef --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json @@ -0,0 +1,52 @@ +{ + "@context": "https://w3id.org/ro/crate/1.4-DRAFT/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.4-DRAFT" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a18c32691 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json @@ -0,0 +1,52 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3/" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..2c5d41597 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json new file mode 100644 index 000000000..8cd97e31b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json @@ -0,0 +1,94 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set/.gitkeep b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set2/.gitkeep b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set2/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set3/.gitkeep b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set3/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2017-06-11%2012.56.14.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2017-06-11%2012.56.14.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2018-06-11 12.56.14.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2018-06-11 12.56.14.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2019-06-11 12.56.14.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2019-06-11 12.56.14.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/sepia_fence.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/sepia_fence.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json new file mode 100644 index 000000000..083bb7a8d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json @@ -0,0 +1,216 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.3/context", + "https://w3id.org/ro/terms/workflow-run/context" + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "https://schema.org/name": "My Pictures", + "description": "A collection of my pictures", + "datePublished": "2024-05-17T01:04:52+01:00", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/2018-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/2019-06-11 12.56.14.jpg" + }, + { + "@id": "data%20set/" + }, + { + "@id": "data%20set2/" + }, + { + "@id": "data set3/" + }, + { + "@id": "pics/sepia_fence.jpg" + }, + { + "@id": "file:///tmp/test.txt" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + } + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1", + "@type": "CreativeWork", + "name": "Other Profile", + "version": "0.1" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4", + "softwareRequirements": { + "@id": "https://example.com/foobar/1.0.0/" + }, + "version": "1.0" + }, + { + "@id": "https://example.com/foobar/1.0.0/", + "@type": "SoftwareApplication", + "name": "foobar", + "softwareVersion": "1.0.0", + "version": "1.0", + "url": "https://example.org/" + }, + { + "@id": "#SepiaConversion_1", + "@type": "CreateAction", + "name": "Convert dog image to sepia", + "description": "convert -sepia-tone 80% pics/2017-06-11\\ 12.56.14.jpg pics/sepia_fence.jpg", + "startTime": "2024-05-17T01:04:50+01:00", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + "result": { + "@id": "pics/sepia_fence.jpg" + }, + "agent": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "actionStatus": "http://schema.org/FailedActionStatus", + "error": "this is just to test the error property", + "environment": [ + { + "@id": "#height-limit-pv" + }, + { + "@id": "#width-limit-pv" + } + ], + "containerImage": "https://example.com/imagemagick.sif" + }, + { + "@id": "#width-limit-pv", + "@type": "PropertyValue", + "name": "MAGICK_WIDTH_LIMIT", + "value": "4096" + }, + { + "@id": "#height-limit-pv", + "@type": "PropertyValue", + "name": "MAGICK_HEIGHT_LIMIT", + "value": "3072" + }, + { + "@id": "file:///tmp/test.txt", + "@type": "File", + "description": "A test file", + "encodingFormat": "text/plain" + }, + { + "@id": "pics/2017-06-11%2012.56.14.jpg", + "@type": "File", + "description": "Original image", + "encodingFormat": "image/jpeg", + "name": "2017-06-11 12.56.14.jpg (input)", + "author": { + "@id": "https://orcid.org/0000-0002-3545-944X" + } + }, + { + "@id": "pics/2018-06-11%2012.56.14.jpg", + "@type": "File", + "description": "Original image", + "encodingFormat": "image/jpeg", + "name": "2018-06-11 12.56.14.jpg (input)" + }, + { + "@id": "pics/2019-06-11 12.56.14.jpg", + "@type": "File", + "description": "Original image", + "encodingFormat": "image/jpeg", + "name": "2018-06-11 12.56.14.jpg (input)" + }, + { + "@id": "data%20set/", + "@type": "Dataset", + "name": "Data set", + "description": "A dataset", + "datePublished": "2024-05-17T01:04:52+01:00", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "data%20set2/", + "@type": "Dataset", + "name": "Data set 2", + "description": "A dataset", + "datePublished": "2024-05-17T01:04:52+01:00", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "data set3/", + "@type": "Dataset", + "name": "Data set 3", + "description": "A dataset", + "datePublished": "2024-05-17T01:04:52+01:00", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "pics/sepia_fence.jpg", + "@type": "File", + "description": "The converted picture, now sepia-colored", + "encodingFormat": "image/jpeg", + "name": "sepia_fence (output)" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://orcid.org/0000-0002-3545-944X", + "@type": "Person", + "name": "Peter Sefton" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json new file mode 100644 index 000000000..7a3831b16 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@type": "CreativeWork", + "@id": "ro-crate-metadata.json", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "This is a test dataset", + "description": "This is a test dataset", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-11-05", + "hasPart": [ + { + "@type": "File", + "name": "File in a nested entity" + } + ] + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json new file mode 100644 index 000000000..5ed1378a0 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json @@ -0,0 +1,19 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "name": "Counterexample: invalid JSON-LD syntax", + "description": "This RO-Crate is INCORRECT because it contains invalid JSON syntax. The trailing comma after the last property and the missing closing brace make it invalid JSON, and therefore invalid JSON-LD." + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid JSON-LD Example", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + } + ] diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json new file mode 100644 index 000000000..a9372f409 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json @@ -0,0 +1,25 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Locale: café résumé naïve", + "description": "This RO-Crate is INCORRECT because it contains non-UTF-8 characters.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..b342d366e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Named entity without #-prefix — invalid", + "description": "RO-Crate where a local Person entity uses a bare relative @id instead of '#'-prefixed.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "author": {"@id": "alice"}, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "alice", + "@type": "Person", + "name": "Alice Researcher", + "affiliation": {"@id": "example-org"} + }, + { + "@id": "example-org", + "@type": "Organization", + "name": "Example University" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..012c0355f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json @@ -0,0 +1,93 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Named entity #-prefix — valid", + "description": "RO-Crate where all local Person and Organization entities use '#'-prefixed @id.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "author": { + "@id": "https://orcid.org/0000-0001-0000-0001" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://orcid.org/0000-0001-0000-0001", + "@type": "Person", + "name": "Alice Researcher", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..731a063e7 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Parent traversal — invalid", + "description": "RO-Crate with a contextual entity whose @id uses ../ to climb out of the RO-Crate Root.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "hasPart": [], + "citation": {"@id": "../external-publication.html"} + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "../external-publication.html", + "@type": "ScholarlyArticle", + "name": "A publication outside the crate root", + "description": "A contextual entity referenced via ../ — demonstrates parent directory traversal." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json new file mode 100644 index 000000000..351fa0179 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "No parent traversal — valid", + "description": "RO-Crate with no @id values that use ../ to climb out of the root.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a990ab0b2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: orphaned contextual entity", + "description": "This RO-Crate INCORRECTLY includes a contextual entity (the Person below) that is NOT linked from any other entity in the @graph. In RO-Crate 1.3, any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity is orphaned - nothing references https://orcid.org/0000-0002-1825-0097.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json new file mode 100644 index 000000000..8cd97e31b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json @@ -0,0 +1,94 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..e7d17895e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json @@ -0,0 +1,65 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol — invalid", + "description": "RO-Crate that types an entity with an https Schema.org IRI and references Schema.org terms through https @id values.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://schema.org/identifier" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "https://schema.org/Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "https://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json new file mode 100644 index 000000000..c2ce88add --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol — valid", + "description": "RO-Crate whose Schema.org IRIs all use the http namespace that the RO-Crate @context defines.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "http://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..d82f4167a --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json @@ -0,0 +1,39 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Percent-encoded UTF-8 — invalid", + "description": "RO-Crate where a contextual entity @id uses percent-encoded non-ASCII bytes instead of native UTF-8.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#donn%C3%A9es-schema", + "@type": "DefinedTerm", + "name": "Données (percent-encoded)", + "description": "A contextual entity whose @id uses %C3%A9 (percent-encoded é) instead of native UTF-8." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json new file mode 100644 index 000000000..69db3a8e2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "UTF-8 identifiers — valid", + "description": "RO-Crate where no @id uses percent-encoded non-ASCII bytes.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py new file mode 100644 index 000000000..58f2b1ea2 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py @@ -0,0 +1,498 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +from unittest.mock import MagicMock + +import pytest + +from rocrate_validator import models, services +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import requirement as requirement_module +from rocrate_validator.requirements.shacl import requirements as shacl_requirements_module +from rocrate_validator.rocrate.plain import ROCrateLocalFolder +from rocrate_validator.utils.uri import URI +from tests.ro_crates_v1_3 import MetadataDocument, MetadataDocumentFormat +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_document_crates__ = MetadataDocument() +__metadata_document_format_crates__ = MetadataDocumentFormat() + + +def test_not_utf8(): + """ + Test that the metadata document is valid when it is not UTF-8 encoded. + """ + do_entity_test( + __metadata_document_format_crates__.not_utf8, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor UTF-8 encoding"], + expected_triggered_issues=['RO-Crate file descriptor "ro-crate-metadata.json" is not UTF-8 encoded'], + ) + + +def test_utf8_check_is_skipped_for_metadata_dict(monkeypatch, tmp_path): + """Metadata-only validation must not read a file descriptor from the CWD.""" + crate_path = __metadata_document_crates__.valid_context_reference + with (crate_path / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata_dict = json.load(stream) + + descriptor_reads = [] + original_get_file_content = ROCrateLocalFolder.get_file_content + + def track_descriptor_reads(crate, path, binary_mode=True): + if path.name == "ro-crate-metadata.json": + descriptor_reads.append((path, binary_mode)) + return original_get_file_content(crate, path, binary_mode) + + monkeypatch.setattr(ROCrateLocalFolder, "get_file_content", track_descriptor_reads) + monkeypatch.chdir(tmp_path) + do_entity_test( + crate_path, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + metadata_dict=metadata_dict, + metadata_only=True, + ) + assert descriptor_reads == [] + + +def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_path): + """Checks depending on metadata should defer to the descriptor checks.""" + requirement_logger = MagicMock() + shacl_logger = MagicMock() + monkeypatch.setattr(requirement_module, "logger", requirement_logger) + monkeypatch.setattr(shacl_requirements_module, "logger", shacl_logger) + + result = services.validate( + models.ValidationSettings( + rocrate_uri=URI(tmp_path), + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.OPTIONAL, + ) + ) + + issues = [issue.message for issue in result.get_issues()] + assert result.passed() is False + assert any( + issue is not None and 'file descriptor "ro-crate-metadata.json" is not present' in issue for issue in issues + ) + assert not any( + issue is not None + and any(message in issue for message in ("Unexpected error", "Error checking", "not in the correct format")) + for issue in issues + ) + assert requirement_logger.warning.call_count == 0 + assert shacl_logger.warning.call_count == 0 + + +def test_missing_descriptor_raises_specific_metadata_error(tmp_path): + """Reading a missing descriptor must preserve its metadata-specific cause.""" + crate = ROCrateLocalFolder(tmp_path) + + with pytest.raises(ROCrateMetadataNotFoundError) as error: + crate.metadata.as_dict() + + assert error.value.path == "ro-crate-metadata.json" + assert isinstance(error.value.__cause__, FileNotFoundError) + + +def test_not_json(): + """ + Test that the metadata document is valid when it is not JSON-LD. + """ + do_entity_test( + __metadata_document_format_crates__.not_jsonld, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON format"], + expected_triggered_issues=['RO-Crate file descriptor "ro-crate-metadata.json" is not valid JSON'], + ) + + +def test_not_flattened(): + """ + Test that the metadata document is valid when it is not flattened. + """ + do_entity_test( + __metadata_document_format_crates__.not_flattened, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON-LD format"], + expected_triggered_issues=[ + 'RO-Crate file descriptor "ro-crate-metadata.json" is not fully flattened at entity "./"' + ], + ) + + +def test_not_compacted(): + """ + Test that the metadata document is valid when it is not compacted. + """ + do_entity_test( + __metadata_document_format_crates__.not_compacted, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON-LD format"], + expected_triggered_issues=['The 1 occurrence of the "https://schema.org/name" URI cannot be used as a key'], + ) + + +def test_invalid_context_reference(): + """ + Test that the metadata document is invalid when it has an invalid context reference. + """ + do_entity_test( + __metadata_document_crates__.invalid_context_reference, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON-LD format"], + expected_triggered_issues=[ + ( + 'RO-Crate file descriptor "ro-crate-metadata.json" ' + 'does not reference the required context "https://w3id.org/ro/crate/1.3/context"' + ) + ], + ) + + +def test_valid_context_reference(): + """ + Test that the metadata document is valid when it has a valid context reference. + """ + do_entity_test( + __metadata_document_crates__.valid_context_reference, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_singleton_entity_property_is_recommended_as_a_single_value(): + """ + Test the RO-Crate 1.3 recommendation that a singleton property array is + represented as a single value in compacted JSON-LD. + """ + do_entity_test( + __metadata_document_crates__.valid_context_reference, + models.Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "hasPart" SHOULD be represented as a single value'], + profile_identifier="ro-crate-1.3", + ) + + +def test_not_referenced_contextual_entity(): + """ + Test that the metadata document is not valid + when it has a contextual entity that is not referenced by any other entity in the graph. + """ + do_entity_test( + __metadata_document_crates__.not_referenced_contextual_entity, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Contextual Entity RECOMMENDED references"], + expected_triggered_issues=["Contextual entities SHOULD be referenced by other entities."], + ) + + +def test_referenced_contextual_entity(): + """ + Test that the metadata document is valid + when it has a contextual entity that is referenced by other entities in the graph. + """ + do_entity_test( + __metadata_document_crates__.valid_referenced_contextual_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_described_contextual_entity(): + """ + Test that the metadata document is valid + when it has a contextual entity that is described in the same graph. + """ + do_entity_test( + __metadata_document_crates__.described_contextual_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_not_described_contextual_entity(): + """ + Test that the metadata document is not valid + when it has a contextual entity that is not described in the same graph. + """ + do_entity_test( + __metadata_document_crates__.not_described_contextual_entity, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Contextual Entity RECOMMENDED description"], + expected_triggered_issues=[ + ( + "Contextual entities that are referenced by other entities SHOULD be " + "described in the same @graph, with at least an RDF type specified." + ) + ], + ) + + +# --------------------------------------------------------------------------- +# @id format: no ../ parent traversal (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_no_parent_traversal(): + """ + Crate with clean relative @id paths passes the no-../ check. + """ + do_entity_test( + __metadata_document_crates__.valid_no_parent_traversal, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_no_parent_traversal(): + """ + Crate with an entity @id containing ../ fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_document_crates__.invalid_no_parent_traversal, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["SHOULD NOT contain '../'"], + ) + + +# --------------------------------------------------------------------------- +# @id format: native UTF-8, not percent-encoded (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_utf8_identifiers(): + """ + Crate with native UTF-8 characters in @id passes the encoding check. + """ + do_entity_test( + __metadata_document_crates__.valid_utf8_identifiers, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_utf8_identifiers(): + """ + Crate with percent-encoded non-ASCII bytes in @id fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_document_crates__.invalid_utf8_identifiers, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["percent-encoded non-ASCII characters"], + ) + + +# --------------------------------------------------------------------------- +# @id format: named contextual entity SHOULD use # prefix (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_named_entity_id_format(): + """ + Crate where local Person/Organization entities use '#'-prefixed @id passes. + """ + do_entity_test( + __metadata_document_crates__.valid_named_entity_id_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_named_entity_id_format(): + """ + Crate where a local Person entity uses a bare relative @id (no '#') fails. + """ + do_entity_test( + __metadata_document_crates__.invalid_named_entity_id_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], + ) + + +# --------------------------------------------------------------------------- +# Schema.org IRIs: http namespace, not https (MUST) — see #188 +# --------------------------------------------------------------------------- + + +def test_valid_schema_org_iri_protocol(): + """ + Crate whose Schema.org IRIs all use the http namespace passes. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_schema_org_type_protocol(): + """ + An entity typed with an https Schema.org IRI is reported, naming the http term. + """ + do_entity_test( + __metadata_document_crates__.invalid_schema_org_iri_protocol, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Schema.org @type protocol"], + expected_triggered_issues=[ + "declares the @type 'https://schema.org/Organization'", + "MUST be typed 'Organization' (i.e. 'http://schema.org/Organization')", + ], + ) + + +def test_schema_org_type_presence_check_accepts_https_namespace(): + """ + The existing Schema.org type recommendation remains a presence check and + must not duplicate the dedicated protocol check. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.3", + ) + ) + assert any(detail.check.requirement.name == "Schema.org @type protocol" for detail in result.get_issues()) + assert not any( + issue.message == "RO-Crate Metadata Entity SHOULD include at least one Schema.org type" + for issue in result.get_issues() + ) + + +def test_https_schema_org_id_is_allowed(): + """ + An https Schema.org IRI used as an @id remains a valid resource identifier. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + ) + + +def test_https_entity_id_is_not_recommended(): + """ + An entity's own HTTPS Schema.org @id is not a compatibility issue. + """ + with (__metadata_document_crates__.valid_schema_org_iri_protocol / "ro-crate-metadata.json").open( + encoding="utf-8" + ) as stream: + metadata_dict = json.load(stream) + metadata_dict["@graph"].append( + { + "@id": "https://schema.org/StandaloneResource", + "@type": "Thing", + "name": "Standalone Schema.org resource", + } + ) + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + metadata_dict=metadata_dict, + metadata_only=True, + ) + + +def test_https_nested_schema_org_id_is_recommended_for_compatibility(): + """ + A nested HTTPS Schema.org @id remains valid but emits a compatibility recommendation. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + expected_triggered_requirements=["Schema.org @id compatibility"], + expected_triggered_issues=[ + "references the Schema.org IRI 'https://schema.org/identifier' through a nested @id", + "SHOULD use 'identifier' (i.e. 'http://schema.org/identifier')", + ], + ) + + +def test_schema_org_protocol_shapes_run_after_context_check(): + """ + The context check runs before the SHACL protocol shapes. A context failure + is reported, while the protocol shapes do not emit unrelated violations. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_context_reference), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.3", + ) + ) + + protocol_issues = { + issue.check.requirement.name + for issue in result.get_issues() + if issue.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + } + assert protocol_issues == set() + assert any( + issue.message + and 'does not reference the required context "https://w3id.org/ro/crate/1.3/context"' in issue.message + for issue in result.get_issues() + ) + assert not any( + detail.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + for detail in result.skipped_check_details + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 2b6e28d1f..f7d9c77e3 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -31,6 +31,86 @@ def ro_crates_path() -> Path: BASE_PATH = CRATES_DATA_PATH / "rocrate-1.3" +class MetadataDocument: + METADATA_DOCUMENT_CRATES_PATH = BASE_PATH / "1_metadata_document" + + @property + def invalid_context_reference(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "context_reference" / "invalid" + + @property + def valid_context_reference(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "context_reference" / "valid" + + @property + def not_referenced_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "referenced_contextual_entities" / "invalid" + + @property + def valid_referenced_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "referenced_contextual_entities" / "valid" + + @property + def described_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "described_contextual_entities" / "valid" + + @property + def not_described_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "described_contextual_entities" / "invalid" + + @property + def valid_no_parent_traversal(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "no_parent_traversal" / "valid" + + @property + def invalid_no_parent_traversal(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "no_parent_traversal" / "invalid" + + @property + def valid_utf8_identifiers(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "utf8_identifiers" / "valid" + + @property + def invalid_utf8_identifiers(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "utf8_identifiers" / "invalid" + + @property + def valid_named_entity_id_format(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "named_entity_id_format" / "valid" + + @property + def invalid_named_entity_id_format(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "named_entity_id_format" / "invalid" + + @property + def valid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "valid" + + @property + def invalid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "invalid" + + +class MetadataDocumentFormat: + METADATA_DOCUMENT_FORMAT_CRATES_PATH = MetadataDocument.METADATA_DOCUMENT_CRATES_PATH / "format" + + @property + def not_compacted(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "compacted" + + @property + def not_flattened(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "flattened" + + @property + def not_jsonld(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "jsonld" + + @property + def not_utf8(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "utf8" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From c0cc7891331bc4c6fdde6dc5ea114890f22a0e95 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 21 Sep 2026 17:28:02 +0200 Subject: [PATCH 084/271] fix(graph): treat schema:additionalType as a reference predicate Prevent schema:additionalType objects from being marked as validation candidates and cover the classification with a unit test. --- rocrate_validator/transformers/validation_candidate_marker.py | 1 + tests/unit/transformers/test_validation_candidate_marker.py | 1 + 2 files changed, 2 insertions(+) diff --git a/rocrate_validator/transformers/validation_candidate_marker.py b/rocrate_validator/transformers/validation_candidate_marker.py index 127c0f55d..4f1800fb6 100644 --- a/rocrate_validator/transformers/validation_candidate_marker.py +++ b/rocrate_validator/transformers/validation_candidate_marker.py @@ -34,6 +34,7 @@ { RDF.type, URIRef("http://schema.org/propertyID"), + URIRef("http://schema.org/additionalType"), URIRef("http://www.w3.org/ns/csvw#propertyUrl"), URIRef("http://schema.org/inDefinedTermSet"), URIRef("http://schema.org/encodingFormat"), diff --git a/tests/unit/transformers/test_validation_candidate_marker.py b/tests/unit/transformers/test_validation_candidate_marker.py index 4da207581..a7df129bc 100644 --- a/tests/unit/transformers/test_validation_candidate_marker.py +++ b/tests/unit/transformers/test_validation_candidate_marker.py @@ -42,6 +42,7 @@ def _is_validation_candidate(graph: Graph, iri: URIRef) -> bool: [ RDF.type, SCHEMA.propertyID, + SCHEMA.additionalType, CSVW.propertyUrl, SCHEMA.inDefinedTermSet, SCHEMA.encodingFormat, From 29d8806b102dad40888c630d4345cfb6d1645392 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 21 Sep 2026 18:13:23 +0200 Subject: [PATCH 085/271] test(profiles): narrow HTTPS Schema.org @id compatibility assertion --- .../ro-crate-1.2/test_metadata_document.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 193b5321c..e7ce584f5 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -440,13 +440,18 @@ def test_https_entity_id_is_not_recommended(): "name": "Standalone Schema.org resource", } ) - do_entity_test( - __metadata_document_crates__.valid_schema_org_iri_protocol, - models.Severity.RECOMMENDED, - True, - profile_identifier="ro-crate-1.2", - metadata_dict=metadata_dict, - metadata_only=True, + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.valid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + metadata_only=True, + metadata_dict=metadata_dict, + ) + ) + assert not any( + issue.check.requirement.name == "Schema.org @id compatibility" + for issue in result.get_issues() ) From 7e8a3abf398b9b2ef7147c25df1f7fb8b459602b Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 22 Sep 2026 10:34:34 +0200 Subject: [PATCH 086/271] rocrate-1.3: add attached rocrates tests --- .../invalid/data.csv | 3 + .../invalid/ro-crate-metadata.json | 54 +++++++ .../invalid/ro-crate-preview.html | 5 + .../valid/data.csv | 3 + .../valid/ro-crate-metadata.json | 146 ++++++++++++++++++ .../valid/ro-crate-preview.html | 8 + .../invalid/ro-crate-metadata.json | 32 ++++ .../valid/ro-crate-metadata.json | 32 ++++ .../invalid/ro-crate-metadata.json | 32 ++++ .../valid/ro-crate-metadata.json | 32 ++++ .../test_attached_rocrates_v1_3.py | 95 ++++++++++++ tests/ro_crates_v1_3.py | 28 ++++ 12 files changed, 470 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv new file mode 100644 index 000000000..53cf763c2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv @@ -0,0 +1,3 @@ +id,value +1,hello +2,world diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..8cab89e96 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json @@ -0,0 +1,54 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./x" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./x", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + }, + { + "@id": "ro-crate-preview.html" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + }, + { + "@id": "ro-crate-preview.html", + "@type": "File", + "name": "RO-Crate Preview", + "description": "HTML preview of the RO-Crate - this SHOULD NOT be in hasPart per RO-Crate 1.3.", + "encodingFormat": "text/html" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html new file mode 100644 index 000000000..be5c1b2fe --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html @@ -0,0 +1,5 @@ + + +RO-Crate Preview +

Counterexample: preview in hasPart

+ diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv new file mode 100644 index 000000000..53cf763c2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv @@ -0,0 +1,3 @@ +id,value +1,hello +2,world diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json new file mode 100644 index 000000000..3874cd5b9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json @@ -0,0 +1,146 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: preview not in hasPart", + "description": "This RO-Crate correctly omits ro-crate-preview.html from hasPart, following the new RO-Crate 1.3 requirement.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "mailto:contact@example.org" + }, + "identifier": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "mailto:contact@example.org", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@example.org", + "name": "Example Research Institute contact point" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file containing temperature readings.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/18" + } + ], + "contentSize": "42", + "contentLocation": { + "@id": "#bologna" + }, + "conformsTo": { + "@id": "#csv-profile" + }, + "dateCreated": "2025-06-15", + "dateModified": "2025-12-01", + "author": { + "@id": "https://orcid.org/0000-0001-2345-6789" + }, + "sdDatePublished": "2026-04-15" + }, + { + "@id": "#csv-profile", + "@type": "CreativeWork", + "name": "CSV Profile for RO-Crate 1.3", + "description": "A profile describing the expected structure and content of CSV files used in RO-Crates.", + "conformsTo": { + "@id": "https://www.iana.org/assignments/media-types/text/csv" + } + }, + { + "@id": "#bologna", + "@type": "Place", + "name": "Bologna, Italy", + "geo": { + "@id": "#bologna-geo" + } + }, + { + "@id": "#bologna-geo", + "@type": [ + "GeoCoordinates", + "Geometry" + ], + "name": "Geographical coordinates of Bologna", + "latitude": 44.4949, + "longitude": 11.3426, + "asWKT": "POINT(11.3426 44.4949)" + }, + { + "@id": "https://orcid.org/0000-0001-2345-6789", + "@type": "Person", + "name": "Dr. Jane Doe", + "affiliation": { + "@id": "https://ror.org/12345" + } + }, + { + "@id": "https://ror.org/12345", + "@type": "Organization", + "name": "Example University", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "mailto:contact@exampleuniversity.edu" + } + }, + { + "@id": "mailto:contact@exampleuniversity.edu", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@exampleuniversity.edu", + "name": "Example University contact point" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html new file mode 100644 index 000000000..99f1e3229 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html @@ -0,0 +1,8 @@ + + +RO-Crate Preview + +

Example: preview not in hasPart

+

This RO-Crate correctly omits ro-crate-preview.html from hasPart.

+ + diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..6c204ba75 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "invalid-IRI-root-dataset" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "invalid-IRI-root-dataset", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json new file mode 100644 index 000000000..4472466a8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://example.org/external-root-dataset" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/external-root-dataset", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..971661f22 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./root-dataset" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./root-dataset", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dbed5c3e1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py new file mode 100644 index 000000000..1d2a1df38 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py @@ -0,0 +1,95 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import AttachedROCrates +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__attached_crates__ = AttachedROCrates() + + +def test_preview_not_in_hasPart(): + """ + Test that the metadata document is valid when the preview file + is not included in the `hasPart` property of the Root Data Entity. + """ + do_entity_test( + __attached_crates__.valid_preview_not_in_hasPart, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_preview_not_in_hasPart_warning(): + """ + Test that a warning is triggered when the preview file + is not included in the `hasPart` property of the Root Data Entity. + """ + do_entity_test( + __attached_crates__.invalid_preview_not_in_hasPart, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Preview file descriptor should not be included in `hasPart`"], + expected_triggered_issues=["RO-Crate Website files SHOULD NOT be included in `hasPart`"], + ) + + +def test_root_with_IRI_identifier(): + do_entity_test( + __attached_crates__.valid_relative_root_entity_id, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_root_with_relative_identifier(): + do_entity_test( + __attached_crates__.valid_relative_root_entity_id, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_root_with_invalid_relative_identifier(): + do_entity_test( + __attached_crates__.invalid_relative_root_entity_id, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity identifier restriction"], + expected_triggered_issues=[ + "The Root Data Entity MUST be a `Dataset` (as per `schema.org`) and use an IRI or `./` as identifier" + ], + ) + + +def test_root_with_invalid_non_relative_identifier(): + do_entity_test( + __attached_crates__.invalid_non_relative_root_entity_id, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity identifier restriction"], + expected_triggered_issues=[ + "The Root Data Entity MUST be a `Dataset` (as per `schema.org`) and use an IRI or `./` as identifier" + ], + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index f7d9c77e3..36c0f1b71 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -111,6 +111,34 @@ def not_utf8(self) -> Path: return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "utf8" +class AttachedROCrates: + ATTACHED_ROCRATES_CRATES_PATH = BASE_PATH / "2_attached_rocrates" + + @property + def valid_preview_not_in_hasPart(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "attached-preview-not-in-hasPart" / "valid" + + @property + def invalid_preview_not_in_hasPart(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "attached-preview-not-in-hasPart" / "invalid" + + @property + def valid_non_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "non-relative-root-identifier" / "valid" + + @property + def invalid_non_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "non-relative-root-identifier" / "invalid" + + @property + def valid_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "relative-root-identifier" / "valid" + + @property + def invalid_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "relative-root-identifier" / "invalid" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From 72589027563518ec16220400f89c174f076d09ca Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 22 Sep 2026 15:26:27 +0200 Subject: [PATCH 087/271] rocrate-1.3: add detached rocrates tests --- .../basic-ro-crate-metadata.json | 37 +++++++ .../multi-ro-crate-metadata.json | 60 +++++++++++ .../profiled-ro-crate-metadata.json | 51 ++++++++++ .../invalid/ro-crate-metadata.json | 37 +++++++ .../valid/basic-ro-crate-metadata.json | 83 ++++++++++++++++ .../invalid/ro-crate-metadata.json | 33 +++++++ .../valid/prefix-ro-crate-metadata.json | 68 +++++++++++++ .../test_detached_rocrates_v1_3.py | 99 +++++++++++++++++++ tests/ro_crates_v1_3.py | 34 +++++++ 9 files changed, 502 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json new file mode 100644 index 000000000..4b2339105 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/basic" + } + }, + { + "@id": "https://example.org/ro-crate/detached/basic", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + } + ] + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json new file mode 100644 index 000000000..8cc75f87d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json @@ -0,0 +1,60 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/multi" + } + }, + { + "@id": "https://example.org/ro-crate/detached/multi", + "@type": "Dataset", + "name": "Detached RO-Crate (multi)", + "description": "Detached RO-Crate with multiple remote data entities.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + }, + { + "@id": "https://www.w3.org/2008/site/images/logo-w3c-mobile-lg" + }, + { + "@id": "https://example.org/datasets/sample" + } + ] + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file.", + "encodingFormat": "text/plain" + }, + { + "@id": "https://www.w3.org/2008/site/images/logo-w3c-mobile-lg", + "@type": "File", + "name": "W3C logo", + "description": "Remote image file.", + "encodingFormat": "image/png" + }, + { + "@id": "https://example.org/datasets/sample", + "@type": "Dataset", + "name": "Sample remote dataset", + "description": "Remote dataset referenced by the detached RO-Crate.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json new file mode 100644 index 000000000..5198cbd0f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/profiled" + } + }, + { + "@id": "https://example.org/ro-crate/detached/profiled", + "@type": "Dataset", + "name": "Detached RO-Crate (profiled)", + "description": "Detached RO-Crate declaring conformance to an additional profile.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "conformsTo": [ + { + "@id": "https://example.org/profiles/example-profile" + } + ], + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + } + ] + }, + { + "@id": "https://example.org/profiles/example-profile", + "@type": [ + "Profile", + "CreativeWork" + ], + "name": "Example profile", + "description": "Example RO-Crate profile contextual entity." + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..84d74e972 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/" + } + }, + { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + ] + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "File", + "name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json new file mode 100644 index 000000000..833682604 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json @@ -0,0 +1,83 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "basic-ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/" + } + }, + { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate with a non-standard metadata descriptor filename.", + "datePublished": "2024-05-17", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "cite-as": { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/" + }, + "url": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/", + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..06c130c8c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json @@ -0,0 +1,33 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Online-available RO-Crate with relative root @id (invalid)", + "description": "A detached RO-Crate whose Root Data Entity has a relative @id instead of an absolute URL.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [], + "publisher": {"@id": "https://ror.org/012345678"} + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json new file mode 100644 index 000000000..2ac3d523f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json @@ -0,0 +1,68 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + } + }, + { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "@type": "Dataset", + "name": "Detached RO-Crate with absolute root @id", + "description": "A detached RO-Crate whose Root Data Entity has an absolute URL as @id.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "url": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "mailto:contact@example.org" + } + }, + { + "@id": "mailto:contact@example.org", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@example.org", + "name": "Example Research Institute contact point" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py new file mode 100644 index 000000000..22a4b2b24 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import DetachedROCrates +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__detached_crates__ = DetachedROCrates() + + +def test_valid_local_descriptor_filename(): + """ + Test that a local descriptor filename is valid in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.valid_local_descriptor_filename, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "Web-based Data Entity: REQUIRED availability", + "Web-based Data Entity: RECOMMENDED availability", + "Web-based Data Entity: `contentSize` property", + "Web-based Data Entity: `contentUrl` availability", + ], + ) + + +def test_invalid_local_descriptor_filename(): + """ + Test that a local descriptor filename is invalid in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.invalid_local_descriptor_filename, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor naming convention"], + expected_triggered_issues=[ + "In a detached RO-Crate, " + "the metadata descriptor filename SHOULD " + "be named according to the convention " + "`{prefix}-ro-crate-metadata.json`" + ], + ) + + +def test_root_data_entity_identifier_when_online_available(): + """ + Test that when the RO-Crate is online available, + the Root Data Entity @id SHOULD be an absolute URL in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.valid_root_data_entity_identifier_when_online_available, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1", "ro-crate-1.3_38.1"], + ) + + +def test_invalid_root_data_entity_identifier_when_online_available(): + """ + Test that when the RO-Crate is online available, + the Root Data Entity @id SHOULD be an absolute URL in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.invalid_root_data_entity_identifier_when_online_available, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "ro-crate-1.3_42.1", + "ro-crate-1.3_42.2", + "ro-crate-1.3_42.3", + "ro-crate-1.3_43.1", + "ro-crate-1.3_44.1", + "ro-crate-1.3_44.2", + "ro-crate-1.3_45.1", + ], + expected_triggered_requirements=["Root Data Entity: RECOMMENDED identifier"], + expected_triggered_issues=["In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL"], + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 36c0f1b71..d679d8808 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -139,6 +139,40 @@ def invalid_relative_root_entity_id(self) -> Path: return self.ATTACHED_ROCRATES_CRATES_PATH / "relative-root-identifier" / "invalid" +class DetachedROCrates: + DETACHED_ROCRATES_CRATES_PATH = BASE_PATH / "3_detached_rocrates" + + __remote_sha__ = "8a3efd8b2b6a87f169ec6f69e893b3cf59538479" + + @property + def __remote_base_url__(self) -> str: + return f"https://bitbucket.org/simleo/ro-crates/raw/{self.__remote_sha__}" + + @property + def valid_local_descriptor_filename(self) -> Path: + return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "valid" + + @property + def invalid_local_descriptor_filename(self) -> Path: + return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "invalid" + + @property + def valid_root_data_entity_identifier_when_online_available(self) -> Path: + return self.DETACHED_ROCRATES_CRATES_PATH / "root-data-entity-identifier" / "online-available" / "valid" + + @property + def invalid_root_data_entity_identifier_when_online_available(self) -> Path: + return f"{self.__remote_base_url__}/online-available/invalid/basic-ro-crate-metadata.json" + + @property + def valid_web_data_entity(self) -> Path: + return f"{self.__remote_base_url__}/online-available/valid/basic-ro-crate-metadata.json" + + @property + def invalid_web_data_entity(self) -> Path: + return f"{self.__remote_base_url__}/online-available/invalid/basic-ro-crate-metadata.json" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From 06637c55cc9087d53763319d2b5ec139a3f97ce1 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 22 Sep 2026 15:59:27 +0200 Subject: [PATCH 088/271] rocrate-1.3: add metadata entities tests --- .../invalid/my-data-file.txt | 0 .../invalid/ro-crate-metadata.json | 38 +++++++++ .../valid/ro-crate-metadata.json | 79 +++++++++++++++++++ .../recommended_name/invalid/my-data-file.txt | 0 .../invalid/ro-crate-metadata.json | 41 ++++++++++ .../recommended_name/valid/my-data-file.txt | 0 .../valid/ro-crate-metadata.json | 79 +++++++++++++++++++ .../invalid/ro-crate-metadata.json | 32 ++++++++ .../valid/ro-crate-metadata.json | 79 +++++++++++++++++++ .../test_metadata_entities_v1_3.py | 78 ++++++++++++++++++ tests/ro_crates_v1_3.py | 20 +++++ 11 files changed, 446 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/my-data-file.txt create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/my-data-file.txt create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/my-data-file.txt create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/my-data-file.txt b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..76597ad63 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "my-data-file.txt", + "@type": "File", + "name": "My Data File", + "description": "A data file that is part of the RO-Crate." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/my-data-file.txt b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..eb470334d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "my-data-file.txt" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "my-data-file.txt", + "@type": "File", + "description": "A data file that is part of the RO-Crate." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/my-data-file.txt b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json new file mode 100644 index 000000000..e32a443b5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": { + "@id": "my-data-file.txt" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "my-data-file.txt", + "@type": "File", + "name": "My Data File", + "description": "A data file that is part of the RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": "0" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..dcec571d7 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "http://purl.org/dc/terms/LicenseDocument", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py new file mode 100644 index 000000000..29f2e44e2 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import MetadataEntities +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_entities__ = MetadataEntities() + + +def test_valid_recommended_schema_type(): + """ + Test that the metadata document includes at least one Schema.org type. + """ + do_entity_test( + __metadata_entities__.valid_recommended_schema_type, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_schema_type_warning(): + """ + Test that a warning is triggered when the metadata document + does not include at least one Schema.org type. + """ + do_entity_test( + __metadata_entities__.invalid_recommended_schema_type, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata Entity: RECOMMENDED properties"], + expected_triggered_issues=["RO-Crate Metadata Entity SHOULD include at least one Schema.org type"], + ) + + +def test_valid_recommended_entity_name(): + """ + Test that the metadata document includes a `name` property for at least one entity. + """ + do_entity_test( + __metadata_entities__.valid_recommended_name, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_entity_name_warning(): + """ + Test that a warning is triggered when the metadata document + does not include a `name` property for at least one entity. + """ + do_entity_test( + __metadata_entities__.invalid_recommended_name, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata Entity: RECOMMENDED properties"], + expected_triggered_issues=["Entities SHOULD have a human-readable name"], + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index d679d8808..5dd08d644 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -173,6 +173,26 @@ def invalid_web_data_entity(self) -> Path: return f"{self.__remote_base_url__}/online-available/invalid/basic-ro-crate-metadata.json" +class MetadataEntities: + METADATA_ENTITIES_CRATES_PATH = BASE_PATH / "5_metadata_entities" + + @property + def valid_recommended_schema_type(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_schema_type" / "valid" + + @property + def invalid_recommended_schema_type(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_schema_type" / "invalid" + + @property + def valid_recommended_name(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "valid" + + @property + def invalid_recommended_name(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "invalid" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From ba8327699ba935fe57eb3e0b915c603a232f48da Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 22 Sep 2026 16:13:21 +0200 Subject: [PATCH 089/271] rocrate-1.3: add metadata descriptor tests --- .../invalid/ro-crate-metadata.json | 32 ++++++++ .../valid/ro-crate-metadata.json | 79 ++++++++++++++++++ .../single_value/invalid/my-data-file.txt | 0 .../invalid/ro-crate-metadata.json | 37 +++++++++ .../single_value/valid/ro-crate-metadata.json | 79 ++++++++++++++++++ .../test_metadata_descriptor_v1_3.py | 82 +++++++++++++++++++ tests/ro_crates_v1_3.py | 20 +++++ 7 files changed, 329 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/my-data-file.txt create mode 100644 tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..8707b6e66 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://xxxw3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/my-data-file.txt b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..c75e3be7b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.3" + }, + { + "@id": "https://w3id.org/ro/crate/1.1" + } + ] + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py new file mode 100644 index 000000000..8829df7ea --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py @@ -0,0 +1,82 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import MetadataDescriptor +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_descriptor_crates__ = MetadataDescriptor() + + +def test_valid_single_value_conformsTo(): + """ + Test that the metadata descriptor is valid when the `conformsTo` property + includes a single value. + """ + do_entity_test( + __metadata_descriptor_crates__.valid_single_value_conformsTo, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_single_value_conformsTo(): + """ + Test that the metadata descriptor is invalid when the `conformsTo` property + includes multiple values. + """ + do_entity_test( + __metadata_descriptor_crates__.invalid_single_value_conformsTo, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata File Descriptor RECOMMENDED conformsTo"], + expected_triggered_issues=["The RO-Crate metadata file descriptor SHOULD have a single `conformsTo` value"], + ) + + +def test_valid_recommended_prefix_conformsTo(): + """ + Test that the metadata descriptor is valid when the `conformsTo` property + includes a value with the recommended prefix. + """ + do_entity_test( + __metadata_descriptor_crates__.valid_recommended_prefix_conformsTo, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_prefix_conformsTo(): + """ + Test that the metadata descriptor is invalid when the `conformsTo` property + includes a value with an incorrect prefix. + """ + do_entity_test( + __metadata_descriptor_crates__.invalid_recommended_prefix_conformsTo, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata File Descriptor RECOMMENDED conformsTo"], + expected_triggered_issues=[ + "The RO-Crate metadata file descriptor `conformsTo` URI SHOULD start with https://w3id.org/ro/crate/" + ], + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 5dd08d644..14df4f84d 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -193,6 +193,26 @@ def invalid_recommended_name(self) -> Path: return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "invalid" +class MetadataDescriptor: + METADATA_DESCRIPTOR_CRATES_PATH = BASE_PATH / "6_metadata_descriptor" + + @property + def valid_single_value_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "single_value" / "valid" + + @property + def invalid_single_value_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "single_value" / "invalid" + + @property + def valid_recommended_prefix_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "recommended_prefix" / "valid" + + @property + def invalid_recommended_prefix_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "recommended_prefix" / "invalid" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From fb90491f74c418115067a4b57513425120437ca3 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 22 Sep 2026 16:42:39 +0200 Subject: [PATCH 090/271] rocrate-1.3: add root data entity tests --- .../invalid/ro-crate-metadata.json | 55 ++ .../valid/ro-crate-metadata.json | 56 ++ .../invalid/citeas-ro-crate-metadata.json | 36 ++ .../valid/citeas-ro-crate-metadata.json | 83 +++ .../invalid/ro-crate-metadata.json | 74 +++ .../valid/ro-crate-metadata.json | 112 ++++ .../invalid_missing/ro-crate-metadata.json | 63 ++ .../invalid_wrong_type/ro-crate-metadata.json | 71 +++ .../valid/ro-crate-metadata.json | 73 +++ .../invalid/ro-crate-metadata.json | 68 +++ .../valid/ro-crate-metadata.json | 103 ++++ .../invalid_no_funder/ro-crate-metadata.json | 40 ++ .../ro-crate-metadata.json | 41 ++ .../ro-crate-metadata.json | 40 ++ .../valid/ro-crate-metadata.json | 103 ++++ .../invalid/ro-crate-metadata.json | 59 ++ .../valid/ro-crate-metadata.json | 103 ++++ .../invalid/ro-crate-metadata.json | 60 ++ .../valid/ro-crate-metadata.json | 103 ++++ .../invalid/ro-crate-metadata.json | 27 + .../valid/ro-crate-metadata.json | 79 +++ .../invalid/ro-crate-metadata.json | 26 + .../valid/ro-crate-metadata.json | 79 +++ .../invalid/ro-crate-metadata.json | 31 + .../valid/ro-crate-metadata.json | 32 + .../invalid/ro-crate-metadata.json | 31 + .../valid/ro-crate-metadata.json | 32 + .../invalid/ro-crate-metadata.json | 41 ++ .../valid/ro-crate-metadata.json | 42 ++ .../invalid/file1.txt | 0 .../invalid/ro-crate-metadata.json | 64 ++ .../invalid_dataset_not_in_haspart/file1.txt | 0 .../ro-crate-metadata.json | 44 ++ .../local.txt | 0 .../ro-crate-metadata.json | 44 ++ .../invalid_workflow_not_in_haspart/data.txt | 0 .../ro-crate-metadata.json | 43 ++ .../workflow.ga | 0 .../valid/file1.txt | 0 .../valid/file2.txt | 0 .../valid/ro-crate-metadata.json | 65 ++ .../test_metadata_rootDataEntity_v1_3.py | 562 ++++++++++++++++++ tests/ro_crates_v1_3.py | 181 ++++++ 43 files changed, 2766 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/file1.txt create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/file1.txt create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/local.txt create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/data.txt create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/workflow.ga create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file1.txt create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file2.txt create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..1b84b6bfc --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1x" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI.", + "url": "https://example.com/download/ro-crate.zip" + }, + { + "@id": "https://w3id.org/ro/crate/1.1x", + "@type": [ + "CreativeWork" + ], + "name": "RO-Crate 1.1x Profile", + "description": "An example of an additional `conformsTo` profile reference in the Root Data Entity metadata." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json new file mode 100644 index 000000000..c2e300e85 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json @@ -0,0 +1,56 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1x" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI.", + "url": "https://example.com/download/ro-crate.zip" + }, + { + "@id": "https://w3id.org/ro/crate/1.1x", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "RO-Crate 1.1x Profile", + "description": "An example of an additional `conformsTo` profile reference in the Root Data Entity metadata." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json new file mode 100644 index 000000000..f2e6868d8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json @@ -0,0 +1,36 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "url": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "distribution": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json new file mode 100644 index 000000000..a0f794606 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json @@ -0,0 +1,83 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "cite-as": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "url": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f8b7c6e70 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json @@ -0,0 +1,74 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: conformsTo missing despite profile in graph", + "description": "This RO-Crate has a Profile entity in the graph but Root lacks conformsTo", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://w3id.org/ro/crate/1.3/profile1", + "@type": "Profile", + "name": "Example Profile", + "url": "https://w3id.org/ro/crate/1.3/profile1" + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json new file mode 100644 index 000000000..860d0717f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: conformsTo present with profile reference", + "description": "This RO-Crate conforms to a profile and has conformsTo on Root", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3/profile1" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "https://w3id.org/ro/crate/1.3/profile1", + "@type": "Profile", + "name": "Example Profile", + "url": "https://w3id.org/ro/crate/1.3/profile1" + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json new file mode 100644 index 000000000..1b3520029 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root contactPoint missing — invalid", + "description": "RO-Crate whose Root Data Entity omits the `contactPoint` property entirely.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json new file mode 100644 index 000000000..dabe41800 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json @@ -0,0 +1,71 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root contactPoint wrong type — invalid", + "description": "RO-Crate whose Root Data Entity references a contactPoint that is NOT a ContactPoint contextual entity (it's a plain Thing).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#root-contact" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#root-contact", + "@type": "Thing", + "name": "Not actually a ContactPoint" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json new file mode 100644 index 000000000..cb1f46ea2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json @@ -0,0 +1,73 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root contactPoint present — valid", + "description": "RO-Crate whose Root Data Entity declares a contactPoint referencing a ContactPoint contextual entity.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#root-contact" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#root-contact", + "@type": "ContactPoint", + "name": "RO-Crate primary contact", + "email": "mailto:crate-contact@example.org", + "contactType": "customer support" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..6656979d9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json @@ -0,0 +1,68 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: datePublished with year only", + "description": "This RO-Crate has a datePublished property specified to year only (not day precision)", + "datePublished": "2024", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json new file mode 100644 index 000000000..9d707e224 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: datePublished with day precision", + "description": "This RO-Crate has a datePublished property specified to day precision (YYYY-MM-DD)", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json new file mode 100644 index 000000000..653437989 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — missing funder", + "description": "RO-Crate that describes a research project (Grant entity) but whose Root Data Entity has no `funder` property, triggering the conditional RECOMMENDED funder check.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "mentions": {"@id": "#grant-eu-2024"}, + "hasPart": [] + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#grant-eu-2024", + "@type": "Grant", + "name": "EU Horizon 2024 Grant", + "description": "A research grant referenced in the crate; the Root SHOULD declare the corresponding funder." + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json new file mode 100644 index 000000000..8f45a5d02 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — project org missing funder", + "description": "RO-Crate whose project Organization is referenced as funder from Root but does not itself reference an external funder.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "#project-org"}, + "hasPart": [] + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#project-org", + "@type": "Organization", + "name": "Example Research Project", + "description": "A project Organization that does not reference any external funder.", + "url": "https://example.org/projects/example-project" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json new file mode 100644 index 000000000..adac51fdd --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — funder is not Organization", + "description": "RO-Crate whose Root Data Entity references a funder that is a Person, not an Organization.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "#funder-person"}, + "hasPart": [] + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-person", + "@type": "Person", + "name": "Jane Doe", + "description": "An individual acting as funder — should be Organization instead." + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json new file mode 100644 index 000000000..ad04789a5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — valid", + "description": "RO-Crate whose Root Data Entity references both a project Organization and an external funder directly via `funder`.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00k4n6c32" + }, + { + "@id": "https://ror.org/00qj9e848" + } + ], + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "Example Research Project", + "description": "The research project associated with this RO-Crate.", + "url": "https://example.org/projects/example-project", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for Example Research Project", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..e25a7b019 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json @@ -0,0 +1,59 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "https://example.org/crate1" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/crate1", + "@type": "Dataset", + "name": "Test RO-Crate: identifier missing with absolute URI", + "description": "This RO-Crate has an absolute URI as @id but does NOT include an identifier property", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a554c1562 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: identifier present with absolute URI", + "description": "This RO-Crate has an absolute URI as @id and includes an identifier property", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..8f0e4e11f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json @@ -0,0 +1,60 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: identifier does NOT use PropertyValue", + "description": "This RO-Crate has identifier as a plain string instead of PropertyValue", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "identifier": "10.5281/zenodo.1234567", + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json new file mode 100644 index 000000000..ec6f37961 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: identifier uses PropertyValue approach", + "description": "This RO-Crate uses PropertyValue for identifier as per Science On Schema.org", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..72bab9215 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json @@ -0,0 +1,27 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root identifier resolution — invalid", + "description": "RO-Crate whose Root Data Entity has an identifier URL that does not resolve to RO-Crate content (returns HTML landing page).", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [], + "identifier": "https://doi.org/10.1234/unresolvable-rocrate" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json new file mode 100644 index 000000000..13449e141 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root identifier resolution — valid", + "description": "RO-Crate whose Root Data Entity has an identifier URL that resolves to RO-Crate content via Signposting.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#identifier-pv" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "DOI", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "https://doi.org/10.1234/resolvable-rocrate" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..cf13d2561 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json @@ -0,0 +1,26 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Publisher missing — invalid", + "description": "RO-Crate whose Root Data Entity does not declare a publisher.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json new file mode 100644 index 000000000..1aabf41b4 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Publisher present — valid", + "description": "RO-Crate whose Root Data Entity declares a publisher Organization.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ee02699a1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dbed5c3e1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ee02699a1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dbed5c3e1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..4f8391888 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0da8d7b11 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json @@ -0,0 +1,42 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI.", + "url": "https://example.com/download/ro-crate.zip" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/file1.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..5ac9e2a94 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json @@ -0,0 +1,64 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: hasPart does NOT reference all Data Entities", + "description": "This RO-Crate has hasPart that is missing reference to file2.txt", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "hasPart": [ + { "@id": "file1.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "file1.txt", + "@type": "File", + "name": "File 1" + }, + { + "@id": "file2.txt", + "@type": "File", + "name": "File 2" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/file1.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json new file mode 100644 index 000000000..67f8bdca2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json @@ -0,0 +1,44 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: Sub-Dataset not in hasPart", + "description": "This RO-Crate has a sub-Dataset (Directory Data Entity) that is missing from hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { "@id": "file1.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "file1.txt", + "@type": "File", + "name": "File 1" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory Data Entity", + "description": "A sub-Dataset Data Entity that is not referenced in hasPart." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/local.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/local.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json new file mode 100644 index 000000000..2bb7ae6ca --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json @@ -0,0 +1,44 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: Web Data Entity not in hasPart", + "description": "This RO-Crate has a web-based File entity (absolute URL @id) that is missing from hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { "@id": "local.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "local.txt", + "@type": "File", + "name": "Local file" + }, + { + "@id": "https://example.org/data/remote.tsv", + "@type": "File", + "name": "Remote web-based data file", + "encodingFormat": "text/tab-separated-values" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/data.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/data.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json new file mode 100644 index 000000000..1cb502a20 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json @@ -0,0 +1,43 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: Workflow not in hasPart", + "description": "This RO-Crate has a workflow File entity (File/SoftwareSourceCode/ComputationalWorkflow) that is missing from hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { "@id": "data.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "data.txt", + "@type": "File", + "name": "Data File" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/workflow.ga b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file1.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file2.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json new file mode 100644 index 000000000..11f1b9203 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json @@ -0,0 +1,65 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: hasPart references all Data Entities", + "description": "This RO-Crate has hasPart that references all Data Entities in the graph", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "hasPart": [ + { "@id": "file1.txt" }, + { "@id": "file2.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "file1.txt", + "@type": "File", + "name": "File 1" + }, + { + "@id": "file2.txt", + "@type": "File", + "name": "File 2" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py new file mode 100644 index 000000000..de4296703 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py @@ -0,0 +1,562 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates_v1_3 import RootDataEntity +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_root_data_entity_crates__ = RootDataEntity() + + +def test_valid_required_datePublished(): + """ + Test that the Root Data Entity is valid when it includes a `datePublished` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_required_datePublished, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_required_datePublished(): + """ + Test that the Root Data Entity is invalid when it does not include a `datePublished` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_required_datePublished, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Root Data Entity REQUIRED properties"], + expected_triggered_issues=[ + "The Root Data Entity MUST have a `datePublished` " + "property (as specified by schema.org) with a valid ISO 8601 date" + ], + ) + + +def test_valid_required_downloadable_citeAs(monkeypatch): + """ + Test that the Root Data Entity is valid when it includes a `cite-as` property + that references a downloadable item (mocked as application/zip via HEAD). + """ + + class _DownloadableResponse: + status_code = 200 + headers = {"Content-Type": "application/zip"} + links = {} + + def raise_for_status(self): + pass + + def _fake_head(url, *args, **kwargs): + logger.debug("Mock HEAD request to %s with args: %s, kwargs: %s", url, args, kwargs) + return _DownloadableResponse() + + monkeypatch.setattr(HttpRequester(), "head", _fake_head) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_required_downloadable_citeAs, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + ) + + +def test_invalid_required_downloadable_citeAs(monkeypatch): + """ + Test that the Root Data Entity is invalid when it includes a `cite-as` property + that does not reference a downloadable item (mocked as text/html via HEAD). + """ + + class _HtmlResponse: + status_code = 200 + headers = {"Content-Type": "text/html; charset=utf-8"} + links = {} + + def raise_for_status(self): + pass + + def _fake_head(url, *args, **kwargs): + logger.debug("Mock HEAD request to %s with args: %s, kwargs: %s", url, args, kwargs) + return _HtmlResponse() + + monkeypatch.setattr(HttpRequester(), "head", _fake_head) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_required_downloadable_citeAs, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + expected_triggered_requirements=["Root Data Entity: `cite-as` downloadability"], + expected_triggered_issues=["MUST ultimately provide the RO-Crate as a downloadable item"], + ) + + +def test_valid_recommended_citeAs_for_resolvable_id(): + """ + Test that the Root Data Entity is valid when it has a resolvable identifier + and includes a `cite-as` property that references the Root Data Entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_citeAs_for_resolvable_id, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + # The test crate focuses on cite-as, not identifier format; + # skip the identifier-presence and PropertyValue-approach checks. + skip_checks=["ro-crate-1.3_52.1", "ro-crate-1.3_53.1"], + ) + + +def test_invalid_recommended_citeAs_for_resolvable_id(): + """ + Test that the Root Data Entity is invalid when it has a resolvable identifier + and does not include a `cite-as` property that references the Root Data Entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_citeAs_for_resolvable_id, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: use cite-as for resolvable identifiers"], + expected_triggered_issues=[ + "If the Root Data Entity has a resolvable identifier, " + "it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity." + ], + ) + + +def test_valid_additional_conformsTo_reference(): + """ + Test that the Root Data Entity is valid when it includes + an additional `conformsTo` property that references a Profile entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_additional_conformsTo_reference, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_additional_conformsTo_reference(): + """ + Test that the Root Data Entity is invalid when it includes + an additional `conformsTo` property that does not reference a Profile entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_additional_conformsTo_reference, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Root Data Entity: optional `conformsTo` property value restriction"], + expected_triggered_issues=[ + "If the Root Data Entity includes a `conformsTo` property, its values MUST reference Profile entities." + ], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity identifier — persistent identifier resolution (RECOMMENDED) +# --------------------------------------------------------------------------- + + +class _ZipResponse: + status_code = 200 + headers = {"Content-Type": "application/zip"} + links = {} + + def raise_for_status(self): + pass + + +class _HtmlResponse: + status_code = 200 + headers = {"Content-Type": "text/html; charset=utf-8"} + links = {} + + def raise_for_status(self): + pass + + +def test_valid_recommended_identifier_resolution(monkeypatch): + """ + Root Data Entity whose identifier URL resolves to a downloadable resource + (mocked as application/zip) passes the RECOMMENDED identifier resolution check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_identifier_resolution, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + # The identifier uses a plain URL string rather than a PropertyValue entity; + # that format check is not the focus of this test (resolution is). + skip_checks=["ro-crate-1.3_53.0"], + ) + + +def test_invalid_recommended_identifier_resolution(monkeypatch): + """ + Root Data Entity whose identifier URL returns text/html (landing page) fails + the RECOMMENDED identifier resolution check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_identifier_resolution, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: persistent identifier resolution"], + expected_triggered_issues=["SHOULD resolve to the RO-Crate Metadata Document or an archive"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: publisher SHOULD be present (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_publisher(): + """ + Root Data Entity with a publisher Organization passes the RECOMMENDED + publisher check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_publisher, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_publisher(): + """ + Root Data Entity without a publisher fails the RECOMMENDED publisher check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_publisher, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: recommended publisher"], + expected_triggered_issues=["SHOULD have a `publisher` property"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: funder SHOULD be present (RECOMMENDED — K1/K2/K3) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_funding(): + """ + Root Data Entity with a funder Organization (which itself references an + external funder) passes all RECOMMENDED funding checks. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_funding, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_funding_no_funder(): + """ + Root Data Entity with no `funder` property fails the RECOMMENDED + direct-funder check (K3). + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_funding_no_funder, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: recommended funder"], + expected_triggered_issues=["SHOULD reference funders directly via the `funder` property"], + ) + + +def test_invalid_recommended_funding_non_org_funder(): + """ + Root Data Entity whose `funder` references a Person (not an Organization) + fails the RECOMMENDED funder-type check (K1). + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_funding_non_org_funder, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Funder entity: recommended Organization type"], + expected_triggered_issues=["SHOULD be of type `Organization`"], + ) + + +def test_invalid_recommended_funding_no_project_funder(): + """ + Root Data Entity whose local project Organization (`funder`) does not itself + reference an external funder fails the RECOMMENDED project-org funder check (K2). + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_funding_no_project_funder, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Project Organization: recommended funder reference"], + expected_triggered_issues=["SHOULD itself reference an external `funder`"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: datePublished SHOULD specify at least day precision +# --------------------------------------------------------------------------- + + +def test_valid_recommended_datePublished_day_precision(): + """ + Root Data Entity with datePublished in YYYY-MM-DD format passes the + RECOMMENDED day precision check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_datePublished_day_precision, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1"], + ) + + +def test_invalid_recommended_datePublished_day_precision(): + """ + Root Data Entity with datePublished as just year (YYYY) fails the + RECOMMENDED day precision check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_datePublished_day_precision, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: datePublished SHOULD specify at least day precision"], + expected_triggered_issues=["SHOULD specify datePublished to at least the precision of a day"], + skip_checks=["ro-crate-1.3_35.1"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: hasPart MUST reference all Data Entities +# --------------------------------------------------------------------------- + + +def test_valid_required_hasPart_all_data_entities(): + """ + Root Data Entity that references all Data Entities via hasPart (directly + or indirectly) passes the REQUIRED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_required_hasPart_all_data_entities, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_47.1"], + ) + + +def test_invalid_required_hasPart_all_data_entities(): + """ + Root Data Entity that does NOT reference all Data Entities via hasPart + fails the REQUIRED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_required_hasPart_all_data_entities, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + skip_checks=["ro-crate-1.3_47.1"], + ) + + +def test_invalid_hasPart_workflow_not_in_haspart(): + """ + A Workflow File entity (typed as File/SoftwareSourceCode/ComputationalWorkflow) + that is not referenced via hasPart triggers the REQUIRED hasPart check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_hasPart_workflow_not_in_haspart, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + ) + + +def test_invalid_hasPart_web_entity_not_in_haspart(): + """ + A Web Data Entity (absolute URL @id) that is not referenced via hasPart + triggers the REQUIRED hasPart check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_hasPart_web_entity_not_in_haspart, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + ) + + +def test_invalid_hasPart_dataset_not_in_haspart(): + """ + A sub-Dataset Directory Data Entity that is not referenced via hasPart + triggers the REQUIRED hasPart check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_hasPart_dataset_not_in_haspart, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: identifier SHOULD be present if PID exists (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_identifier_if_pid(): + """ + Root Data Entity with absolute URI @id and identifier property passes + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_identifier_if_pid, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "ro-crate-1.3_35.1", + "ro-crate-1.3_38.1", + "ro-crate-1.3_42.1", + "ro-crate-1.3_42.2", + "ro-crate-1.3_42.3", + "ro-crate-1.3_43.1", + "ro-crate-1.3_44.1", + "ro-crate-1.3_44.2", + "ro-crate-1.3_45.1", + "Root Data Entity: use cite-as for resolvable identifiers", + "Root Data Entity: persistent identifier resolution", + "Root Data Entity: identifier SHOULD be present if PID exists", + ], + skip_availability_check=True, + ) + + +def test_invalid_recommended_identifier_if_pid(): + """ + Root Data Entity with absolute URI @id but no identifier property fails + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_identifier_if_pid, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: identifier SHOULD be present if PID exists"], + expected_triggered_issues=["SHOULD have an `identifier` property if it has a persistent identifier"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: identifier SHOULD use PropertyValue approach (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_identifier_propertyvalue(): + """ + Root Data Entity that uses PropertyValue for identifier passes the + RECOMMENDED check per Science On Schema.org. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_identifier_propertyvalue, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1"], + ) + + +def test_invalid_recommended_identifier_propertyvalue(): + """ + Root Data Entity that uses plain string for identifier fails the + RECOMMENDED PropertyValue approach check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_identifier_propertyvalue, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: identifier SHOULD use PropertyValue approach"], + expected_triggered_issues=["SHOULD use PropertyValue entities for identifiers"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: conformsTo SHOULD be present if profiles exist (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_conformsto_if_profiles(): + """ + Root Data Entity that has a Profile entity in the graph and includes + conformsTo on the Root passes the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_conformsto_if_profiles, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1"], + ) + + +def test_invalid_recommended_conformsto_if_profiles(): + """ + Root Data Entity that has a Profile entity in the graph but does NOT + include conformsTo on the Root fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_conformsto_if_profiles, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: conformsTo SHOULD be present if profiles exist"], + expected_triggered_issues=["SHOULD have a `conformsTo` property if the RO-Crate conforms to profiles"], + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 14df4f84d..2d4bf2383 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -213,6 +213,187 @@ def invalid_recommended_prefix_conformsTo(self) -> Path: return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "recommended_prefix" / "invalid" +class RootDataEntity: + ROOT_DATA_ENTITY_CRATES_PATH = BASE_PATH / "7_root_data_entity" + + @property + def valid_required_datePublished(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_datePublished" / "valid" + + @property + def invalid_required_datePublished(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_datePublished" / "invalid" + + @property + def valid_required_downloadable_citeAs(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_downloadable_citeas" / "valid" + + @property + def invalid_required_downloadable_citeAs(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_downloadable_citeas" / "invalid" + + @property + def valid_recommended_citeAs_for_resolvable_id(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "valid" + + @property + def invalid_recommended_citeAs_for_resolvable_id(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "invalid" + + @property + def valid_additional_conformsTo_reference(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "additional_conformsTo" / "valid" + + @property + def invalid_additional_conformsTo_reference(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "additional_conformsTo" / "invalid" + + @property + def missing_root(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root" / "invalid" + + @property + def invalid_root_type(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_root_type" / "invalid" + + @property + def invalid_root_value(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_root_value" / "invalid" + + @property + def recommended_root_value(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_root_value" / "valid" + + @property + def invalid_root_date(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_root_date" / "invalid" + + @property + def invalid_recommended_root_date(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_recommended_root_date" / "invalid" + + @property + def missing_root_name(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_name" / "invalid" + + @property + def missing_root_description(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_description" / "invalid" + + @property + def missing_root_license(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_license" / "invalid" + + @property + def missing_root_license_name(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_license_name" / "invalid" + + @property + def missing_root_license_description(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_license_description" / "invalid" + + @property + def valid_referenced_generic_data_entities(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "valid_referenced_generic_data_entities" / "valid" + + @property + def valid_recommended_identifier_resolution(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_resolution" / "valid" + + @property + def invalid_recommended_identifier_resolution(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_resolution" / "invalid" + + @property + def valid_recommended_publisher(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_publisher" / "valid" + + @property + def invalid_recommended_publisher(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_publisher" / "invalid" + + @property + def valid_recommended_funding(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "valid" + + @property + def invalid_recommended_funding_no_funder(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "invalid_no_funder" + + @property + def invalid_recommended_funding_non_org_funder(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "invalid_non_org_funder" + + @property + def invalid_recommended_funding_no_project_funder(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "invalid_no_project_funder" + + # R1: datePublished day precision (SHOULD) + @property + def valid_recommended_datePublished_day_precision(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_datepublished_day_precision" / "valid" + + @property + def invalid_recommended_datePublished_day_precision(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_datepublished_day_precision" / "invalid" + + # R2: hasPart MUST reference all Data Entities + @property + def valid_required_hasPart_all_data_entities(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "valid" + + @property + def invalid_required_hasPart_all_data_entities(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid" + + @property + def invalid_hasPart_workflow_not_in_haspart(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid_workflow_not_in_haspart" + ) + + @property + def invalid_hasPart_web_entity_not_in_haspart(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "required_haspart_all_data_entities" + / "invalid_web_entity_not_in_haspart" + ) + + @property + def invalid_hasPart_dataset_not_in_haspart(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid_dataset_not_in_haspart" + ) + + # R3: identifier SHOULD be present if PID exists (SHOULD) + @property + def valid_recommended_identifier_if_pid(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_if_pid" / "valid" + + @property + def invalid_recommended_identifier_if_pid(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_if_pid" / "invalid" + + # R4: identifier SHOULD use PropertyValue approach (SHOULD) + @property + def valid_recommended_identifier_propertyvalue(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_propertyvalue" / "valid" + + @property + def invalid_recommended_identifier_propertyvalue(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_propertyvalue" / "invalid" + + # R5: conformsTo SHOULD be present if profiles exist (SHOULD) + @property + def valid_recommended_conformsto_if_profiles(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_conformsto_if_profiles" / "valid" + + @property + def invalid_recommended_conformsto_if_profiles(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_conformsto_if_profiles" / "invalid" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From 401b8d64b39182983f8ed446b8789fa4dc9e2f88 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 22 Sep 2026 18:00:49 +0200 Subject: [PATCH 091/271] =?UTF-8?q?perf(validation):=20=E2=9A=A1=EF=B8=8F?= =?UTF-8?q?=20reuse=20selected=20profiles=20for=20statistics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/statistics.py | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index 281b567cd..7900e0c27 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -68,7 +68,7 @@ def __init__( settings = ValidationSettings.parse(settings) self._settings = settings self._context = context - self._stats = self.__initialise__(settings) if not skip_initialization else {} + self._stats = self.__initialise__(settings, context=context) if not skip_initialization else {} self._result: ValidationResult | None = None self._listeners: list[ValidationStatisticsListener] = [] @@ -300,18 +300,29 @@ def __collect_requirement_checks__( return requirement_checks_count @classmethod - def __initialise__(cls, validation_settings: ValidationSettings): + def __initialise__( + cls, + validation_settings: ValidationSettings, + context: ValidationContext | None = None, + ): # pylint: disable=too-many-locals """ Compute the statistics of the profile """ # extract the validation settings severity_validation = validation_settings.requirement_severity - profiles: list[Profile] = Profile.load_profiles( - validation_settings.profiles_path, - extra_profiles_path=validation_settings.extra_profiles_path, - severity=cast("Severity", severity_validation), - allow_requirement_check_override=validation_settings.allow_requirement_check_override, - ) + profiles: list[Profile] + if context is not None: + # Reuse the exact profile instances selected by the validation + # context. They already carry the lazily prepared requirements and + # shape registries used by the run. + profiles = context.profiles + else: + profiles = Profile.load_profiles( + validation_settings.profiles_path, + extra_profiles_path=validation_settings.extra_profiles_path, + severity=cast("Severity", severity_validation), + allow_requirement_check_override=validation_settings.allow_requirement_check_override, + ) profile: Profile = cast("Profile", Profile.find_in_list(profiles, validation_settings.profile_identifier)) target_profile_identifier = profile.identifier # initialize the profiles list From 83ab1564510d5fe5971a8e25f5dc769e3a92c409 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 23 Sep 2026 10:00:19 +0200 Subject: [PATCH 092/271] rocrate-1.3: add data entities tests --- .../invalid/data/research-data.csv | 0 .../invalid/ro-crate-metadata.json | 80 +++ .../valid/data/open-data.csv | 0 .../valid/ro-crate-metadata.json | 86 +++ .../invalid/ro-crate-metadata.json | 55 ++ .../valid/ro-crate-metadata.json | 98 ++++ .../invalid/ro-crate-metadata.json | 64 +++ .../valid/ro-crate-metadata.json | 98 ++++ .../invalid/basic-ro-crate-metadata.json | 37 ++ .../valid/basic-ro-crate-metadata.json | 37 ++ .../invalid/ro-crate-metadata.json | 46 ++ .../valid/my-test-file-2.txt | 0 .../valid/my-test-file.txt | 0 .../valid/ro-crate-metadata.json | 59 ++ .../invalid/ro-crate-metadata.json | 47 ++ .../valid/ro-crate-metadata.json | 88 +++ .../recommended_conformsto/invalid/data.csv | 0 .../invalid/ro-crate-metadata.json | 63 +++ .../recommended_conformsto/valid/data.csv | 0 .../valid/ro-crate-metadata.json | 98 ++++ .../recommended_contentSize/invalid/data.csv | 0 .../invalid/ro-crate-metadata.json | 56 ++ .../recommended_contentSize/valid/data.csv | 0 .../valid/ro-crate-metadata.json | 89 +++ .../invalid/data.csv | 2 + .../invalid/ro-crate-metadata.json | 78 +++ .../valid/data.csv | 2 + .../valid/ro-crate-metadata.json | 102 ++++ .../invalid/my-test-data.csv | 0 .../invalid/ro-crate-metadata.json | 41 ++ .../recommended_properties/valid/data.csv | 2 + .../valid/ro-crate-metadata.json | 102 ++++ .../invalid/ro-crate-metadata.json | 57 ++ .../valid/ro-crate-metadata.json | 90 +++ .../invalid/ro-crate-metadata.json | 34 ++ .../valid/ro-crate-metadata.json | 89 +++ .../invalid/ro-crate-metadata.json | 32 ++ .../valid/ro-crate-metadata.json | 97 ++++ .../invalid/ro-crate-metadata.json | 50 ++ .../valid/ro-crate-metadata.json | 91 +++ .../invalid/ro-crate-metadata.json | 33 ++ .../valid/ro-crate-metadata.json | 90 +++ .../test_metadata_dataEntities_v1_3.py | 524 ++++++++++++++++++ tests/ro_crates_v1_3.py | 126 +++++ 44 files changed, 2743 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/data/research-data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/data/open-data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file-2.txt create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file.txt create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/my-test-data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/data/research-data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/data/research-data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..6e0292d9c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json @@ -0,0 +1,80 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate with redundant license on Data Entity", + "description": "The Data Entity repeats the same license as the Root.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "data/research-data.csv" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "cite-as": { + "@id": "./" + }, + "url": "./", + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "data/research-data.csv", + "@type": "File", + "name": "research-data.csv", + "description": "Research data under the same license as the Root.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + } + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/data/open-data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/data/open-data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json new file mode 100644 index 000000000..d63b769e4 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json @@ -0,0 +1,86 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate with divergent licenses", + "description": "The Root Data Entity has CC-BY-4.0; a Data Entity has CC0.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "data/open-data.csv" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "cite-as": { + "@id": "./" + }, + "url": "./", + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "data/open-data.csv", + "@type": "File", + "name": "open-data.csv", + "description": "Open data file under CC0.", + "encodingFormat": "text/csv", + "contentSize": "1024", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "CreativeWork", + "name": "CC0 1.0 Universal", + "description": "Public domain dedication." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..c0827e2d1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset hasPart — invalid", + "description": "RO-Crate where the local Dataset sub-entity does NOT declare hasPart.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "identifier": {"@id": "#uuid"}, + "hasPart": [{"@id": "subdir/"}] + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory without hasPart", + "description": "A local Dataset sub-entity that omits hasPart, violating the RECOMMENDED convention." + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json new file mode 100644 index 000000000..482d981f5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset hasPart — valid", + "description": "RO-Crate where the local Dataset sub-entity declares hasPart listing its contents.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + }, + "hasPart": { + "@id": "subdir/" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory with hasPart", + "description": "A local Dataset sub-entity that correctly lists its contents via hasPart.", + "hasPart": { + "@id": "subdir/data.csv" + } + }, + { + "@id": "subdir/data.csv", + "@type": "File", + "name": "Data file", + "description": "A data file listed via hasPart.", + "encodingFormat": "text/csv", + "contentSize": "1024" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..caae9e514 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json @@ -0,0 +1,64 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset trailing slash — invalid", + "description": "RO-Crate where the local Dataset sub-entity @id does NOT end with '/'.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "identifier": {"@id": "#uuid"}, + "hasPart": [{"@id": "subdir"}] + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir", + "@type": "Dataset", + "name": "Sub-directory (missing trailing slash)", + "description": "A local Dataset sub-entity whose @id does NOT end with '/'. This violates the RECOMMENDED convention.", + "hasPart": [{"@id": "subdir/data.csv"}] + }, + { + "@id": "subdir/data.csv", + "@type": "File", + "name": "Data file", + "description": "A data file inside the sub-directory.", + "encodingFormat": "text/csv", + "contentSize": "1024" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json new file mode 100644 index 000000000..8db8d4808 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset trailing slash — valid", + "description": "RO-Crate where the local Dataset sub-entity @id ends with '/' as required.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + }, + "hasPart": { + "@id": "subdir/" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory", + "description": "A local Dataset sub-entity whose @id correctly ends with '/'.", + "hasPart": { + "@id": "subdir/data.csv" + } + }, + { + "@id": "subdir/data.csv", + "@type": "File", + "name": "Data file", + "description": "A data file inside the sub-directory.", + "encodingFormat": "text/csv", + "contentSize": "1024" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json new file mode 100644 index 000000000..a94448efb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/basic" + } + }, + { + "@id": "https://example.org/ro-crate/detached/basic", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "./my-test-file.txt" + } + ] + }, + { + "@id": "./my-test-file.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json new file mode 100644 index 000000000..4b2339105 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/basic" + } + }, + { + "@id": "https://example.org/ro-crate/detached/basic", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + } + ] + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..0e16314aa --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json @@ -0,0 +1,46 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "file://./my-test-file.txt" + } + ] + }, + { + "@id": "file://./my-test-file.txt", + "@type": "File", + "name": "My Test File", + "description": "A test file for RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": 1024, + "dateCreated": "2024-01-01T12:00:00Z", + "dateModified": "2024-01-02T12:00:00Z" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file-2.txt b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file-2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file.txt b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json new file mode 100644 index 000000000..81c187a35 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json @@ -0,0 +1,59 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "./my-test-file.txt" + }, + { + "@id": "my-test-file-2.txt" + } + ] + }, + { + "@id": "./my-test-file.txt", + "@type": "File", + "name": "My Test File", + "description": "A test file for RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": 1024, + "dateCreated": "2024-01-01T12:00:00Z", + "dateModified": "2024-01-02T12:00:00Z" + }, + { + "@id": "my-test-file-2.txt", + "@type": "File", + "name": "My Test File 2", + "description": "Another test file for RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": 2048, + "dateCreated": "2024-01-01T12:00:00Z", + "dateModified": "2024-01-02T12:00:00Z" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..5a0d139e0 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json @@ -0,0 +1,47 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate missing file without localPath", + "description": "A local file is referenced but not present in the payload and has no localPath.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [{"@id": "data/missing-file.csv"}, {"@id": "#output-file.csv"}] + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "data/missing-file.csv", + "@type": "File", + "name": "missing-file.csv", + "description": "A CSV file that is not included in the payload and has no localPath.", + "encodingFormat": "text/csv" + }, + { + "@id": "#output-file.csv", + "@type": "File", + "name": "output-file.csv", + "description": "A deliberately absent file without localPath.", + "encodingFormat": "text/csv" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json new file mode 100644 index 000000000..45a22932c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json @@ -0,0 +1,88 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate with localPath for missing file", + "description": "A local file is referenced but not present in the payload; localPath is provided.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": [ + { + "@id": "data/missing-file.csv" + }, + { + "@id": "#output-file.csv" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data/missing-file.csv", + "@type": "File", + "name": "missing-file.csv", + "description": "A CSV file that is not included in the payload but has localPath.", + "encodingFormat": "text/csv", + "contentSize": "1024", + "localPath": "/mnt/archive/data/missing-file.csv" + }, + { + "@id": "#output-file.csv", + "@type": "File", + "name": "output-file.csv", + "description": "A deliberately absent file with localPath.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "localPath": "/tmp/output-file.csv" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..236c066f6 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File with invalid conformsTo — invalid", + "description": "This RO-Crate has a File Data Entity with conformsTo referencing a Person (not a Profile or CreativeWork).", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "hasPart": [{"@id": "data.csv"}], + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A CSV file with an invalid conformsTo reference.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "conformsTo": {"@id": "https://orcid.org/0000-0001-2345-6789"} + }, + { + "@id": "https://orcid.org/0000-0001-2345-6789", + "@type": "Person", + "name": "Dr. Jane Doe" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json new file mode 100644 index 000000000..e980051d1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File with conformsTo — valid", + "description": "This RO-Crate has a File Data Entity with conformsTo referencing a CreativeWork profile.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "data.csv" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A CSV file conforming to a format profile.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "conformsTo": { + "@id": "#csv-profile" + } + }, + { + "@id": "#csv-profile", + "@type": "CreativeWork", + "name": "CSV Profile for RO-Crate 1.3", + "description": "A profile describing the expected structure of CSV files used in RO-Crates." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..2e1e0f477 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json @@ -0,0 +1,56 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File without contentSize — invalid", + "description": "This RO-Crate has a File Data Entity missing the recommended contentSize property.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "hasPart": [{"@id": "data.csv"}], + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file without contentSize.", + "encodingFormat": "text/csv" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json new file mode 100644 index 000000000..04ef97578 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json @@ -0,0 +1,89 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File with contentSize — valid", + "description": "This RO-Crate has a File Data Entity with the recommended contentSize property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "data.csv" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file with contentSize.", + "encodingFormat": "text/csv", + "contentSize": "2048" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f769f2cfe --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json @@ -0,0 +1,78 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "contentSize": "42", + "contentLocation": { + "@id": "#location" + } + }, + { + "@id": "#location", + "@type": "Place", + "name": "Example location" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..6ad94da35 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42", + "contentLocation": { + "@id": "#location" + } + }, + { + "@id": "#location", + "@type": "Place", + "name": "Example location" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/my-test-data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/my-test-data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a3b86b2b1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "my-test-data.csv" + } + ] + }, + { + "@id": "my-test-data.csv", + "@type": "File", + "encodingFormat": "text/csv" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json new file mode 100644 index 000000000..6ad94da35 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42", + "contentLocation": { + "@id": "#location" + } + }, + { + "@id": "#location", + "@type": "Place", + "name": "Example location" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f2cd6528e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web-based File without sdDatePublished — invalid", + "description": "This RO-Crate has a web-based File Data Entity missing the recommended sdDatePublished property.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "hasPart": [{"@id": "https://example.com/data.csv"}], + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/data.csv", + "@type": "File", + "name": "Remote data file", + "description": "A web-based CSV file without sdDatePublished.", + "encodingFormat": "text/csv", + "contentSize": "512" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json new file mode 100644 index 000000000..caa5d7a1c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json @@ -0,0 +1,90 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web-based File with sdDatePublished — valid", + "description": "This RO-Crate has a web-based File Data Entity with the recommended sdDatePublished property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "https://example.com/data.csv" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/data.csv", + "@type": "File", + "name": "Remote data file", + "description": "A web-based CSV file with sdDatePublished.", + "encodingFormat": "text/csv", + "contentSize": "512", + "sdDatePublished": "2024-06-15" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ab7bc3de4 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json @@ -0,0 +1,34 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset distribution — invalid", + "description": "RO-Crate whose Root Data Entity declares a distribution pointing to a DataDownload that is not downloadable.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [], + "distribution": {"@id": "https://example.com/unavailable-archive.zip"} + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/unavailable-archive.zip", + "@type": "DataDownload", + "name": "Unavailable archive", + "description": "A DataDownload whose URL is not downloadable (returns HTML or is unreachable).", + "encodingFormat": "application/zip" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a4735803a --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json @@ -0,0 +1,89 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset distribution — valid", + "description": "RO-Crate whose Root Data Entity declares a distribution pointing to a downloadable DataDownload archive.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "distribution": { + "@id": "https://example.com/rocrate.zip" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/rocrate.zip", + "@type": "DataDownload", + "name": "RO-Crate archive", + "description": "A ZIP archive of the RO-Crate, directly downloadable.", + "encodingFormat": "application/zip" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..1ddf0f579 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web directory without distribution — invalid", + "description": "RO-Crate containing a web-based Directory Data Entity that does not declare a distribution.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [{"@id": "https://example.com/dataset-dir/"}] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/dataset-dir/", + "@type": "Dataset", + "name": "Web dataset directory (no distribution)", + "description": "A web-based directory that does not declare a distribution property." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json new file mode 100644 index 000000000..55f65122d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json @@ -0,0 +1,97 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web directory with distribution — valid", + "description": "RO-Crate containing a web-based Directory Data Entity that declares a distribution.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "https://example.com/dataset-dir/" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/dataset-dir/", + "@type": "Dataset", + "name": "Web dataset directory", + "description": "A web-based directory with a distribution pointing to a downloadable archive.", + "distribution": { + "@id": "https://example.com/dataset-dir.zip" + } + }, + { + "@id": "https://example.com/dataset-dir.zip", + "@type": "DataDownload", + "name": "Dataset archive", + "description": "ZIP archive of the web dataset directory.", + "encodingFormat": "application/zip" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..3c00fa96c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json @@ -0,0 +1,50 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity contentUrl — invalid", + "description": "RO-Crate with a web-based File Data Entity whose contentUrl is not downloadable (returns HTML).", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [{"@id": "https://example.com/landing/dataset"}], + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "#publisher-org"}, + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/landing/dataset", + "@type": "File", + "name": "Dataset with broken contentUrl", + "description": "A web entity whose contentUrl resolves to an HTML page rather than a direct download.", + "encodingFormat": "application/zip", + "contentUrl": "https://cdn.example.com/broken-link" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json new file mode 100644 index 000000000..2542b4c95 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json @@ -0,0 +1,91 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity contentUrl — valid", + "description": "RO-Crate with a web-based File Data Entity that declares a downloadable contentUrl.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "https://example.com/landing/dataset" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/landing/dataset", + "@type": "File", + "name": "Dataset with contentUrl", + "description": "A web entity whose primary @id is a landing page, but contentUrl provides a direct download link.", + "encodingFormat": "application/zip", + "contentUrl": "https://cdn.example.com/dataset.zip", + "contentSize": "1024", + "sdDatePublished": "2024-01-01" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..655aad2dc --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json @@ -0,0 +1,33 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity downloadable — invalid", + "description": "RO-Crate with a web-based File Data Entity whose @id returns text/html (splash page), making it not directly downloadable.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [{"@id": "https://example.com/splash-page"}] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/splash-page", + "@type": "File", + "name": "Splash page (not downloadable)", + "description": "A web-based entity that resolves to an HTML splash page instead of a downloadable resource.", + "encodingFormat": "text/html" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0ff8f4d4d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json @@ -0,0 +1,90 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity downloadable — valid", + "description": "RO-Crate with a web-based File Data Entity whose @id returns a non-HTML Content-Type (directly downloadable).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "https://example.com/data.csv" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A web-based CSV file that is directly downloadable.", + "encodingFormat": "text/csv", + "contentSize": "512", + "sdDatePublished": "2024-01-01" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py new file mode 100644 index 000000000..333e468f3 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py @@ -0,0 +1,524 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates_v1_3 import DataEntities +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_root_data_entity_crates__ = DataEntities() + + +def test_valid_local_entity_reference(): + """ + Test that a Data Entity is valid when it references a local file using a relative path in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_local_entity_reference, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_local_entity_reference(): + """ + Test that a Data Entity is invalid when it references a local file using an absolute path in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_local_entity_reference, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Data Entity: identifier requirements"], + expected_triggered_issues=["MUST use a relative @id within the RO-Crate root"], + ) + + +def test_valid_detached_rocrate_dataEntities(): + """ + Test that a Data Entity is valid when it references a remote file using an absolute URI in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_detached_rocrate_dataEntities, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_detached_rocrate_dataEntities(): + """ + Test that a Data Entity is invalid when it references a remote file using a relative path in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_detached_rocrate_dataEntities, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Data Entity: identifier requirements"], + expected_triggered_issues=["has a local identifier but the Root Data Entity does not have a local identifier"], + ) + + +def test_valid_recommended_properties(): + """ + Test that a Data Entity is valid when it includes recommended properties. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_properties, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_properties(): + """ + Test that a Data Entity is invalid when it lacks recommended properties. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_properties, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Data Entity: RECOMMENDED properties"], + expected_triggered_issues=[ + "Data Entities SHOULD have a `name` property", + "Data Entities SHOULD have a `description` property", + ], + ) + + +def test_valid_recommended_encoding_format(): + """ + Test that a Data Entity is valid when it includes the recommended `encodingFormat` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_encoding_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_encoding_format(): + """ + Test that a Data Entity is invalid when it includes an invalid value for the recommended `encodingFormat` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_encoding_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Data Entity: RECOMMENDED `encodingFormat` property"], + expected_triggered_issues=["Missing or invalid `encodingFormat` linked to the `File Data Entity`"], + ) + + +# --------------------------------------------------------------------------- +# Web entity @id — downloadability (MUST at creation_time / enforce_availability) +# --------------------------------------------------------------------------- + + +class _ZipResponse: + status_code = 200 + headers = {"Content-Type": "application/zip"} + links = {} + + def raise_for_status(self): + pass + + +class _HtmlResponse: + status_code = 200 + headers = {"Content-Type": "text/html; charset=utf-8"} + links = {} + + def raise_for_status(self): + pass + + +def test_valid_required_web_entity_downloadable(monkeypatch): + """ + Web-based Data Entity whose @id returns a non-HTML Content-Type MUST pass + the availability check when enforce_availability=True. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_web_entity_downloadable, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + ) + + +def test_invalid_required_web_entity_not_downloadable(monkeypatch): + """ + Web-based Data Entity whose @id returns text/html (splash page) MUST fail + the availability check when enforce_availability=True. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_web_entity_splash_page, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + expected_triggered_requirements=["Web-based Data Entity: REQUIRED availability"], + expected_triggered_issues=["HTML page"], + ) + + +# --------------------------------------------------------------------------- +# Web entity @id — availability warning (RECOMMENDED, default mode) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_web_entity_downloadable_warning(monkeypatch): + """ + Web-based Data Entity whose @id returns application/zip passes the + RECOMMENDED availability check in default mode. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_web_entity_downloadable, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_web_entity_splash_page_warning(monkeypatch): + """ + Web-based Data Entity whose @id returns text/html triggers a RECOMMENDED + warning about a possible splash page in default mode. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_web_entity_splash_page, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Web-based Data Entity: REQUIRED availability"], + expected_triggered_issues=["HTML page"], + ) + + +# --------------------------------------------------------------------------- +# Web entity contentUrl — downloadability (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_content_url_downloadable(monkeypatch): + """ + Web-based Data Entity whose contentUrl returns application/zip passes the + RECOMMENDED contentUrl availability check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_content_url, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_content_url_not_downloadable(monkeypatch): + """ + Web-based Data Entity whose contentUrl returns text/html fails the + RECOMMENDED contentUrl availability check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_content_url, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Web-based Data Entity: REQUIRED availability"], + expected_triggered_issues=["contentUrl", "not directly downloadable"], + ) + + +def test_valid_missing_file_local_path(): + """ + A missing local file that declares localPath, and a deliberately absent + file (#id) that declares localPath, SHOULD both pass the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_missing_file_local_path, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "ro-crate-1.3_16.1", + "ro-crate-1.3_38.1", + "ro-crate-1.3_17.1", + "ro-crate-1.3_39.0", + "ro-crate-1.3_39.1", + "ro-crate-1.3_18.1", + ], + ) + + +def test_invalid_missing_file_no_local_path(): + """ + A missing local file without localPath and a deliberately absent file (#id) + without localPath SHOULD each trigger a RECOMMENDED warning. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_missing_file_local_path, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1"], + expected_triggered_requirements=["Data Entity: missing file SHOULD use localPath"], + expected_triggered_issues=["localPath"], + ) + + +def test_valid_data_entity_license_divergence(): + """ + A Data Entity with a different license from the Root SHOULD pass + the RECOMMENDED check (the entity is correctly overriding the + default license). + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_data_entity_license_divergence, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1", "ro-crate-1.3_38.1"], + ) + + +def test_invalid_data_entity_redundant_license(): + """ + A Data Entity that declares the same license as the Root SHOULD + still pass validation, but a warning SHOULD be logged about the + redundant license declaration. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_data_entity_license_divergence, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1", "ro-crate-1.3_38.1"], + ) + + +def test_redundant_license_logs_warning(): + """ + When a Data Entity declares the same license as the Root, a warning + message SHOULD be logged (not a validation issue). The validation + result MUST pass, and the warning MUST appear in the log stream. + """ + from rocrate_validator.utils.log import __log_stream__ + + # Clear any previous log output + __log_stream__.truncate(0) + __log_stream__.seek(0) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_data_entity_license_divergence, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1", "ro-crate-1.3_38.1"], + ) + + log_contents = __log_stream__.getvalue() + assert "redundant" in log_contents.lower(), f"Expected a warning log about redundant license, got:\n{log_contents}" + + +# --------------------------------------------------------------------------- +# File Data Entity — contentSize (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_contentSize(): + """ + A File Data Entity with the recommended `contentSize` property SHOULD pass + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_contentSize, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_contentSize(): + """ + A File Data Entity without the recommended `contentSize` property SHOULD + fail the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_contentSize, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Data Entity: RECOMMENDED contentSize"], + expected_triggered_issues=["contentSize"], + ) + + +# --------------------------------------------------------------------------- +# File Data Entity — conformsTo profile (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_conformsto(): + """ + A File Data Entity whose `conformsTo` references a CreativeWork or Profile + SHOULD pass the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_conformsto, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_conformsto(): + """ + A File Data Entity whose `conformsTo` references a non-Profile/non-CreativeWork + entity SHOULD fail the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_conformsto, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File: RECOMMENDED `conformsTo` profile"], + expected_triggered_issues=["conformsTo"], + ) + + +# --------------------------------------------------------------------------- +# Web-based File Data Entity — sdDatePublished (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_sdDatePublished(monkeypatch): + """ + A web-based File Data Entity with the recommended `sdDatePublished` property + SHOULD pass the RECOMMENDED check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_sdDatePublished, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_sdDatePublished(monkeypatch): + """ + A web-based File Data Entity without the recommended `sdDatePublished` property + SHOULD fail the RECOMMENDED check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_sdDatePublished, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Web-based Data Entity: RECOMMENDED properties"], + expected_triggered_issues=["sdDatePublished"], + ) + + +# --------------------------------------------------------------------------- +# 4.3 Dataset (Directory) Data Entity — trailing slash (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_dataset_trailing_slash(): + """ + A local Dataset Data Entity whose @id ends with '/' passes the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_dataset_trailing_slash, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + metadata_only=True, + ) + + +def test_invalid_recommended_dataset_trailing_slash(): + """ + A local Dataset Data Entity whose @id does NOT end with '/' fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_dataset_trailing_slash, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + metadata_only=True, + expected_triggered_requirements=["Dataset Data Entity: SHOULD end with trailing slash"], + expected_triggered_issues=["The @id of a local Dataset Data Entity SHOULD end with '/'"], + ) + + +# --------------------------------------------------------------------------- +# 4.3 Dataset (Directory) Data Entity — hasPart (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_dataset_has_part(): + """ + A local Dataset Data Entity that lists its contents via hasPart passes + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_dataset_has_part, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + metadata_only=True, + ) + + +def test_invalid_recommended_dataset_has_part(): + """ + A local Dataset Data Entity without a hasPart property fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_dataset_has_part, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + metadata_only=True, + expected_triggered_requirements=["Dataset Data Entity: SHOULD have hasPart"], + expected_triggered_issues=["Local Dataset Data Entities SHOULD list their contents via `hasPart`"], + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 2d4bf2383..7f237be99 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -394,6 +394,132 @@ def invalid_recommended_conformsto_if_profiles(self) -> Path: return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_conformsto_if_profiles" / "invalid" +class DataEntities: + DATA_ENTITIES_CRATES_PATH = BASE_PATH / "8_metadata_dataEntities" + + @property + def valid_local_entity_reference(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "local_entity_reference" / "valid" + + @property + def invalid_local_entity_reference(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "local_entity_reference" / "invalid" + + @property + def valid_detached_rocrate_dataEntities(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" + + @property + def invalid_detached_rocrate_dataEntities(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "invalid" + + @property + def valid_recommended_properties(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_properties" / "valid" + + @property + def invalid_recommended_properties(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_properties" / "invalid" + + @property + def valid_recommended_encoding_format(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_encoding_format" / "valid" + + @property + def invalid_recommended_encoding_format(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_encoding_format" / "invalid" + + @property + def valid_web_entity_downloadable(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_downloadable" / "valid" + + @property + def invalid_web_entity_splash_page(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_downloadable" / "invalid" + + @property + def valid_recommended_content_url(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_content_url" / "valid" + + @property + def invalid_recommended_content_url(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_content_url" / "invalid" + + @property + def valid_recommended_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_dataset_distribution" / "valid" + + @property + def invalid_recommended_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_dataset_distribution" / "invalid" + + @property + def valid_web_directory_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_directory_distribution" / "valid" + + @property + def invalid_web_directory_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_directory_distribution" / "invalid" + + @property + def valid_missing_file_local_path(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "missing_file_local_path" / "valid" + + @property + def invalid_missing_file_local_path(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "missing_file_local_path" / "invalid" + + @property + def valid_data_entity_license_divergence(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "data_entity_license" / "valid" + + @property + def invalid_data_entity_license_divergence(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "data_entity_license" / "invalid" + + @property + def valid_recommended_contentSize(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_contentSize" / "valid" + + @property + def invalid_recommended_contentSize(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_contentSize" / "invalid" + + @property + def valid_recommended_conformsto(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_conformsto" / "valid" + + @property + def invalid_recommended_conformsto(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_conformsto" / "invalid" + + @property + def valid_recommended_sdDatePublished(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_sdDatePublished" / "valid" + + @property + def invalid_recommended_sdDatePublished(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_sdDatePublished" / "invalid" + + # 4.3 Dataset (Directory) Data Entity — trailing slash + @property + def valid_recommended_dataset_trailing_slash(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_trailing_slash" / "valid" + + @property + def invalid_recommended_dataset_trailing_slash(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_trailing_slash" / "invalid" + + # 4.3 Dataset (Directory) Data Entity — hasPart + @property + def valid_recommended_dataset_has_part(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_has_part" / "valid" + + @property + def invalid_recommended_dataset_has_part(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_has_part" / "invalid" + + class WorkflowsScripts: WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" From 6bd45817e2be936179ff007d727a0450eef8d6ba Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 23 Sep 2026 10:13:29 +0200 Subject: [PATCH 093/271] =?UTF-8?q?feat(validation):=20=E2=9C=A8=20add=20r?= =?UTF-8?q?eusable=20validation=20preparation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 267 +++++++++++-- .../requirements/shacl/validator.py | 61 +-- rocrate_validator/services.py | 14 +- tests/unit/test_warm_start.py | 371 ++++++++++++++++++ 4 files changed, 622 insertions(+), 91 deletions(-) create mode 100644 tests/unit/test_warm_start.py diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 4a50b1bca..e02fa0563 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -14,6 +14,10 @@ from __future__ import annotations +import copy +import json +import threading +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any from urllib.error import HTTPError @@ -46,10 +50,26 @@ from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import find_offline_cache_miss +from rocrate_validator.utils.rdf import extract_base_from_jsonld +from rocrate_validator.utils.uri import URI if TYPE_CHECKING: from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck - from rocrate_validator.utils.uri import URI + + +@dataclass(frozen=True) +class PreparedValidationPlan: + """ + Profile state that can be reused by validation runs with the same base. + + Profiles own their lazily loaded requirements and shape registries, so keeping + the same profile instances avoids reparsing those artifacts on every run. The + plan deliberately excludes all crate data, results and SHACL execution state; + those remain scoped to :class:`ValidationContext`. + """ + + profiles: tuple[Profile, ...] + ontology_graph: Graph class Validator(Publisher): @@ -65,9 +85,13 @@ class Validator(Publisher): Initializes the Validator with the given settings. validation_settings() -> ValidationSettings: Returns the validation settings. - detect_rocrate_profiles() -> list[Profile]: + ``detect_rocrate_profiles(rocrate_uri=None, metadata_dict=None)``: Detects the profiles to validate against. - validate() -> ValidationResult: + ``prepare(rocrate_uri=None, metadata_dict=None, refresh=False)``: + Eagerly prepares reusable profile artifacts. + clear_prepared_profiles(): + Invalidates profile preparation owned by this validator. + ``validate(rocrate_uri=None, metadata_dict=None)``: Validate the RO-Crate against the detected profiles according to the validation settings validate_requirements(requirements: list[Requirement]) -> ValidationResult: Validates the RO-Crate against the specified subset of the profile requirements. @@ -78,17 +102,162 @@ def __init__(self, settings: dict | ValidationSettings): super().__init__() # initialize the current context self.__current_context__: ValidationContext | None = None + # Profile artifacts may contain relative IRIs resolved against the crate + # public ID, so plans are cached by every setting that affects profile + # preparation, including that effective base. + self.__prepared_profile_catalogs: dict[tuple[Any, ...], tuple[Profile, ...]] = {} + self.__prepared_validation_plans: dict[tuple[Any, ...], PreparedValidationPlan] = {} + # Publisher state and ``__current_context__`` are instance-local and + # mutable. Serialize calls made on the same Validator instead of + # allowing two runs to corrupt each other's event/statistics context. + self.__run_lock = threading.Lock() @property def validation_settings(self) -> ValidationSettings: return self._validation_settings - def detect_rocrate_profiles(self) -> list[Profile]: + @staticmethod + def __prepared_profile_catalog_key__(context: ValidationContext) -> tuple[Any, ...]: + return ( + context.profiles_path.resolve(), + context.extra_profiles_path.resolve() if context.extra_profiles_path else None, + context.publicID, + context.requirement_severity, + context.allow_requirement_check_override, + ) + + def __get_prepared_profile_catalog__(self, context: ValidationContext) -> tuple[Profile, ...]: + key = self.__prepared_profile_catalog_key__(context) + profiles = self.__prepared_profile_catalogs.get(key) + if profiles is None: + profiles = tuple( + Profile.load_profiles( + context.profiles_path, + extra_profiles_path=context.extra_profiles_path, + publicID=context.publicID, + severity=context.requirement_severity, + allow_requirement_check_override=context.allow_requirement_check_override, + ) + ) + self.__prepared_profile_catalogs[key] = profiles + return profiles + + @staticmethod + def __prepared_validation_plan_key__(context: ValidationContext) -> tuple[Any, ...]: + return ( + context.profiles_path.resolve(), + context.extra_profiles_path.resolve() if context.extra_profiles_path else None, + context.publicID, + context.effective_ontology_base, + context.profile_identifier, + context.requirement_severity, + context.inheritance_enabled, + context.allow_requirement_check_override, + context.disable_check_for_duplicates, + ) + + def __get_prepared_validation_plan__(self, context: ValidationContext) -> PreparedValidationPlan: + key = self.__prepared_validation_plan_key__(context) + plan = self.__prepared_validation_plans.get(key) + if plan is None: + profiles = tuple(context.__load_profiles__()) + ontology_graph = Graph() + for profile in profiles: + # Materialize lazy requirements and their per-profile shape + # registries once. Contextual graphs passed to pySHACL are still + # copied for every validation run. + _ = profile.requirements + ontology_path = profile.path / "ontology.ttl" + if ontology_path.exists(): + ontology_graph.parse( + ontology_path, + format="ttl", + publicID=context.effective_ontology_base, + ) + plan = PreparedValidationPlan(profiles, ontology_graph) + self.__prepared_validation_plans[key] = plan + # Loading may resolve a bare profile token (for example + # ``ro-crate``) to a versioned identifier and update the settings. + # Store the canonical key as an alias for subsequent warm runs. + self.__prepared_validation_plans[self.__prepared_validation_plan_key__(context)] = plan + return plan + + def clear_prepared_profiles(self) -> None: + """ + Discard all profile preparation cached by this validator. + + Call this after changing profile files on disk. Existing validation + results remain valid and keep their own per-run contexts. + """ + + with self.__run_lock: + self.__prepared_profile_catalogs.clear() + self.__prepared_validation_plans.clear() + + def prepare( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + refresh: bool = False, + ) -> None: + """ + Eagerly prepare the selected profile for a later validation run. + + By default an instance prepares profiles lazily during its first + :meth:`validate` call. ``refresh=True`` first discards every catalog and + plan cached by this validator, which is useful after editing profile + files in a long-running process. + """ + + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + with self.__run_lock: + if refresh: + self.__prepared_profile_catalogs.clear() + self.__prepared_validation_plans.clear() + context = ValidationContext(self, settings) + _ = context.prepared_validation_plan + + @staticmethod + def __settings_for_run__( + settings: ValidationSettings, + rocrate_uri: str | Path | URI | None, + metadata_dict: dict | None, + ) -> ValidationSettings: + if rocrate_uri is not None and metadata_dict is not None: + raise ValueError("rocrate_uri and metadata_dict are mutually exclusive") + if rocrate_uri is None and metadata_dict is None: + # Preserve the established behavior, including writing a resolved + # bare profile identifier back to the caller's settings. + return settings + + run_settings = copy.copy(settings) + if rocrate_uri is not None: + run_settings.rocrate_uri = URI(str(rocrate_uri)) + run_settings.metadata_dict = None + else: + if not isinstance(metadata_dict, dict): + raise TypeError("metadata_dict must be a dictionary") + run_settings.metadata_dict = copy.deepcopy(metadata_dict) + run_settings.metadata_only = True + return run_settings + + def detect_rocrate_profiles( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + ) -> list[Profile]: """ Detect the profiles to validate against """ + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + with self.__run_lock: + return self.__detect_rocrate_profiles__(settings) + + def __detect_rocrate_profiles__(self, settings: ValidationSettings) -> list[Profile]: # initialize the validation context - context = ValidationContext(self, self.validation_settings) + context = ValidationContext(self, settings) candidate_profiles_uris: set[str] = set() candidate_profiles_uris.update(context.ro_crate.metadata.get_conforms_to() or []) candidate_profiles_uris.update(context.ro_crate.metadata.get_root_data_entity_conforms_to() or []) @@ -100,12 +269,7 @@ def detect_rocrate_profiles(self) -> list[Profile]: # load the profiles profiles = [] candidate_profiles = [] - available_profiles = Profile.load_profiles( - context.profiles_path, - extra_profiles_path=context.extra_profiles_path, - publicID=context.publicID, - severity=context.requirement_severity, - ) + available_profiles = self.__get_prepared_profile_catalog__(context) profiles = [p for p in available_profiles if p.uri in candidate_profiles_uris] # get the candidate profiles for profile in profiles: @@ -129,17 +293,25 @@ def detect_rocrate_profiles(self) -> list[Profile]: ) return candidate_profiles - def validate(self) -> ValidationResult: + def validate( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + ) -> ValidationResult: """ Validate the RO-Crate against the detected profiles according to the validation settings """ - return self.__do_validate__() + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + return self.__do_validate__(settings=settings) def validate_requirements( self, requirements: list[Requirement], *, include_dependencies: bool = True, + rocrate_uri: str | Path | URI | None = None, + metadata_dict: dict | None = None, ) -> ValidationResult: """ Validates the RO-Crate against the specified subset of the profile requirements. @@ -153,12 +325,28 @@ def validate_requirements( requirements, include_dependencies=include_dependencies, ) - return self.__do_validate__(resolved_requirements) + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + return self.__do_validate__(resolved_requirements, settings=settings) + + def __do_validate__( + self, + requirements: list[Requirement] | None = None, + *, + settings: ValidationSettings, + ) -> ValidationResult: - def __do_validate__(self, requirements: list[Requirement] | None = None) -> ValidationResult: # noqa: C901, PLR0912, PLR0915 + with self.__run_lock: + return self.__do_validate_locked__(requirements, settings=settings) + + def __do_validate_locked__( # noqa: C901, PLR0912, PLR0915 # pylint: disable=too-many-locals,too-many-branches,too-many-statements + self, + requirements: list[Requirement] | None, + *, + settings: ValidationSettings, + ) -> ValidationResult: # initialize the validation context - context = ValidationContext(self, self.validation_settings) + context = ValidationContext(self, settings) # register the current context self.__current_context__ = context @@ -175,8 +363,11 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali # profiles that have not yet been visited. for p in profiles: _ = p.requirements + # Requirement objects belong to one prepared plan. Match by stable + # identifier so a caller can reuse a selection when a per-call + # crate input selects an equivalent plan for another RDF base. selected_requirement_ids = ( - None if requirements is None else {id(requirement) for requirement in requirements} + None if requirements is None else {requirement.identifier for requirement in requirements} ) self.notify(EventType.VALIDATION_START) for profile_index, profile in enumerate(profiles): @@ -198,7 +389,7 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali profile_requirements = [ requirement for requirement in profile.requirements - if id(requirement) in selected_requirement_ids + if requirement.identifier in selected_requirement_ids ] logger.debug( "Validating profile %s with %s requirements", @@ -213,7 +404,7 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali ) terminate = False requirement_index = -1 - for requirement_index in range(len(profile_requirements)): + for requirement_index in range(len(profile_requirements)): # pylint: disable=consider-using-enumerate requirement = profile_requirements[requirement_index] if not requirement.overridden: self.notify( @@ -263,7 +454,7 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali remaining_requirements = [ requirement for requirement in remaining_profile.requirements - if id(requirement) in selected_requirement_ids + if requirement.identifier in selected_requirement_ids ] Requirement.record_skipped_checks(remaining_requirements, context) break @@ -321,6 +512,8 @@ def __init__(self, validator: Validator, settings: ValidationSettings): self._data_graph: Graph | None = None # reference to the profiles self._profiles: list[Profile] | None = None + # reference to the immutable profile preparation selected for this run + self._prepared_validation_plan: PreparedValidationPlan | None = None # reference to the target profile self._target_validation_profile: Profile | None = None # reference to the validation result @@ -410,6 +603,20 @@ def publicID(self) -> str: path = f"{path}/" return path + @property + def effective_ontology_base(self) -> str: + """Base used while parsing profile ontology artifacts for this run.""" + + try: + return extract_base_from_jsonld(self.ro_crate.metadata.as_dict()) or self.publicID + except (ROCrateMetadataNotFoundError, json.JSONDecodeError, UnicodeDecodeError) as error: + # Profile preparation happens before descriptor checks execute. An + # unreadable descriptor is validation input, not a preparation + # failure: use the crate public ID and let the owning checks report + # the precise missing/JSON/encoding issue in the normal pipeline. + logger.debug("Unable to read metadata @base while preparing profiles: %s", error) + return self.publicID + @property def profiles_path(self) -> Path: """ @@ -618,13 +825,7 @@ def disable_check_for_duplicates(self) -> bool: def __load_profiles__(self) -> list[Profile]: # load all profiles - profiles = Profile.load_profiles( - self.profiles_path, - extra_profiles_path=self.settings.extra_profiles_path, - publicID=self.publicID, - severity=self.requirement_severity, - allow_requirement_check_override=self.allow_requirement_check_override, - ) + profiles = list(self.validator.__get_prepared_profile_catalog__(self)) # Check if the target profile is in the list of profiles. A bare token # (e.g. `ro-crate`) resolves to the highest available version. @@ -683,9 +884,19 @@ def profiles(self) -> list[Profile]: :rtype: list[Profile] """ if not self._profiles: - self._profiles = self.__load_profiles__() + self._prepared_validation_plan = self.validator.__get_prepared_validation_plan__(self) + self._profiles = list(self._prepared_validation_plan.profiles) return self._profiles.copy() + @property + def prepared_validation_plan(self) -> PreparedValidationPlan: + """Prepared profile state selected for this validation context.""" + + if self._prepared_validation_plan is None: + _ = self.profiles + assert self._prepared_validation_plan is not None + return self._prepared_validation_plan + @property def target_validation_profile(self) -> Profile: """ diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 114867095..a4e74256a 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -27,7 +27,6 @@ from pyshacl.pytypes import GraphLike from rocrate_validator.constants import ( - DEFAULT_ONTOLOGY_FILE, RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, @@ -50,7 +49,6 @@ ) from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging -from rocrate_validator.utils.rdf import extract_base_from_jsonld # set up logging logger = logging.getLogger(__name__) @@ -112,9 +110,10 @@ def __init__(self, context: ValidationContext): """Initialize a SHACL context shared by all profiles in ``context``.""" super().__init__(context.validator, context.settings) self._base_context: ValidationContext = context - # ontology paths resolved for each profile and optional filename - self._ontology_paths: dict[tuple[Path, str], Path] = {} - + # The SHACL adapter is a per-run context, but profile definitions are + # immutable preparation state. Reuse the base context's instances so + # target lookup and override handling cannot trigger another load. + self._profiles = context.profiles # reference to the contextual ShapeRegistry instance self._shapes_registry: ShapesRegistry = ShapesRegistry() @@ -135,8 +134,10 @@ def __init__(self, context: ValidationContext): # store the validation result of the current profile (a pass/fail boolean) self._validation_result: bool | None = None - # reference to the contextual ontology graph + # pySHACL may mutate supplied graphs while applying inference/rules. + # Copy the prepared ontology into this run's working graph. self._ontology_graph: Graph = Graph() + self._ontology_graph += context.prepared_validation_plan.ontology_graph def __set_current_validation_profile__(self, profile: Profile) -> bool: """ @@ -156,10 +157,6 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: raise SHACLValidationAlreadyProcessed(profile.identifier, self.get_validation_result(profile)) if profile.identifier not in self._loaded_profiles: - # augment the ontology graph with the profile ontology - ontology_graph = self.__load_ontology_graph__(profile.path) - if ontology_graph: - self._ontology_graph += ontology_graph # augment the shapes registry with the profile shapes profile_registry = ShapesRegistry.get_instance(profile) profile_shapes = profile_registry.get_shapes() @@ -232,50 +229,6 @@ def shapes_registry(self) -> ShapesRegistry: def shapes_graph(self) -> Graph: return self.shapes_registry.shapes_graph - def __get_ontology_path__(self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE) -> Path: - """Return the cached ontology path for a profile and filename pair.""" - key = (profile_path, ontology_filename) - if key not in self._ontology_paths: - self._ontology_paths[key] = profile_path / ontology_filename - return self._ontology_paths[key] - - def __get_data_graph_base__(self) -> str | None: - """ - Get the @base from the RO-Crate metadata JSON-LD. - - This extracts the @base from the @context of the data graph metadata, - which can be used to align the ontology graph's base URI with the data graph. - - :return: The @base value if found, None otherwise - """ - metadata_dict = self.ro_crate.metadata.as_dict() - return extract_base_from_jsonld(metadata_dict) - - def __load_ontology_graph__( - self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE - ) -> Graph | None: - # load the graph of ontologies - ontology_graph: Graph | None = None - ontology_path = self.__get_ontology_path__(profile_path, ontology_filename) - if ontology_path.exists(): - logger.debug("Loading ontologies: %s", ontology_path) - ontology_graph = Graph() - - # Determine the publicID to use: - # 1. First, try to get @base from the data graph metadata - # 2. Fall back to the default publicID (RO-Crate URI) - data_graph_base = self.__get_data_graph_base__() - public_id = data_graph_base or self.publicID - - if data_graph_base: - logger.debug("Using @base from data graph metadata: %s", data_graph_base) - else: - logger.debug("Using default publicID: %s", self.publicID) - - ontology_graph.parse(ontology_path, format="ttl", publicID=public_id) - logger.debug("Ontologies loaded: %s", ontology_graph) - return ontology_graph - @property def ontology_graph(self) -> Graph: return self._ontology_graph diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index b8fbc89df..f5bc76e92 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -51,15 +51,11 @@ def validate_metadata_as_dict( """ assert metadata_dict is not None, "Metadata dictionary cannot be None" assert isinstance(metadata_dict, dict), "Metadata must be a dictionary" - # set the RO-Crate metadata dictionary in the settings - if isinstance(settings, dict): - settings["metadata_dict"] = metadata_dict - settings["metadata_only"] = True - else: - settings.metadata_dict = metadata_dict - settings.metadata_only = True - # validate the RO-Crate metadata - return validate(settings, subscribers) + parsed_settings = ValidationSettings.parse(settings) + validator = _build_validator(parsed_settings, subscribers) + # Use per-run input instead of mutating caller-owned settings. This shares + # the same prepared-validator path used for directory-based validation. + return validator.validate(metadata_dict=metadata_dict) def validate(settings: dict | ValidationSettings, subscribers: list[Subscriber] | None = None) -> ValidationResult: diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py new file mode 100644 index 000000000..bbc10a676 --- /dev/null +++ b/tests/unit/test_warm_start.py @@ -0,0 +1,371 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import json +import threading +import time +from pathlib import Path + +import pytest +from rdflib import Graph, URIRef +from rdflib.compare import isomorphic + +from rocrate_validator.errors import ValidationExecutionError +from rocrate_validator.models import URI, Profile, Requirement, ValidationContext, ValidationSettings, Validator +from rocrate_validator.requirements.shacl.checks import SHACLCheck +from rocrate_validator.requirements.shacl.models import ShapesRegistry +from rocrate_validator.requirements.shacl.validator import SHACLValidationContext +from rocrate_validator.services import validate_metadata_as_dict +from tests.ro_crates import ValidROC + + +def _settings(crate: Path) -> ValidationSettings: + return ValidationSettings( + rocrate_uri=URI(crate), + extra_profiles_path=Path("tests/data/profiles/fake"), + profile_identifier="c", + offline=True, + ) + + +def test_repeated_validation_reuses_prepared_profiles(monkeypatch): + """A warm run must not reload profiles or rebuild their requirements.""" + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + cold_result = validator.validate() + warm_result = validator.validate() + + assert calls == 1 + assert cold_result.passed() == warm_result.passed() + assert cold_result.statistics.total_checks == warm_result.statistics.total_checks + assert cold_result.statistics.profiles == warm_result.statistics.profiles + + +def test_warm_run_does_not_reparse_shapes(monkeypatch): + calls = 0 + original = ShapesRegistry.load_shapes + + def counted_load_shapes(self, *args, **kwargs): + nonlocal calls + calls += 1 + return original(self, *args, **kwargs) + + monkeypatch.setattr(ShapesRegistry, "load_shapes", counted_load_shapes) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + validator.validate() + cold_calls = calls + validator.validate() + + assert cold_calls > 0 + assert calls == cold_calls + + +def test_per_call_metadata_does_not_mutate_validator_settings(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata = json.load(stream) + metadata["@context"] = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + settings = _settings(crate) + validator = Validator(settings) + + result = validator.validate(metadata_dict=metadata) + + assert result.passed() + assert settings.metadata_dict is None + assert not settings.metadata_only + + +def test_distinct_explicit_metadata_bases_keep_separate_plans(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + first_metadata = json.load(stream) + first_metadata["@context"] = { + "@base": "https://example.org/crate-a/", + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + second_metadata = copy.deepcopy(first_metadata) + second_metadata["@context"]["@base"] = "https://example.org/crate-b/" + validator = Validator(_settings(crate)) + + first = validator.validate(metadata_dict=first_metadata) + second = validator.validate(metadata_dict=second_metadata) + + assert first.context.effective_ontology_base == "https://example.org/crate-a/" + assert second.context.effective_ontology_base == "https://example.org/crate-b/" + assert first.context.prepared_validation_plan is not second.context.prepared_validation_plan + + +def test_metadata_service_does_not_mutate_caller_settings(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata = json.load(stream) + metadata["@context"] = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + settings = _settings(crate) + + result = validate_metadata_as_dict(metadata, settings) + + assert result.passed() + assert settings.metadata_dict is None + assert not settings.metadata_only + + +def test_per_call_inputs_are_mutually_exclusive(): + validator = Validator(_settings(ValidROC().wrroc_paper)) + + with pytest.raises(ValueError, match="mutually exclusive"): + validator.validate(ValidROC().wrroc_paper, metadata_dict={"@graph": []}) + + +def test_requirement_selection_survives_a_per_call_base_change(monkeypatch): + settings = _settings(ValidROC().wrroc_paper) + validator = Validator(settings) + source_context = ValidationContext(validator, settings) + selected = source_context.profiles[-1].requirements[0] + executed = [] + + def record_requirement(requirement, context): + executed.append(requirement.identifier) + return True + + monkeypatch.setattr(Requirement, "_do_validate_", record_requirement) + + validator.validate_requirements( + [selected], + include_dependencies=False, + rocrate_uri=ValidROC().wrroc_paper_long_date, + ) + + assert executed == [selected.identifier] + + +def test_clear_prepared_profiles_forces_reload(monkeypatch): + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + validator.validate() + validator.clear_prepared_profiles() + validator.validate() + + assert calls == 2 + + +def test_profile_detection_and_validation_share_catalog(monkeypatch): + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + detected = validator.detect_rocrate_profiles() + result = validator.validate() + + assert detected + assert result.passed() + assert calls == 1 + + +def test_prepare_can_eagerly_warm_and_refresh_profiles(monkeypatch): + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + validator.prepare() + validator.validate() + assert calls == 1 + + validator.prepare(refresh=True) + validator.validate() + assert calls == 2 + + +def test_inherited_ontologies_are_prepared_once(monkeypatch): + ontology_parses = 0 + original_parse = Graph.parse + + def counted_parse(self, source=None, *args, **kwargs): + nonlocal ontology_parses + if source is not None and str(source).endswith("ontology.ttl"): + ontology_parses += 1 + return original_parse(self, source, *args, **kwargs) + + monkeypatch.setattr(Graph, "parse", counted_parse) + settings = ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + profile_identifier="isa-ro-crate", + offline=True, + ) + validator = Validator(settings) + + first_context = ValidationContext(validator, settings) + first_plan = first_context.prepared_validation_plan + second_context = ValidationContext(validator, settings) + second_plan = second_context.prepared_validation_plan + + expected = Graph() + for profile in first_plan.profiles: + ontology_path = profile.path / "ontology.ttl" + if ontology_path.exists(): + original_parse(expected, ontology_path, format="ttl", publicID=first_context.effective_ontology_base) + + assert ontology_parses == 2 + assert first_plan is second_plan + assert isomorphic(first_plan.ontology_graph, expected) + + +def test_failed_run_does_not_contaminate_next_run(monkeypatch): + validator = Validator(_settings(ValidROC().wrroc_paper)) + original_execute_check = SHACLCheck.execute_check + + def fail_validation(*args, **kwargs): + raise ValidationExecutionError("synthetic engine failure") + + monkeypatch.setattr(SHACLCheck, "execute_check", fail_validation) + with pytest.raises(ValidationExecutionError, match="synthetic engine failure"): + validator.validate() + + monkeypatch.setattr(SHACLCheck, "execute_check", original_execute_check) + assert validator.validate().passed() + + +def test_per_run_graph_mutations_do_not_reach_the_next_run(): + validator = Validator(_settings(ValidROC().wrroc_paper)) + marker = ( + URIRef("https://example.org/run"), + URIRef("https://example.org/has"), + URIRef("https://example.org/private-marker"), + ) + + first = validator.validate() + first_shacl_context = SHACLValidationContext.get_instance(first.context) + first_shacl_context.ontology_graph.add(marker) + first_shacl_context.shapes_registry._shapes_graph.add(marker) + + second = validator.validate() + second_shacl_context = SHACLValidationContext.get_instance(second.context) + + assert marker not in second_shacl_context.ontology_graph + assert marker not in second_shacl_context.shapes_registry.shapes_graph + assert marker not in second.context.prepared_validation_plan.ontology_graph + + +def test_cold_and_warm_runs_report_equivalent_violations(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata = json.load(stream) + metadata["@context"] = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + settings = ValidationSettings( # type: ignore[call-arg] + metadata_dict=metadata, + profiles_path=Path("tests/data/profiles/sparql_test"), + profile_identifier="sparql-test", + offline=True, + ) + validator = Validator(settings) + + cold = validator.validate() + warm = validator.validate() + + def issue_signature(result): + return [ + ( + issue.check.identifier, + issue.message, + issue.violatingEntity, + issue.violatingProperty, + issue.violatingPropertyValue, + ) + for issue in result.get_issues() + ] + + assert issue_signature(cold) + assert issue_signature(cold) == issue_signature(warm) + assert cold.statistics.total_checks == warm.statistics.total_checks + assert cold.statistics.check_count_by_severity == warm.statistics.check_count_by_severity + + +def test_validator_serializes_overlapping_runs(monkeypatch): + validator = Validator(_settings(ValidROC().wrroc_paper)) + active = 0 + max_active = 0 + state_lock = threading.Lock() + original = validator.__do_validate_locked__ + + def tracked(*args, **kwargs): + nonlocal active, max_active + with state_lock: + active += 1 + max_active = max(max_active, active) + time.sleep(0.05) + try: + return original(*args, **kwargs) + finally: + with state_lock: + active -= 1 + + monkeypatch.setattr(validator, "__do_validate_locked__", tracked) + results = [] + threads = [threading.Thread(target=lambda: results.append(validator.validate())) for _ in range(2)] + + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert max_active == 1 + assert len(results) == 2 + assert all(result.passed() for result in results) From f04b1fa20ec051c76492630e3347f7d519489415 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 23 Sep 2026 10:19:02 +0200 Subject: [PATCH 094/271] rocrate-1.3: add referenced rocrate tests --- .../invalid_md_about/ro-crate-metadata.json | 62 +++++ .../ro-crate-metadata.json | 62 +++++ .../ro-crate-metadata.json | 61 +++++ .../ro-crate-metadata.json | 113 ++++++++ .../ro-crate-metadata.json | 55 ++++ .../ro-crate-metadata.json | 53 ++++ .../ro-crate-metadata.json | 66 +++++ .../valid/ro-crate-metadata.json | 112 ++++++++ .../ro-crate-metadata.json | 112 ++++++++ .../ro-crate-metadata.json | 113 ++++++++ .../subcrate/data.txt | 0 .../test_referenced_rocrate_v1_3.py | 243 ++++++++++++++++++ tests/ro_crates_v1_3.py | 44 ++++ 13 files changed, 1096 insertions(+) create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json create mode 100644 tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/subcrate/data.txt create mode 100644 tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json new file mode 100644 index 000000000..3b060e95b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json @@ -0,0 +1,62 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with referenced RO-Crate metadata descriptor with about", + "description": "The metadata descriptor of the referenced RO-Crate has about which should not be present.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate whose metadata descriptor has about.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "application/ld+json", + "about": {"@id": "https://example.org/other-ro-crate/"} + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json new file mode 100644 index 000000000..a7f4ec4b2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json @@ -0,0 +1,62 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with referenced RO-Crate metadata descriptor with conformsTo", + "description": "The metadata descriptor of the referenced RO-Crate has conformsTo which should not be present.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate whose metadata descriptor has conformsTo.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "application/ld+json", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json new file mode 100644 index 000000000..c19e6a7d9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json @@ -0,0 +1,61 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with referenced RO-Crate metadata descriptor with wrong encodingFormat", + "description": "The metadata descriptor of the referenced RO-Crate has the wrong encodingFormat.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate with wrong encodingFormat on its metadata descriptor.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "text/turtle" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json new file mode 100644 index 000000000..93a8eb998 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate (missing sdDatePublished)", + "description": "An RO-Crate referencing another RO-Crate whose @id is an absolute URI not declared as a persistent identifier; the referenced entity lacks sdDatePublished.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": [ + { + "@id": "https://example.org/other-ro-crate/" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate with neither identifier nor sdDatePublished.", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "subjectOf": { + "@id": "#other-ro-crate-metadata" + }, + "distribution": { + "@id": "https://example.org/other-ro-crate/archive.tar.gz" + } + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://example.org/other-ro-crate/archive.tar.gz", + "@type": "DataDownload", + "name": "Other RO-Crate Archive", + "encodingFormat": "application/gzip" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json new file mode 100644 index 000000000..8ed56c37d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate without subjectOf", + "description": "An RO-Crate that references another RO-Crate missing the subjectOf property.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate without subjectOf.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ] + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json new file mode 100644 index 000000000..5b27f6654 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json @@ -0,0 +1,53 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate without version-less conformsTo", + "description": "An RO-Crate that references another RO-Crate missing the version-less profile URI.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate missing the version-less profile URI.", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json new file mode 100644 index 000000000..334633225 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json @@ -0,0 +1,66 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with version-less conformsTo on Root", + "description": "An RO-Crate whose Root Data Entity incorrectly declares the version-less generic profile URI.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0b3cb6a54 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate", + "description": "An RO-Crate that references another RO-Crate as a data entity.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "https://example.org/other-ro-crate/" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate.", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "subjectOf": { + "@id": "#other-ro-crate-metadata" + }, + "sdDatePublished": "2024-01-01", + "distribution": { + "@id": "https://example.org/other-ro-crate/archive.tar.gz" + } + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://example.org/other-ro-crate/archive.tar.gz", + "@type": "DataDownload", + "name": "Other RO-Crate Archive", + "encodingFormat": "application/gzip" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json new file mode 100644 index 000000000..c62ba3c7f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate with declared identifier", + "description": "An RO-Crate referencing another RO-Crate whose @id is a declared persistent identifier (case #1): no sdDatePublished required.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "https://example.org/persistent-crate/" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://example.org/persistent-crate/", + "@type": "Dataset", + "name": "Other RO-Crate with persistent identifier", + "description": "A referenced RO-Crate whose @id IS a declared persistent identifier (DOI).", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "identifier": "10.5281/zenodo.1234567", + "subjectOf": { + "@id": "#other-ro-crate-metadata" + }, + "distribution": { + "@id": "https://example.org/persistent-crate//archive.zip" + } + }, + { + "@id": "https://example.org/persistent-crate//archive.zip", + "@type": "DataDownload", + "name": "Other RO-Crate Archive", + "encodingFormat": "application/zip" + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json new file mode 100644 index 000000000..5a78a58fe --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Attached crate referencing a nested RO-Crate by relative path", + "description": "An attached RO-Crate (A) referencing another nested RO-Crate (B) via a relative path (case #2): no sdDatePublished required.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "subcrate/" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subcrate/", + "@type": "Dataset", + "name": "Nested RO-Crate", + "description": "A referenced RO-Crate nested within A, identified by relative path.", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "hasPart": { + "@id": "subcrate/data.txt" + }, + "subjectOf": { + "@id": "#subcrate-metadata" + } + }, + { + "@id": "subcrate/data.txt", + "@type": "File", + "name": "Nested data file", + "description": "A data file inside the nested RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": "0" + }, + { + "@id": "#subcrate-metadata", + "@type": "CreativeWork", + "name": "Nested RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/subcrate/data.txt b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/subcrate/data.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py new file mode 100644 index 000000000..66e5c501e --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py @@ -0,0 +1,243 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates_v1_3 import ReferencedROCrates +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__referenced_rocrate_crates__ = ReferencedROCrates() + + +def test_valid_referenced_rocrate(): + """ + A crate referencing another RO-Crate with all recommended properties + SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __referenced_rocrate_crates__.valid, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_40.0", "ro-crate-1.3_46.1", "ro-crate-1.3_72.1", "ro-crate-1.3_75.1"], + ) + + +def test_invalid_referenced_rocrate_no_versionless_conformsto(): + """ + A referenced RO-Crate data entity missing the version-less conformsTo + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_no_versionless_conformsto, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate: SHOULD include version-less profile in conformsTo"], + expected_triggered_issues=["version-less"], + ) + + +def test_invalid_root_conformsto_versionless(): + """ + The Root Data Entity MUST NOT declare the version-less conformsTo profile URI. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_root_conformsto_versionless, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Root Data Entity: MUST NOT declare version-less conformsTo profile"], + expected_triggered_issues=["version-less"], + ) + + +def test_invalid_referenced_rocrate_no_subjectof(): + """ + A referenced RO-Crate data entity missing subjectOf SHOULD trigger + a RECOMMENDED warning. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_no_subjectof, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate: SHOULD have subjectOf"], + expected_triggered_issues=["subjectOf"], + ) + + +def test_invalid_referenced_rocrate_md_encoding_format(): + """ + A referenced RO-Crate metadata descriptor with wrong encodingFormat + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_md_encoding_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate metadata descriptor: recommended properties"], + expected_triggered_issues=["encodingFormat"], + ) + + +def test_invalid_referenced_rocrate_md_conformsto(): + """ + A referenced RO-Crate metadata descriptor with conformsTo SHOULD trigger + a RECOMMENDED warning (conformsTo belongs on the data entity). + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_md_conformsto, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate metadata descriptor: recommended properties"], + expected_triggered_issues=["conformsTo"], + ) + + +def test_invalid_referenced_rocrate_md_about(): + """ + A referenced RO-Crate metadata descriptor with about SHOULD trigger + a RECOMMENDED warning (about belongs on the crate's own metadata descriptor). + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_md_about, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate metadata descriptor: recommended properties"], + expected_triggered_issues=["about"], + ) + + +def test_invalid_referenced_rocrate_missing_sddatepublished(): + """ + A referenced RO-Crate data entity whose @id is an absolute URI not declared + as a persistent identifier SHOULD include `sdDatePublished`; omitting it + triggers a RECOMMENDED warning (RO-Crate 1.3, § 4.5). + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_missing_sddatepublished, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=[ + "Referenced RO-Crate: SHOULD have sdDatePublished when @id is an absolute URI without persistent identifier" + ], + expected_triggered_issues=["SHOULD include `sdDatePublished` to indicate when the URI was accessed"], + ) + + +def test_valid_referenced_rocrate_with_identifier(): + """ + A referenced RO-Crate with a declared `identifier` (case #1) does NOT + require `sdDatePublished`; the structural SHACL check should NOT fire. + """ + do_entity_test( + __referenced_rocrate_crates__.valid_with_identifier, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_40.0", "ro-crate-1.3_46.1", "ro-crate-1.3_72.1", "ro-crate-1.3_75.1"], + ) + + +def test_valid_referenced_rocrate_with_relative_path(): + """ + A referenced RO-Crate with a relative path @id (case #2: attached nested + sub-crate) does NOT require `sdDatePublished`; the structural SHACL check + should NOT fire on relative path @ids. + """ + do_entity_test( + __referenced_rocrate_crates__.valid_with_relative_path, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_40.0", "ro-crate-1.3_46.1", "ro-crate-1.3_72.1", "ro-crate-1.3_75.1"], + ) + + +def test_invalid_referenced_rocrate_no_cite_as_signposting(monkeypatch): + """ + Python network-aware refinement: when the referenced RO-Crate @id does NOT + declare Signposting `Link: rel="cite-as"`, `sdDatePublished` SHOULD be + present. Mocked HEAD response returns no `cite-as` link. + """ + + class _NoCiteAsResponse: + status_code = 200 + headers = {"Content-Type": "text/html"} + links = {} + + def raise_for_status(self): + pass + + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _NoCiteAsResponse()) + + do_entity_test( + __referenced_rocrate_crates__.invalid_missing_sddatepublished, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + expected_triggered_requirements=["Referenced RO-Crate: Signposting cite-as refinement for sdDatePublished"], + expected_triggered_issues=['has no Signposting `Link: rel="cite-as"` declared'], + ) + + +def test_valid_referenced_rocrate_with_cite_as_signposting(monkeypatch): + """ + Python network-aware refinement: when the referenced RO-Crate @id DOES + declare Signposting `Link: rel="cite-as"`, `sdDatePublished` is NOT + required; the Python check suppresses its warning. The structural SHACL + warning still fires (it cannot inspect network headers). + """ + + class _CiteAsResponse: + status_code = 200 + headers = {"Content-Type": "text/html"} + links = { + "cite-as": { + "url": "https://doi.org/10.5281/zenodo.1234567", + "rel": "cite-as", + } + } + + def raise_for_status(self): + pass + + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _CiteAsResponse()) + + # Python check passes (cite-as present); SHACL structural shape still + # warns. We verify the Python-specific requirement is NOT triggered. + from rocrate_validator import services + + result = services.validate( + { + "rocrate_uri": str(__referenced_rocrate_crates__.invalid_missing_sddatepublished), + "profile_identifier": "ro-crate-1.3", + "requirement_severity": models.Severity.RECOMMENDED, + "enforce_availability": True, + } + ) + failed_requirement_names = {issue.check.requirement.name for issue in result.get_issues()} + assert "Referenced RO-Crate: Signposting cite-as refinement for sdDatePublished" not in failed_requirement_names, ( + "Python cite-as refinement should NOT fire when cite-as is declared" + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 7f237be99..cac5f7635 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -712,6 +712,50 @@ def encoding_format_no_fragment(self) -> Path: return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "no_fragment" +class ReferencedROCrates: + REFERENCED_ROCRATES_CRATES_PATH = CRATES_DATA_PATH / "rocrate-1.3" / "9_referenced_rocrate" + + @property + def valid(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "valid" + + @property + def invalid_no_versionless_conformsto(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_no_versionless_conformsto" + + @property + def invalid_root_conformsto_versionless(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_root_conformsto_versionless" + + @property + def invalid_no_subjectof(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_no_subjectof" + + @property + def invalid_md_encoding_format(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_md_encoding_format" + + @property + def invalid_md_conformsto(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_md_conformsto" + + @property + def invalid_md_about(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_md_about" + + @property + def invalid_missing_sddatepublished(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_missing_sddatepublished" + + @property + def valid_with_identifier(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "valid_with_identifier" + + @property + def valid_with_relative_path(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "valid_with_relative_path" + + class ValidROCrate13: base_path = VALID_CRATES_DATA_PATH From 08d317e1aae9377daed99c43505ca069da580798 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 23 Sep 2026 10:19:56 +0200 Subject: [PATCH 095/271] =?UTF-8?q?perf(validation):=20=E2=9A=A1=EF=B8=8F?= =?UTF-8?q?=20reuse=20prepared=20profiles=20across=20crate=20bases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 52 ++++++++--- .../requirements/shacl/validator.py | 22 ++++- rocrate_validator/utils/rdf.py | 52 ++++++++++- tests/unit/test_warm_start.py | 88 ++++++++++++++++++- 4 files changed, 197 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index e02fa0563..500ce2f79 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -50,7 +50,7 @@ from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import find_offline_cache_miss -from rocrate_validator.utils.rdf import extract_base_from_jsonld +from rocrate_validator.utils.rdf import PREPARED_PROFILE_BASE, extract_base_from_jsonld from rocrate_validator.utils.uri import URI if TYPE_CHECKING: @@ -60,7 +60,7 @@ @dataclass(frozen=True) class PreparedValidationPlan: """ - Profile state that can be reused by validation runs with the same base. + Profile state reusable by validation runs with compatible RDF bases. Profiles own their lazily loaded requirements and shape registries, so keeping the same profile instances avoids reparsing those artifacts on every run. The @@ -102,9 +102,9 @@ def __init__(self, settings: dict | ValidationSettings): super().__init__() # initialize the current context self.__current_context__: ValidationContext | None = None - # Profile artifacts may contain relative IRIs resolved against the crate - # public ID, so plans are cached by every setting that affects profile - # preparation, including that effective base. + # Ordinary profile artifacts are prepared against a stable internal base + # and materialized per run. An explicit JSON-LD @base different from the + # crate public ID remains base-specific. self.__prepared_profile_catalogs: dict[tuple[Any, ...], tuple[Profile, ...]] = {} self.__prepared_validation_plans: dict[tuple[Any, ...], PreparedValidationPlan] = {} # Publisher state and ``__current_context__`` are instance-local and @@ -121,7 +121,7 @@ def __prepared_profile_catalog_key__(context: ValidationContext) -> tuple[Any, . return ( context.profiles_path.resolve(), context.extra_profiles_path.resolve() if context.extra_profiles_path else None, - context.publicID, + context.prepared_profile_base, context.requirement_severity, context.allow_requirement_check_override, ) @@ -134,7 +134,7 @@ def __get_prepared_profile_catalog__(self, context: ValidationContext) -> tuple[ Profile.load_profiles( context.profiles_path, extra_profiles_path=context.extra_profiles_path, - publicID=context.publicID, + publicID=context.prepared_profile_base, severity=context.requirement_severity, allow_requirement_check_override=context.allow_requirement_check_override, ) @@ -147,8 +147,8 @@ def __prepared_validation_plan_key__(context: ValidationContext) -> tuple[Any, . return ( context.profiles_path.resolve(), context.extra_profiles_path.resolve() if context.extra_profiles_path else None, - context.publicID, - context.effective_ontology_base, + context.prepared_profile_base, + context.prepared_ontology_base, context.profile_identifier, context.requirement_severity, context.inheritance_enabled, @@ -172,7 +172,7 @@ def __get_prepared_validation_plan__(self, context: ValidationContext) -> Prepar ontology_graph.parse( ontology_path, format="ttl", - publicID=context.effective_ontology_base, + publicID=context.prepared_ontology_base, ) plan = PreparedValidationPlan(profiles, ontology_graph) self.__prepared_validation_plans[key] = plan @@ -514,6 +514,7 @@ def __init__(self, validator: Validator, settings: ValidationSettings): self._profiles: list[Profile] | None = None # reference to the immutable profile preparation selected for this run self._prepared_validation_plan: PreparedValidationPlan | None = None + self._effective_ontology_base: str | None = None # reference to the target profile self._target_validation_profile: Profile | None = None # reference to the validation result @@ -607,15 +608,42 @@ def publicID(self) -> str: def effective_ontology_base(self) -> str: """Base used while parsing profile ontology artifacts for this run.""" + if self._effective_ontology_base is not None: + return self._effective_ontology_base try: - return extract_base_from_jsonld(self.ro_crate.metadata.as_dict()) or self.publicID + self._effective_ontology_base = extract_base_from_jsonld(self.ro_crate.metadata.as_dict()) or self.publicID except (ROCrateMetadataNotFoundError, json.JSONDecodeError, UnicodeDecodeError) as error: # Profile preparation happens before descriptor checks execute. An # unreadable descriptor is validation input, not a preparation # failure: use the crate public ID and let the owning checks report # the precise missing/JSON/encoding issue in the normal pipeline. logger.debug("Unable to read metadata @base while preparing profiles: %s", error) - return self.publicID + self._effective_ontology_base = self.publicID + return self._effective_ontology_base + + @property + def prepared_profile_base(self) -> str: + """Stable RDF base used to prepare reusable SHACL profile artifacts.""" + + if self.effective_ontology_base == self.publicID: + return PREPARED_PROFILE_BASE + return self.publicID + + @property + def prepared_ontology_base(self) -> str: + """Stable RDF base preserving whether ontology and crate bases differ.""" + + if self.effective_ontology_base == self.publicID: + return self.prepared_profile_base + return self.effective_ontology_base + + @property + def prepared_base_mappings(self) -> tuple[tuple[str, str], ...]: + """Real-to-canonical base mappings for the per-run SHACL data view.""" + + mappings = {self.publicID: self.prepared_profile_base} + mappings[self.effective_ontology_base] = self.prepared_ontology_base + return tuple(sorted(mappings.items(), key=lambda item: len(item[0]), reverse=True)) @property def profiles_path(self) -> Path: diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index a4e74256a..f39b4302c 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -19,6 +19,7 @@ import pyshacl from rdflib import BNode, Graph, Literal, Namespace +from rdflib.namespace import RDF from rdflib.term import Node, URIRef if TYPE_CHECKING: @@ -49,6 +50,7 @@ ) from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging +from rocrate_validator.utils.rdf import rebase_graph # set up logging logger = logging.getLogger(__name__) @@ -136,8 +138,14 @@ def __init__(self, context: ValidationContext): # pySHACL may mutate supplied graphs while applying inference/rules. # Copy the prepared ontology into this run's working graph. - self._ontology_graph: Graph = Graph() - self._ontology_graph += context.prepared_validation_plan.ontology_graph + run_base_mappings = tuple( + (prepared, actual) for actual, prepared in context.prepared_base_mappings if actual != prepared + ) + self._run_base_mappings = run_base_mappings + self._ontology_graph = rebase_graph( + context.prepared_validation_plan.ontology_graph, + self._run_base_mappings, + ) def __set_current_validation_profile__(self, profile: Profile) -> bool: """ @@ -161,6 +169,16 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: profile_registry = ShapesRegistry.get_instance(profile) profile_shapes = profile_registry.get_shapes() profile_shapes_graph = profile_registry.shapes_graph + structural_nodes = { + subject + for subject, predicate, object_ in profile_shapes_graph + if str(predicate).startswith(SHACL_NS) or (predicate == RDF.type and str(object_).startswith(SHACL_NS)) + } + profile_shapes_graph = rebase_graph( + profile_shapes_graph, + self._run_base_mappings, + preserve_nodes=structural_nodes, + ) logger.debug("Loaded shapes: %s", profile_shapes) # Filter shapes that must not participate in the combined SHACL run. diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 6e4f1af12..5b638efbe 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -12,7 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rdflib import Graph +from collections.abc import Iterable + +from rdflib import Graph, URIRef +from rdflib.term import Node from rocrate_validator import constants from rocrate_validator.utils import log as logging @@ -22,6 +25,53 @@ logger = logging.getLogger(__name__) +PREPARED_PROFILE_BASE = "https://example.invalid/rocrate-validator/prepared/crate/" + + +def rebase_node(node: Node, base_mappings: Iterable[tuple[str, str]]) -> Node: + """ + Return ``node`` with the first matching URI base replaced. + + Mappings must be ordered from the most specific source base to the least + specific one. Blank nodes and literals are unchanged. + """ + + if isinstance(node, URIRef): + value = str(node) + for source_base, target_base in base_mappings: + if value.startswith(source_base): + return URIRef(f"{target_base}{value[len(source_base) :]}") + return node + return node + + +def rebase_graph( + graph: Graph, + base_mappings: Iterable[tuple[str, str]], + *, + preserve_nodes: Iterable[Node] = (), +) -> Graph: + """ + Copy a graph while replacing selected URI bases. + + ``preserve_nodes`` keeps structural identifiers stable while their constraint + values are rebased (for example SHACL shape identifiers versus target nodes). + """ + + mappings = tuple(base_mappings) + preserved = frozenset(preserve_nodes) + + def transform(node: Node) -> Node: + return node if node in preserved else rebase_node(node, mappings) + + rebased = Graph() + for prefix, namespace in graph.namespaces(): + rebased.bind(prefix, namespace) + for subject, predicate, object_ in graph: + rebased.add((transform(subject), transform(predicate), transform(object_))) + return rebased + + def get_full_graph( graphs_dir: str, serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle", publicID: str = "." ) -> Graph: diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index bbc10a676..2fe25af85 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -19,7 +19,7 @@ from pathlib import Path import pytest -from rdflib import Graph, URIRef +from rdflib import Graph, Namespace, URIRef from rdflib.compare import isomorphic from rocrate_validator.errors import ValidationExecutionError @@ -82,6 +82,90 @@ def counted_load_shapes(self, *args, **kwargs): assert calls == cold_calls +def test_prepared_profiles_are_reused_across_crate_public_ids(monkeypatch): + """Different crate bases share one canonically prepared profile plan.""" + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + original_uri = URI(ValidROC().wrroc_paper) + settings = _settings(ValidROC().wrroc_paper) + validator = Validator(settings) + + first_result = validator.validate() + second_result = validator.validate(ValidROC().wrroc_paper_long_date) + + assert calls == 1 + assert settings.rocrate_uri == original_uri + assert first_result.rocrate_uri != second_result.rocrate_uri + shacl = Namespace("http://www.w3.org/ns/shacl#") + first_target = next( + ShapesRegistry.get_instance(first_result.context.profiles[-1]).shapes_graph.objects(predicate=shacl.targetNode) + ) + second_target = next( + ShapesRegistry.get_instance(second_result.context.profiles[-1]).shapes_graph.objects(predicate=shacl.targetNode) + ) + assert first_result.context.prepared_validation_plan is second_result.context.prepared_validation_plan + assert first_target == second_target + assert str(first_target).startswith(first_result.context.prepared_profile_base) + + +def test_cross_base_reuse_keeps_each_crates_data_and_public_output_isolated(tmp_path, monkeypatch): + profile_loads = 0 + shape_loads = 0 + original_profile_load = Profile.load_profiles + original_shape_load = ShapesRegistry.load_shapes + + def counted_profile_load(cls, *args, **kwargs): + nonlocal profile_loads + profile_loads += 1 + return original_profile_load(*args, **kwargs) + + def counted_shape_load(self, *args, **kwargs): + nonlocal shape_loads + shape_loads += 1 + return original_shape_load(self, *args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_profile_load)) + monkeypatch.setattr(ShapesRegistry, "load_shapes", counted_shape_load) + + context = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + } + valid_metadata = { + "@context": context, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + ], + } + invalid_metadata = copy.deepcopy(valid_metadata) + invalid_metadata["@graph"][0].pop("@type") + crate_a = tmp_path / "crate-a" + crate_b = tmp_path / "crate-b" + for crate, metadata in ((crate_a, valid_metadata), (crate_b, invalid_metadata)): + crate.mkdir() + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + + validator = Validator(_settings(crate_a)) + valid_result = validator.validate() + invalid_result = validator.validate(crate_b) + + assert valid_result.passed() + assert not invalid_result.passed() + assert valid_result.context.publicID != invalid_result.context.publicID + assert valid_result.context.prepared_validation_plan is invalid_result.context.prepared_validation_plan + assert profile_loads == 1 + assert shape_loads > 0 + assert all(issue.violatingEntity == "./ro-crate-metadata.json" for issue in invalid_result.get_issues()) + + def test_per_call_metadata_does_not_mutate_validator_settings(): crate = ValidROC().wrroc_paper with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: @@ -257,7 +341,7 @@ def counted_parse(self, source=None, *args, **kwargs): for profile in first_plan.profiles: ontology_path = profile.path / "ontology.ttl" if ontology_path.exists(): - original_parse(expected, ontology_path, format="ttl", publicID=first_context.effective_ontology_base) + original_parse(expected, ontology_path, format="ttl", publicID=first_context.prepared_ontology_base) assert ontology_parses == 2 assert first_plan is second_plan From 4c88ced7124ec4cea2caae45d1eacb85489c609d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 23 Sep 2026 10:34:25 +0200 Subject: [PATCH 096/271] =?UTF-8?q?docs(validation):=20=F0=9F=93=9D=20docu?= =?UTF-8?q?ment=20reusable=20validation=20lifecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++++++++++++++++ docs/10_api.rst | 3 +++ docs/3_usage_api.rst | 47 +++++++++++++++++++++++++++++++++++++++++ docs/4_how_it_works.rst | 28 ++++++++++++++++++++++++ 4 files changed, 99 insertions(+) diff --git a/README.md b/README.md index a0d0502fe..e8579df8c 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,27 @@ else: print(f"Detected issue of severity {issue.severity.name} with check \"{issue.check.identifier}\": {issue.message}") ``` +For repeated validation against the same profile, reuse a `Validator`. Profile +artifacts are prepared on the first call (or explicitly with `prepare()`) while +crate data and results remain isolated per call: + +```python +validator = models.Validator(settings) +validator.prepare() # optional eager warm-up + +result_a = validator.validate("/path/to/ro-crate-a") +result_b = validator.validate("/path/to/ro-crate-b") + +# Invalidate preparation after changing profile files on disk. +validator.clear_prepared_profiles() +``` + +For ordinary crates, profile artifacts containing relative IRIs are prepared +once against an internal base and materialized for each crate public ID. A +metadata document whose explicit JSON-LD `@base` differs from its crate public +ID keeps a separate prepared plan. Calls on the same `Validator` are serialized; +use one instance per concurrent worker. + The following is a possible output: ```bash diff --git a/docs/10_api.rst b/docs/10_api.rst index 9fc33b470..cf09896e7 100644 --- a/docs/10_api.rst +++ b/docs/10_api.rst @@ -104,6 +104,9 @@ Severity Validation ----------------------------------------- +.. autoclass:: rocrate_validator.models.Validator + :members: + .. autoclass:: rocrate_validator.models.ValidationSettings :members: diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index ad034a9eb..801fb0e0a 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -69,6 +69,53 @@ representing the metadata and validates it against a given validation profile. ... +Warm validation with a reusable Validator +----------------------------------------- + +Applications that validate repeatedly against the same profile can keep a +single :class:`rocrate_validator.models.Validator` instance. The first call +prepares the selected profile chain, requirements, SHACL shapes, and ontology; +later calls reuse that preparation when the profile settings and RDF-base mode +match. Ordinary crates at different locations therefore share one preparation. + +.. code-block:: python + + from rocrate_validator.models import ValidationSettings, Validator + + settings = ValidationSettings( + rocrate_uri="/path/to/default-crate", + profile_identifier="my-custom-profile", + extra_profiles_path="/path/to/custom-profiles", + ) + validator = Validator(settings) + + # Optional: move profile preparation out of the first measured request. + validator.prepare() + + default_result = validator.validate() # existing API remains supported + crate_a_result = validator.validate("/path/to/crate-a") + crate_b_result = validator.validate("/path/to/crate-b") + +The per-call crate argument does not mutate ``settings.rocrate_uri``. Metadata +dictionaries can be supplied with ``validator.validate(metadata_dict=data)``; +``rocrate_uri`` and ``metadata_dict`` are mutually exclusive in one call. + +Every call creates a fresh data graph, result, statistics updates, and SHACL +execution context. The same validator serializes overlapping calls because its +subscriber and current-context state are instance-local; use separate validator +instances when validations must run concurrently. + +For metadata without an explicit JSON-LD ``@base`` distinct from the crate +public ID, relative IRIs in profile artifacts are compiled against a stable +internal base. Per-run copies of the SHACL and ontology graphs rebase only those +relative terms to the current crate, while structural shape identifiers and the +crate data graph remain unchanged. Metadata whose explicit ``@base`` differs +from its crate public ID retains a base-specific prepared plan. After editing +profile files in a long-running process, call +``validator.clear_prepared_profiles()`` or ``validator.prepare(refresh=True)`` +before the next validation. + + Formatting Validation Results ----------------------------- diff --git a/docs/4_how_it_works.rst b/docs/4_how_it_works.rst index 7801eaed1..876f0775d 100644 --- a/docs/4_how_it_works.rst +++ b/docs/4_how_it_works.rst @@ -36,6 +36,34 @@ Validation profiles can be related by inheritance -- i.e., where one validation profile extends another one. For instance, Workflow Testing RO-Crate extends Workflow RO-Crate. +Validation lifecycle and prepared profiles +------------------------------------------ + +An instance of :class:`rocrate_validator.models.Validator` keeps the profile +preparation state used by its validation runs. This state contains the resolved +profile chain and the profile objects which lazily own requirements and SHACL +shape registries. Repeated calls on the same validator can therefore reuse +parsed profile artifacts instead of loading them again. + +Crate-specific state is never part of the prepared profile state. Every run +still creates a new validation context, data graph, result, and SHACL execution +context, so inference, SHACL rules, violations, and skipped checks cannot leak +from one run into another. Validation statistics use the same prepared profile +instances as their owning validation context rather than loading a second copy. + +Profile artifacts can contain relative IRIs. For the ordinary case where the +effective JSON-LD base is the crate public ID, profiles are prepared once using +a stable internal base. Each SHACL run copies the much smaller prepared shapes +and ontology graphs, rebases their relative data terms to the current crate, +and preserves structural shape identifiers. The crate data graph itself is +never rewritten. This permits different crate locations to share preparation +without leaking their public IDs into one another. + +An explicit JSON-LD ``@base`` different from the crate public ID retains a +base-specific prepared plan. This conservative boundary preserves the two RDF +spaces used by existing profiles rather than collapsing them during rebasing. + + Validation profile selection ---------------------------- From cc1f15f844de2ab9947d0ecfdd3b467e78a46a00 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 23 Sep 2026 10:47:22 +0200 Subject: [PATCH 097/271] rocrate-1.3: add test_availability_flags --- .../test_availability_flags_v1_3.py | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py diff --git a/tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py new file mode 100644 index 000000000..82da82cb3 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py @@ -0,0 +1,212 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models, services +from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.uri import URI +from tests.ro_crates_v1_3 import ValidROCrate13 + +logger = logging.getLogger(__name__) + + +valid = ValidROCrate13() + +# Minimal set of JSON-LD context keys needed to pass `check_compaction` +# for the test crates used in this module. +_FAKE_CONTEXT_KEYS = { + "about", + "affiliation", + "author", + "cite-as", + "conformsTo", + "funder", + "contentLocation", + "contentSize", + "contentUrl", + "dateCreated", + "dateModified", + "datePublished", + "description", + "encodingFormat", + "hasPart", + "identifier", + "license", + "name", + "propertyID", + "publisher", + "sdDatePublished", + "url", + "value", + "contactPoint", + "contactType", + "email", +} + + +class _FakeContextResponse: + """Minimal HTTP response that satisfies `FileDescriptorJsonLdFormat` checks.""" + + status_code = 200 + headers = {"Content-Type": "application/ld+json"} + + def json(self): + # Map each known key explicitly so `check_compaction` finds them, and add a + # schema.org default vocabulary so entity *types* (Dataset, File, Person, + # Organization, CreativeWork, ...) expand correctly during validation even + # though they are not listed individually. Without `@vocab`, types do not + # resolve to schema.org and the crate fails REQUIRED structural checks. + context = {k: f"http://schema.org/{k}" for k in _FAKE_CONTEXT_KEYS} + context["@vocab"] = "http://schema.org/" + # `conformsTo` is a Dublin Core term in the real RO-Crate context; the + # file-descriptor checks query `dct:conformsTo`, so it must not fall back + # to the schema.org default vocabulary. + context["conformsTo"] = {"@id": "http://purl.org/dc/terms/conformsTo", "@type": "@id"} + return {"@context": context} + + +def _fake_context_get(url, *args, **kwargs): + """Return a fake JSON-LD context for w3id.org; reject anything else.""" + if "w3id.org" in url: + return _FakeContextResponse() + raise RuntimeError(f"Unexpected GET request in test: {url}") + + +def _validate_with_settings(**kwargs): + return services.validate( + models.ValidationSettings( + rocrate_uri=URI(valid.attached_absolute_root), + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.RECOMMENDED, + **kwargs, + ) + ) + + +def _availability_messages(result): + return [ + issue.message + for issue in result.get_issues(models.Severity.RECOMMENDED) + if "Web-based Data Entity" in issue.message + ] + + +def _patch_unavailable(monkeypatch): + """Make every HEAD request fail (simulates unreachable web entities) + and return a fake JSON-LD context for GET requests to avoid proxy errors.""" + + def fake_head(url, *args, **kwargs): + raise RuntimeError("Not downloadable") + + monkeypatch.setattr(HttpRequester(), "head", fake_head) + monkeypatch.setattr(HttpRequester(), "get", _fake_context_get) + + +def test_default_availability_warns(monkeypatch): + _patch_unavailable(monkeypatch) + result = _validate_with_settings() + # REQUIRED checks must pass; the unavailable entity only raises a RECOMMENDED warning + assert result.passed(models.Severity.REQUIRED) + messages = _availability_messages(result) + assert messages, "Expected availability warnings for web-based data entities" + + +def test_creation_time_enforces_availability(monkeypatch): + _patch_unavailable(monkeypatch) + result = _validate_with_settings(creation_time=True) + assert not result.passed() + messages = _availability_messages(result) + assert messages, "Expected availability violations at creation time" + + +def test_enforce_availability_flag(monkeypatch): + _patch_unavailable(monkeypatch) + result = _validate_with_settings(enforce_availability=True) + assert not result.passed() + messages = _availability_messages(result) + assert messages, "Expected availability violations when enforced" + + +def test_skip_availability_check(monkeypatch): + monkeypatch.setattr(HttpRequester(), "get", _fake_context_get) + result = _validate_with_settings(skip_availability_check=True) + assert result.passed() + messages = _availability_messages(result) + assert not messages, "Availability warnings should be skipped" + + +def test_content_size_warning(monkeypatch): + class FakeResponse: + def __init__(self, status_code=200, content_length="10"): + self.status_code = status_code + self.headers = {"Content-Length": content_length} + + def raise_for_status(self): + if self.status_code >= 400: + raise RuntimeError("HTTP error") + + def fake_head(url, *args, **kwargs): + if url.endswith("/file.txt"): + return FakeResponse(content_length="10") + if url.endswith("/content.txt"): + raise RuntimeError("Not downloadable") + return FakeResponse(content_length="10") + + monkeypatch.setattr(HttpRequester(), "head", fake_head) + monkeypatch.setattr(HttpRequester(), "get", _fake_context_get) + + metadata_dict = { + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"}, + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Availability test", + "description": "Content size and contentUrl warnings.", + "license": {"@id": "http://spdx.org/licenses/CC0-1.0"}, + "datePublished": "2024-01-01", + "hasPart": [{"@id": "https://example.org/file.txt"}], + }, + { + "@id": "https://example.org/file.txt", + "@type": "File", + "name": "file.txt", + "description": "Remote file.", + "encodingFormat": "text/plain", + "contentSize": "5", + "contentUrl": "https://example.org/content.txt", + }, + ], + } + + result = services.validate( + models.ValidationSettings( + rocrate_uri=URI("."), + metadata_dict=metadata_dict, + metadata_only=True, + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.RECOMMENDED, + ) + ) + + messages = [issue.message for issue in result.get_issues(models.Severity.RECOMMENDED) if issue.message] + assert any("contentSize" in message for message in messages) + assert any("contentUrl" in message for message in messages) From 56f9880d51c2b1d38beaec426f4a456971dba0a6 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 23 Sep 2026 11:04:02 +0200 Subject: [PATCH 098/271] rocrate-1.3: add test_ro_crate_1_3 --- .../ro-crate-metadata.json | 37 +++++++ .../dataset-ro-crate-metadata.json | 37 +++++++ .../ro-crate-1.3/invalid-context/data.txt | 1 + .../invalid-context/ro-crate-metadata.json | 37 +++++++ .../invalid-date-published/data.txt | 1 + .../ro-crate-metadata.json | 37 +++++++ .../ro-crate-1.3/test_ro_crate_1_3.py | 103 ++++++++++++++++++ tests/ro_crates_v1_3.py | 20 ++++ 8 files changed, 273 insertions(+) create mode 100644 tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt create mode 100644 tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt create mode 100644 tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py diff --git a/tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json new file mode 100644 index 000000000..8b72c5c26 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached" + } + }, + { + "@id": "https://example.org/ro-crate/detached", + "@type": "Dataset", + "name": "Detached RO-Crate filename", + "description": "Detached RO-Crate with discouraged filename.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "https://example.org/data/file1.txt" + } + ] + }, + { + "@id": "https://example.org/data/file1.txt", + "@type": "File", + "name": "file1.txt", + "description": "Remote file in detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json new file mode 100644 index 000000000..77554ef38 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached" + } + }, + { + "@id": "https://example.org/ro-crate/detached", + "@type": "Dataset", + "name": "Detached RO-Crate invalid", + "description": "Detached crate with relative data entity.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Relative file in detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt @@ -0,0 +1 @@ +hello diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json new file mode 100644 index 000000000..96387ffdd --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid context", + "description": "Uses 1.1 context.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Example data file.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt @@ -0,0 +1 @@ +hello diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json new file mode 100644 index 000000000..69cdf6b37 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid datePublished", + "description": "datePublished is an array.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": ["2024-01-01", "2024-01-02"], + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Example data file.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py b/tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py new file mode 100644 index 000000000..12958b18a --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py @@ -0,0 +1,103 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import InvalidROCrate13, ValidROCrate13 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +paths = InvalidROCrate13() +valid = ValidROCrate13() + + +def test_valid_attached_ro_crate_1_3(): + do_entity_test( + valid.attached, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_attached_ro_crate_absolute_root_id(): + do_entity_test( + valid.attached_absolute_root, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_context_version(): + do_entity_test( + paths.invalid_context, + models.Severity.REQUIRED, + False, + ["File Descriptor JSON-LD format"], + ["does not reference the required context"], + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_date_published_single_value(): + do_entity_test( + paths.invalid_date_published, + models.Severity.REQUIRED, + False, + ["RO-Crate Root Data Entity REQUIRED properties"], + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_detached_ro_crate(): + do_entity_test( + valid.detached, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_detached_prefixed_metadata_filename(): + do_entity_test( + valid.detached_prefixed, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_detached_relative_entity(): + do_entity_test( + paths.detached_relative_entity, + models.Severity.REQUIRED, + False, + ["Detached RO-Crate: data entities MUST be web-based"], + profile_identifier="ro-crate-1.3", + ) + + +def test_detached_bad_filename_recommended(): + do_entity_test( + paths.detached_bad_filename, + models.Severity.RECOMMENDED, + False, + ["File Descriptor naming convention"], + ["metadata descriptor filename"], + profile_identifier="ro-crate-1.3", + ) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index cac5f7635..4c393b79c 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -774,3 +774,23 @@ def detached(self) -> Path: @property def detached_prefixed(self) -> Path: return self.base_path / "detached-1.3" / "test-ro-crate-metadata.json" + + +class InvalidROCrate13: + base_path = INVALID_CRATES_DATA_PATH / "ro-crate-1.3" + + @property + def invalid_context(self) -> Path: + return self.base_path / "invalid-context" + + @property + def invalid_date_published(self) -> Path: + return self.base_path / "invalid-date-published" + + @property + def detached_relative_entity(self) -> Path: + return self.base_path / "detached-relative-entity" / "dataset-ro-crate-metadata.json" + + @property + def detached_bad_filename(self) -> Path: + return self.base_path / "detached-bad-filename" / "ro-crate-metadata.json" From e92e826bd2b2e49b36775f5150c81f50089439fb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 14:53:03 +0200 Subject: [PATCH 099/271] =?UTF-8?q?feat(core):=20=F0=9F=A7=A9=20support=20?= =?UTF-8?q?profile=20rule=20overlays=20by=20check=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 4 +- rocrate_validator/models/events.py | 27 ++++++++++ rocrate_validator/models/profile.py | 49 +++++++++++++++---- rocrate_validator/models/requirement.py | 6 ++- rocrate_validator/models/result.py | 29 ++++++++++- rocrate_validator/models/skipped_check.py | 18 +++++-- rocrate_validator/models/statistics.py | 20 ++++++-- rocrate_validator/models/validation.py | 33 +++++++++++++ .../requirements/python/__init__.py | 2 +- .../requirements/shacl/checks.py | 2 + 10 files changed, 166 insertions(+), 24 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index e0d8ad010..7376d5929 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -719,8 +719,8 @@ def _print_skipped_checks(results: dict[str, ValidationResult], console: Console table.add_column("Reason") for detail in details: table.add_row( - Text(detail.check.requirement.profile.identifier, style="cyan"), - Text(detail.check.identifier, style=get_severity_color(detail.check.severity)), + Text(detail.to_dict()["profile"], style="cyan"), + Text(detail.to_dict()["identifier"], style=get_severity_color(detail.check.severity)), Text(detail.category.value, style="yellow"), Text(f"{detail.check.name}: {detail.message}"), ) diff --git a/rocrate_validator/models/events.py b/rocrate_validator/models/events.py index 2b8156360..61d6b4dc2 100644 --- a/rocrate_validator/models/events.py +++ b/rocrate_validator/models/events.py @@ -133,6 +133,8 @@ def __init__( super().__init__(event_type, message) self._requirement_check = requirement_check self._validation_result = validation_result + self._effective_identifier = requirement_check.identifier + self._effective_profile_identifier = requirement_check.requirement.profile.identifier @property def requirement_check(self) -> RequirementCheck: @@ -142,6 +144,31 @@ def requirement_check(self) -> RequirementCheck: def validation_result(self) -> CheckResultValue: return self._validation_result + @property + def effective_identifier(self) -> str: + """Identifier exposed for the active validation target.""" + return self._effective_identifier + + @property + def effective_profile_identifier(self) -> str: + """Profile exposed for the active validation target.""" + return self._effective_profile_identifier + + @property + def source_identifier(self) -> str: + """Identifier of the physically declared check.""" + return self.requirement_check.identifier + + @property + def source_profile_identifier(self) -> str: + """Profile that physically declares the check.""" + return self.requirement_check.requirement.profile.identifier + + def set_effective_identity(self, identifier: str, profile_identifier: str) -> None: + """Attach context-local reporting identity before dispatch.""" + self._effective_identifier = identifier + self._effective_profile_identifier = profile_identifier + def __str__(self) -> str: return f"RequirementCheckValidationEvent({self.event_type}, {self.requirement_check})" diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index e1c71de10..5d641dc7c 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -27,6 +27,7 @@ PROF_NS, PROFILE_SPECIFICATION_FILE, SCHEMA_ORG_NS, + VALIDATOR_NS, ) from rocrate_validator.errors import ( DuplicateRequirementCheck, @@ -291,6 +292,17 @@ def is_transitive_profile_of(self) -> list[str]: """ return cast("list[str]", self.__get_specification_property__("isTransitiveProfileOf", PROF_NS, pop_first=False)) + @property + def rule_overlay_of(self) -> list[str]: + """ + Profiles whose validation rules are composed into this profile. + + This implementation relationship is deliberately separate from + ``prof:isProfileOf``: ordinary profile inheritance retains its source + identity, while overlay rules are reported in the target namespace. + """ + return cast("list[str]", self.__get_specification_property__("ruleOverlayOf", VALIDATOR_NS, pop_first=False)) + @property def parents(self) -> list[Profile]: """ @@ -399,15 +411,34 @@ def get_requirement(self, name: str) -> Requirement | None: return requirement return None - def get_requirement_check(self, check_name: str) -> RequirementCheck | None: - """ - Get the requirement check with the given name - """ - for requirement in self.requirements: - check = requirement.get_check(check_name) - if check: - return check - return None + def get_requirement_checks( + self, + check_name: str, + severity: Severity | None = None, + ) -> list[RequirementCheck]: + """Return checks matching a name and, when provided, a severity.""" + return [ + check + for requirement in self.requirements + for check in requirement.get_checks() + if check.name == check_name and (severity is None or check.severity == severity) + ] + + def get_requirement_check( + self, + check_name: str, + severity: Severity | None = None, + ) -> RequirementCheck | None: + """Get the check matching a name and optional severity. + + A check identity must be unique within a profile. Ambiguous lookups fail + instead of making override behavior depend on requirement load order. + """ + checks = self.get_requirement_checks(check_name, severity) + if len(checks) > 1: + identity = f"{check_name} [{severity.name}]" if severity else check_name + raise DuplicateRequirementCheck(identity, self.identifier) + return checks[0] if checks else None @classmethod def __get_nested_profiles__(cls, source: str) -> list[str]: diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index fa842c20a..c1dc9867b 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -300,6 +300,7 @@ def __record_skipped_check__( context.result._record_check_result(check, CheckResult.SKIPPED, message, category) inherited_reporting_disabled = ( check.requirement.profile.identifier != context.profile_identifier + and not context.is_rule_overlay_source(check.requirement.profile) and context.settings.disable_inherited_profiles_issue_reporting ) if not inherited_reporting_disabled: @@ -378,6 +379,7 @@ def __execute_check__(self, check, context, all_passed): skip_event_notify = False if ( check.requirement.profile.identifier != context.profile_identifier + and not context.is_rule_overlay_source(check.requirement.profile) and context.settings.disable_inherited_profiles_issue_reporting ): logger.debug( @@ -822,7 +824,7 @@ def severity(self) -> Severity: def overridden_by(self) -> list[RequirementCheck]: overridden_by = [] for sibling_profile in self.requirement.profile.siblings: - check = sibling_profile.get_requirement_check(self.name) + check = sibling_profile.get_requirement_check(self.name, self.severity) if check: overridden_by.append(check) return overridden_by @@ -831,7 +833,7 @@ def overridden_by(self) -> list[RequirementCheck]: def overrides(self) -> list[RequirementCheck]: overrides = [] for parent in self.requirement.profile.parents: - check = parent.get_requirement_check(self.name) + check = parent.get_requirement_check(self.name, self.severity) if check: overrides.append(check) return overrides diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 71c83ede2..58f2bd2ec 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -56,12 +56,31 @@ def __init__( violatingProperty: str | None = None, violatingEntity: str | None = None, value: str | None = None, + context: ValidationContext | None = None, ): self._message = message self._check: RequirementCheck = check self._violatingProperty = violatingProperty self._violatingEntity = violatingEntity self._propertyValue = value + self._context = context + + @property + def identifier(self) -> str: + """Check identifier as exposed by the active validation target.""" + return self._context.effective_check_identifier(self.check) if self._context else self.check.identifier + + @property + def profile_identifier(self) -> str: + """Effective profile identifier used for reporting.""" + if self._context: + return self._context.effective_check_profile(self.check).identifier + return self.check.requirement.profile.identifier + + @property + def source_profile_identifier(self) -> str: + """Profile that physically declares the check implementation.""" + return self.check.requirement.profile.identifier @property def message(self) -> str | None: @@ -135,7 +154,7 @@ def __repr__(self) -> str: return f"CheckIssue(severity={self.severity}, check={self.check}, message={self.message})" def __str__(self) -> str: - return f'Issue of severity {self.severity.name} with check "{self.check.identifier}": {self.message}' + return f'Issue of severity {self.severity.name} with check "{self.identifier}": {self.message}' def to_dict( self, @@ -152,6 +171,10 @@ def to_dict( } if with_check: result["check"] = self.check.to_dict(with_requirement=with_requirement, with_profile=with_profile) + result["check"]["source_identifier"] = self.check.identifier + result["check"]["identifier"] = self.identifier + result["check"]["source_profile"] = self.source_profile_identifier + result["check"]["profile"] = self.profile_identifier return result def to_json( @@ -204,7 +227,7 @@ def __init__(self, context: ValidationContext): self._skipped_checks: set[RequirementCheck] = set() self._skipped_check_details: dict[str, SkippedCheckDetail] = {} # initialize the statistics - self._statistics = ValidationStatistics(context.settings) + self._statistics = ValidationStatistics(context.settings, context=context) @property def context(self) -> ValidationContext: @@ -265,6 +288,7 @@ def _record_check_result( check=check, message=skip_message or "Check returned SKIPPED", category=normalized_skip_category, + context=getattr(self, "_context", None), ) return normalized_result @@ -399,6 +423,7 @@ def add_issue( violatingProperty=violatingProperty, violatingEntity=violatingEntity, value=violatingPropertyValue, + context=self.context, ) bisect.insort(self._issues, c) return c diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py index 47c125f6f..8e6cb3ea5 100644 --- a/rocrate_validator/models/skipped_check.py +++ b/rocrate_validator/models/skipped_check.py @@ -14,12 +14,13 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum, unique from typing import TYPE_CHECKING, Literal, TypeAlias if TYPE_CHECKING: from rocrate_validator.models.requirement import RequirementCheck + from rocrate_validator.models.validation import ValidationContext @unique @@ -86,17 +87,26 @@ class SkippedCheckDetail: check: RequirementCheck message: str category: SkipCategory = SkipCategory.RETURNED + context: ValidationContext | None = field(default=None, compare=False, repr=False) def __post_init__(self) -> None: object.__setattr__(self, "category", SkipCategory(self.category)) def to_dict(self) -> dict[str, str]: - return { - "identifier": self.check.identifier, - "profile": self.check.requirement.profile.identifier, + source_identifier = self.check.identifier + source_profile = self.check.requirement.profile.identifier + identifier = self.context.effective_check_identifier(self.check) if self.context else source_identifier + profile = self.context.effective_check_profile(self.check).identifier if self.context else source_profile + result = { + "identifier": identifier, + "profile": profile, "requirement": self.check.requirement.name, "name": self.check.name, "severity": self.check.severity.name, "message": self.message, "category": self.category.value, } + if identifier != source_identifier or profile != source_profile: + result["source_identifier"] = source_identifier + result["source_profile"] = source_profile + return result diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index 82d964205..a0035a5bc 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -232,6 +232,14 @@ def validated_checks(self) -> list[RequirementCheck]: """ return self._stats.get("validated_checks", []) + def effective_check_identifier(self, check: RequirementCheck) -> str: + """Return the identifier used to report a check in this validation.""" + return self._context.effective_check_identifier(check) if self._context else check.identifier + + def effective_check_profile(self, check: RequirementCheck) -> Profile: + """Return the profile used to report a check in this validation.""" + return self._context.effective_check_profile(check) if self._context else check.requirement.profile + @property def started_at(self) -> datetime | None: """ @@ -312,8 +320,12 @@ def __initialise__(cls, validation_settings: ValidationSettings): profiles = [profile] # add inherited profiles if enabled - if not validation_settings.disable_inherited_profiles_issue_reporting: - profiles.extend(profile.inherited_profiles) + profiles.extend( + inherited + for inherited in profile.inherited_profiles + if not validation_settings.disable_inherited_profiles_issue_reporting + or inherited.uri in profile.rule_overlay_of + ) logger.debug("Inherited profiles: %r", profile.inherited_profiles) # Initialize the counters @@ -428,12 +440,12 @@ def __handle_requirement_check_validation_end__(self, event: Event, ctx: Validat else: logger.debug( "Requirement check validation result is None: %s", - event.requirement_check.identifier, + event.effective_identifier, ) else: logger.debug( "Skipping requirement check validation: %s", - event.requirement_check.identifier, + event.effective_identifier, ) def __handle_requirement_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 02a137849..cd0a8bb0b 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -28,6 +28,7 @@ from rocrate_validator.models._logging import logger from rocrate_validator.models.events import ( ProfileValidationEvent, + RequirementCheckValidationEvent, RequirementValidationEvent, ValidationEvent, ) @@ -295,6 +296,11 @@ def notify(self, event: Event | EventType, ctx: Any | None = None): result: ValidationResult = self.__current_context__.result if isinstance(event, EventType): event = Event(event) + if isinstance(event, RequirementCheckValidationEvent): + event.set_effective_identity( + self.__current_context__.effective_check_identifier(event.requirement_check), + self.__current_context__.effective_check_profile(event.requirement_check).identifier, + ) result.statistics.update(event, ctx=self.__current_context__) return super().notify(event, ctx=self.__current_context__) @@ -687,6 +693,33 @@ def target_profile(self) -> Profile: assert len(profiles) > 0, "No profiles to validate" return self.profiles[-1] + def is_rule_overlay_source(self, profile: Profile) -> bool: + """Return whether ``profile`` is composed into the validation target.""" + target = self.target_profile + return profile == target or profile.uri in target.rule_overlay_of + + def effective_check_identifier(self, check: RequirementCheck) -> str: + """Return a context-local identifier without mutating the source check.""" + if not self.is_rule_overlay_source(check.requirement.profile): + return check.identifier + identity_check = check + if check.requirement.profile == self.target_profile: + overlay_overrides = [ + parent_check + for parent_check in check.overrides + if parent_check.requirement.profile.uri in self.target_profile.rule_overlay_of + ] + if len(overlay_overrides) == 1: + identity_check = overlay_overrides[0] + relative_identifier = identity_check.relative_identifier.split(" ", maxsplit=1)[-1] + return f"{self.target_profile.identifier}_{relative_identifier}" + + def effective_check_profile(self, check: RequirementCheck) -> Profile: + """Return the reporting profile for a check in this validation.""" + return ( + self.target_profile if self.is_rule_overlay_source(check.requirement.profile) else check.requirement.profile + ) + def get_profile_by_token(self, token: str) -> list[Profile]: """ Get the profile by token from the profiles to validate against diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 4a1fb96dc..091a70f7b 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -98,6 +98,7 @@ def __init__( def execute_check(self, context: ValidationContext) -> CheckResultValue: if ( self.requirement.profile.identifier != context.profile_identifier + and not context.is_rule_overlay_source(self.requirement.profile) and context.settings.disable_inherited_profiles_issue_reporting ): logger.debug( @@ -260,7 +261,6 @@ def check( :type deactivated: bool :param depends_on: names of checks that must run before this check. :type depends_on: Optional[Tuple[str, ...]] - :return: the decorated function :rtype: Callable """ diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 9dc7dfd37..a888ed593 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -419,6 +419,7 @@ def __process_failed_checks__( # and the disable_inherited_profiles_reporting is enabled, skip it if ( requirementCheck.requirement.profile != shacl_context.current_validation_profile + and not shacl_context.is_rule_overlay_source(requirementCheck.requirement.profile) and shacl_context.settings.disable_inherited_profiles_issue_reporting ): continue @@ -500,6 +501,7 @@ def __notify_skipped_checks__(self, shacl_context, failed_requirement_checks_not shacl_context.result._add_executed_check(skipped_check, True) if ( skipped_check.requirement.profile != shacl_context.target_profile + and not shacl_context.is_rule_overlay_source(skipped_check.requirement.profile) and shacl_context.settings.disable_inherited_profiles_issue_reporting ): continue From 16709a69d6ac06ad22dc8e7a7e392b8afd875821 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 14:53:07 +0200 Subject: [PATCH 100/271] =?UTF-8?q?fix(profiles):=20=F0=9F=8F=B7=EF=B8=8F?= =?UTF-8?q?=20disambiguate=20legacy=20requirement=20check=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl | 4 ++-- .../ro-crate/1.1/should/5_web_data_entity_metadata.py | 2 +- .../profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl index f69cb2a8c..c10007a89 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl @@ -21,7 +21,7 @@ @prefix validator: . ro-crate:DataEntityRequiredProperties a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; + sh:name "Data Entity: REQUIRED @id format" ; sh:description """A Data Entity MUST be a `URI Path` relative to the ROCrate root, or an absolute URI""" ; sh:targetClass ro-crate:DataEntity ; @@ -135,7 +135,7 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; ] . ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; + sh:name "Data Entity: REQUIRED Root linkage" ; sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; sh:property diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py index 7df84b2c5..2baa10480 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py @@ -81,7 +81,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: return result return result - @check(name="Web-based Data Entity: `contentSize` property") + @check(name="Web-based Data Entity: `contentSize` matches downloadable content") def check_content_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the Web-based Data Entity has a `contentSize` property diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl index 44750088b..a2dc53a04 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl @@ -20,7 +20,7 @@ @prefix validator: . ro-crate:DataEntityRequiredProperties a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; + sh:name "Data Entity: REQUIRED @id format" ; sh:description """A Data Entity MUST be a `URI Path` relative to the ROCrate root, or an absolute URI""" ; sh:targetClass ro-crate:DataEntity ; @@ -142,7 +142,7 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; ] . ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; + sh:name "Data Entity: REQUIRED Root linkage" ; sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; sh:property From f4d63159b7d66a558d80befa3864cf151338d35a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 14:53:08 +0200 Subject: [PATCH 101/271] =?UTF-8?q?test(profiles):=20=E2=9C=85=20cover=20n?= =?UTF-8?q?ame=20and=20severity=20overlay=20matching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/check_overriding/b/profile.ttl | 2 + .../ro-crate/test_data_entity_metadata.py | 2 +- tests/unit/requirements/test_profiles.py | 164 +++++++++++++++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/tests/data/profiles/check_overriding/b/profile.ttl b/tests/data/profiles/check_overriding/b/profile.ttl index e947b732d..7b1c66e1d 100644 --- a/tests/data/profiles/check_overriding/b/profile.ttl +++ b/tests/data/profiles/check_overriding/b/profile.ttl @@ -16,6 +16,7 @@ @prefix prof: . @prefix role: . @prefix rdfs: . +@prefix validator: . @@ -33,6 +34,7 @@ # # This profile is an extension of the RO-Crate Metadata Specification 1.1 profile prof:isProfileOf ; + validator:ruleOverlayOf ; # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile prof:isTransitiveProfileOf ; diff --git a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py index 6317d32ee..4e7fe9046 100644 --- a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py +++ b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py @@ -36,7 +36,7 @@ def test_missing_data_entity_reference(): paths.missing_hasPart_data_entity_reference, models.Severity.REQUIRED, False, - ["Data Entity: REQUIRED properties"], + ["Data Entity: REQUIRED Root linkage"], ["sort-and-change-case.ga", "foo/xxx"], ) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index bd847f642..4ca359af3 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +from dataclasses import replace from pathlib import Path from typing import Any @@ -21,7 +22,17 @@ from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER, SHACL_NS from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError -from rocrate_validator.models import URI, Profile, ValidationContext, ValidationSettings, Validator +from rocrate_validator.events import EventType +from rocrate_validator.models import ( + URI, + CheckResult, + Profile, + Severity, + ValidationContext, + ValidationSettings, + Validator, +) +from rocrate_validator.models.events import RequirementCheckValidationEvent from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry @@ -318,6 +329,154 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat assert len(requirements_checks) == 3, "The number of requirements should be 2" +def test_check_name_and_severity_are_unique_within_each_profile(): + profiles = Profile.load_profiles( + profiles_path=Path("rocrate_validator/profiles"), + severity=Severity.OPTIONAL, + ) + for profile in profiles: + checks = [check for requirement in profile.requirements for check in requirement.get_checks()] + identities = {(check.name, check.severity) for check in checks} + assert len(identities) == len(checks), profile.identifier + + +def test_check_name_and_severity_match_parent_override(check_overriding_profiles_path: str): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + parent = next(item for item in profiles if item.identifier == "a") + child = next(item for item in profiles if item.identifier == "b") + + parent_check = parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + child_check = child.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert parent_check is not None + assert child_check is not None + assert child_check.overrides == [parent_check] + assert child_check in parent_check.overridden_by + + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + original_order = child_check.order_number + child_check.order_number = 99 + try: + assert ( + context.effective_check_identifier(child_check) == f"b_{parent_check.relative_identifier.split(' ', 1)[-1]}" + ) + finally: + child_check.order_number = original_order + + +def test_same_name_with_different_severity_does_not_override(): + profiles = Profile.load_profiles( + profiles_path=Path("rocrate_validator/profiles"), + severity=Severity.OPTIONAL, + allow_requirement_check_override=True, + ) + workflow_run = next(item for item in profiles if item.identifier == "workflow-run-crate-0.5") + process_run = next(item for item in profiles if item.identifier == "process-run-crate-0.5") + + required = workflow_run.get_requirement_check("Root Data Entity conformsTo", Severity.REQUIRED) + recommended = workflow_run.get_requirement_check("Root Data Entity conformsTo", Severity.RECOMMENDED) + parent = process_run.get_requirement_check("Root Data Entity conformsTo", Severity.REQUIRED) + + assert required is not None + assert recommended is not None + assert parent is not None + assert required.overrides == [parent] + assert recommended.overrides == [] + + +def test_rule_overlay_effective_identity_is_context_local(check_overriding_profiles_path: str): + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + parent = next(item for item in context.profiles if item.identifier == "a") + inherited_check = parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert inherited_check is not None + source_identifier = inherited_check.identifier + assert context.is_rule_overlay_source(parent) + assert context.effective_check_profile(inherited_check).identifier == "b" + assert context.effective_check_identifier(inherited_check).startswith("b_") + assert inherited_check.identifier == source_identifier + assert inherited_check.requirement.profile.identifier == "a" + + issue = context.result.add_issue("test issue", inherited_check) + serialized_check = issue.to_dict()["check"] + assert serialized_check["identifier"].startswith("b_") + assert serialized_check["profile"] == "b" + assert serialized_check["source_identifier"] == source_identifier + assert serialized_check["source_profile"] == "a" + + context.result._record_check_result(inherited_check, CheckResult.SKIPPED, "test skip") + serialized_skip = context.result.skipped_check_details[0].to_dict() + assert serialized_skip["identifier"].startswith("b_") + assert serialized_skip["profile"] == "b" + assert serialized_skip["source_identifier"] == source_identifier + assert serialized_skip["source_profile"] == "a" + assert context.result.statistics.effective_check_identifier(inherited_check).startswith("b_") + assert context.result.statistics.effective_check_profile(inherited_check).identifier == "b" + + event = RequirementCheckValidationEvent(EventType.REQUIREMENT_CHECK_VALIDATION_END, inherited_check) + event.set_effective_identity( + context.effective_check_identifier(inherited_check), + context.effective_check_profile(inherited_check).identifier, + ) + assert event.effective_identifier.startswith("b_") + assert event.effective_profile_identifier == "b" + assert event.source_identifier == source_identifier + assert event.source_profile_identifier == "a" + + suppressed_settings = replace(settings, disable_inherited_profiles_issue_reporting=True) + suppressed_context = ValidationContext(Validator(suppressed_settings), suppressed_settings) + assert suppressed_context.result.statistics.total_checks == context.result.statistics.total_checks + + +def test_normally_inherited_check_keeps_source_identity(check_overriding_profiles_path: str): + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="c", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + parent = next(item for item in context.profiles if item.identifier == "a") + inherited_check = parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert inherited_check is not None + assert not context.is_rule_overlay_source(parent) + assert context.effective_check_profile(inherited_check).identifier == "a" + assert context.effective_check_identifier(inherited_check) == inherited_check.identifier + + +def test_overlay_identity_is_isolated_between_consecutive_contexts(check_overriding_profiles_path: str): + base_settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + overlay_context = ValidationContext(Validator(base_settings), base_settings) + overlay_parent = next(item for item in overlay_context.profiles if item.identifier == "a") + overlay_check = overlay_parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + assert overlay_check is not None + assert overlay_context.effective_check_profile(overlay_check).identifier == "b" + + inherited_settings = replace(base_settings, profile_identifier="c") + inherited_context = ValidationContext(Validator(inherited_settings), inherited_settings) + inherited_parent = next(item for item in inherited_context.profiles if item.identifier == "a") + inherited_check = inherited_parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + assert inherited_check is not None + assert inherited_context.effective_check_profile(inherited_check).identifier == "a" + assert inherited_context.effective_check_identifier(inherited_check) == inherited_check.identifier + + assert overlay_context.effective_check_profile(overlay_check).identifier == "b" + + def test_zero_shape_target_profile_triggers_pyshacl_run(monkeypatch, fake_profiles_path: str): """Regression test for the 0-shape profile bug: when the target profile has no SHACL checks of its own, @@ -667,7 +826,8 @@ def test_shacl_check_deactivation_scoped_to_descendants(fake_profiles_path: str) # Trigger lazy loading. for p in all_profiles: - _ = p.requirements + if p.token != "invalid-duplicated-shapes": + _ = p.requirements _ = context.profiles # warm context too parent_shape_check = next(c for c in parent_c.requirements[0].get_checks() if isinstance(c, SHACLCheck)) From c0b65cad72434db97e3cf56405b57a65e1462e80 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 15:25:59 +0200 Subject: [PATCH 102/271] =?UTF-8?q?perf(core):=20=E2=9A=A1=20cache=20profi?= =?UTF-8?q?le=20check=20identity=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile.py | 29 ++++++++++++++++++++++-- rocrate_validator/models/validation.py | 6 +++++ tests/unit/requirements/test_profiles.py | 13 +++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 5d641dc7c..5a27abddb 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -17,7 +17,7 @@ import re from functools import total_ordering from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, ClassVar, cast from rdflib import RDF, RDFS, Graph, Namespace, URIRef @@ -64,6 +64,8 @@ class Profile: MapIndex("token_path", unique=False), ], ) + __validated_check_identities: ClassVar[set[tuple[Path, Severity]]] = set() + __invalid_check_identities: ClassVar[dict[tuple[Path, Severity], str]] = {} def __init__( self, @@ -429,7 +431,8 @@ def get_requirement_check( check_name: str, severity: Severity | None = None, ) -> RequirementCheck | None: - """Get the check matching a name and optional severity. + """ + Get the check matching a name and optional severity. A check identity must be unique within a profile. Ambiguous lookups fail instead of making override behavior depend on requirement load order. @@ -440,6 +443,28 @@ def get_requirement_check( raise DuplicateRequirementCheck(identity, self.identifier) return checks[0] if checks else None + def validate_requirement_check_identities(self) -> None: + """Reject duplicate ``(name, severity)`` identities in this profile.""" + cache_key = (self.path.resolve(), self.severity) + if cache_key in self.__validated_check_identities: + return + if duplicate := self.__invalid_check_identities.get(cache_key): + raise DuplicateRequirementCheck(duplicate, self.identifier) + + seen: set[tuple[str, Severity]] = set() + for requirement in self.requirements: + for check in requirement.get_checks(): + identity = (check.name, check.severity) + if identity in seen: + duplicate = f"{check.name} [{check.severity.name}]" + self.__invalid_check_identities[cache_key] = duplicate + raise DuplicateRequirementCheck( + duplicate, + self.identifier, + ) + seen.add(identity) + self.__validated_check_identities.add(cache_key) + @classmethod def __get_nested_profiles__(cls, source: str) -> list[str]: result: list[str] = [] diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index cd0a8bb0b..54cbbd44f 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -651,6 +651,12 @@ def __load_profiles__(self) -> list[Profile]: # Set the profiles to validate against as the target profile and its inherited profiles profiles = [*profile.inherited_profiles, profile] + # Validate check identities only for profiles participating in this run. + # Profile listing and discovery may still inspect intentionally invalid + # fixtures without forcing requirement loading. + for validation_profile in profiles: + validation_profile.validate_requirement_check_identities() + # if the check for duplicates is disabled, return the profiles if self.disable_check_for_duplicates: return profiles diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 4ca359af3..d6f53cbaa 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -300,6 +300,19 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: logger.debug("The profiles: %r", profiles) +def test_validation_rejects_duplicate_check_identity_with_overrides_enabled(fake_profiles_path: str): + settings = ValidationSettings( + profiles_path=fake_profiles_path, + profile_identifier="invalid-duplicated-shapes", + rocrate_uri=ValidROC().wrroc_paper, + enable_profile_inheritance=True, + allow_requirement_check_override=True, + ) + + with pytest.raises(DuplicateRequirementCheck, match="Check Metadata File Descriptor entity existence"): + _ = ValidationContext(Validator(settings), settings).profiles + + def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" settings_dict: dict[str, Any] = { From 997489564c1065df9fc71f36844271bfffe5d4a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 15:54:57 +0200 Subject: [PATCH 103/271] =?UTF-8?q?feat(core):=20=F0=9F=A7=A9=20add=20exte?= =?UTF-8?q?nsible=20profile=20consistency=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/__init__.py | 10 +++ rocrate_validator/models/profile.py | 42 +++++----- rocrate_validator/models/profile_check.py | 99 +++++++++++++++++++++++ rocrate_validator/models/settings.py | 2 + rocrate_validator/models/validation.py | 5 +- tests/unit/requirements/test_profiles.py | 30 +++++-- tests/unit/test_validation_settings.py | 5 ++ 7 files changed, 162 insertions(+), 31 deletions(-) create mode 100644 rocrate_validator/models/profile_check.py diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index 559da0e0e..4bf018d88 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -21,6 +21,12 @@ ValidationEvent, ) from rocrate_validator.models.profile import Profile +from rocrate_validator.models.profile_check import ( + ProfileCheck, + ProfileCheckFailure, + ProfileCheckResult, + ProfileCheckSuite, +) from rocrate_validator.models.requirement import ( Requirement, RequirementCheck, @@ -61,6 +67,10 @@ "CustomEncoder", "LevelCollection", "Profile", + "ProfileCheck", + "ProfileCheckFailure", + "ProfileCheckResult", + "ProfileCheckSuite", "ProfileValidationEvent", "Requirement", "RequirementCheck", diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 5a27abddb..44b13cfc9 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -37,6 +37,7 @@ ProfileSpecificationNotFound, ) from rocrate_validator.models._logging import logger +from rocrate_validator.models.profile_check import ProfileCheckFailure, ProfileCheckResult, ProfileCheckSuite from rocrate_validator.models.severity import Severity from rocrate_validator.utils.collections import MapIndex, MultiIndexMap @@ -64,8 +65,7 @@ class Profile: MapIndex("token_path", unique=False), ], ) - __validated_check_identities: ClassVar[set[tuple[Path, Severity]]] = set() - __invalid_check_identities: ClassVar[dict[tuple[Path, Severity], str]] = {} + __profile_check_results: ClassVar[dict[tuple[Path, Severity], tuple[ProfileCheckResult, ...]]] = {} def __init__( self, @@ -443,27 +443,27 @@ def get_requirement_check( raise DuplicateRequirementCheck(identity, self.identifier) return checks[0] if checks else None - def validate_requirement_check_identities(self) -> None: - """Reject duplicate ``(name, severity)`` identities in this profile.""" + def validate_checks(self) -> tuple[ProfileCheckResult, ...]: + """Run and cache the registered consistency checks for this profile.""" cache_key = (self.path.resolve(), self.severity) - if cache_key in self.__validated_check_identities: - return - if duplicate := self.__invalid_check_identities.get(cache_key): - raise DuplicateRequirementCheck(duplicate, self.identifier) + if cache_key not in self.__profile_check_results: + self.__profile_check_results[cache_key] = ProfileCheckSuite().run(self) + return self.__profile_check_results[cache_key] + + def validate_profile_checks(self) -> None: + """Run all profile checks and raise when one of them fails.""" + for result in self.validate_checks(): + if result.passed: + continue + if result.check_id == "unique-requirement-check-identity": + name = result.details["name"] + severity = result.details["severity"] + raise DuplicateRequirementCheck(f"{name} [{severity}]", self.identifier) + raise ProfileCheckFailure(result) - seen: set[tuple[str, Severity]] = set() - for requirement in self.requirements: - for check in requirement.get_checks(): - identity = (check.name, check.severity) - if identity in seen: - duplicate = f"{check.name} [{check.severity.name}]" - self.__invalid_check_identities[cache_key] = duplicate - raise DuplicateRequirementCheck( - duplicate, - self.identifier, - ) - seen.add(identity) - self.__validated_check_identities.add(cache_key) + def validate_requirement_check_identities(self) -> None: + """Backward-compatible alias for the profile check suite.""" + self.validate_profile_checks() @classmethod def __get_nested_profiles__(cls, source: str) -> list[str]: diff --git a/rocrate_validator/models/profile_check.py b/rocrate_validator/models/profile_check.py new file mode 100644 index 000000000..9b0369818 --- /dev/null +++ b/rocrate_validator/models/profile_check.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, ClassVar + +if TYPE_CHECKING: + from collections.abc import Mapping + + from rocrate_validator.models.profile import Profile + from rocrate_validator.models.severity import Severity + + +@dataclass(frozen=True) +class ProfileCheckResult: + """Structured result produced by a profile consistency check.""" + + check_id: str + profile_identifier: str + passed: bool + message: str + details: Mapping[str, str] = field(default_factory=dict) + + +class ProfileCheckFailure(Exception): + """Raised when a profile consistency check does not pass.""" + + def __init__(self, result: ProfileCheckResult): + self.result = result + super().__init__(result.message) + + +class ProfileCheck(ABC): + """Base class for checks validating a profile definition.""" + + identifier: ClassVar[str] + description: ClassVar[str] + + @abstractmethod + def run(self, profile: Profile) -> ProfileCheckResult: + """Run this check against ``profile``.""" + + +class UniqueRequirementCheckIdentity(ProfileCheck): + """Ensure ``(name, severity)`` identifies at most one check per profile.""" + + identifier = "unique-requirement-check-identity" + description = "Requirement check names and severities must be unique within a profile" + + def run(self, profile: Profile) -> ProfileCheckResult: + seen: set[tuple[str, Severity]] = set() + for requirement in profile.requirements: + for check in requirement.get_checks(): + identity = (check.name, check.severity) + if identity in seen: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Duplicate requirement check identity", + details={ + "name": check.name, + "severity": check.severity.name, + }, + ) + seen.add(identity) + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="All requirement check identities are unique", + ) + + +class ProfileCheckSuite: + """Run the registered consistency checks for a profile.""" + + DEFAULT_CHECKS: ClassVar[tuple[type[ProfileCheck], ...]] = (UniqueRequirementCheckIdentity,) + + def __init__(self, checks: tuple[type[ProfileCheck], ...] | None = None): + self._checks = tuple(check() for check in (checks or self.DEFAULT_CHECKS)) + + def run(self, profile: Profile) -> tuple[ProfileCheckResult, ...]: + """Return one result for every registered profile check.""" + return tuple(check.run(profile) for check in self._checks) diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index da3df7ceb..43be3a660 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -82,6 +82,8 @@ class ValidationSettings: # Requirement check settings #: Flag to allow requirement check override allow_requirement_check_override: bool = True + #: Flag to validate profile consistency checks before validation + validate_profile_checks: bool = True #: Flag to disable the check for duplicates disable_check_for_duplicates: bool = False #: Checks to skip diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 54cbbd44f..b358f55d2 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -654,8 +654,9 @@ def __load_profiles__(self) -> list[Profile]: # Validate check identities only for profiles participating in this run. # Profile listing and discovery may still inspect intentionally invalid # fixtures without forcing requirement loading. - for validation_profile in profiles: - validation_profile.validate_requirement_check_identities() + if self.settings.validate_profile_checks: + for validation_profile in profiles: + validation_profile.validate_profile_checks() # if the check for duplicates is disabled, return the profiles if self.disable_check_for_duplicates: diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index d6f53cbaa..4d93f5ad3 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -83,8 +83,8 @@ def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): # Check if the InvalidProfilePath exception is raised with pytest.raises(InvalidProfilePath): - profiles = context.profiles - logger.debug("The profiles: %r", profiles) + # Load the profiles + _ = context.profiles def test_profile_detection_error_is_not_silenced(monkeypatch): @@ -273,8 +273,7 @@ def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): with pytest.raises(DuplicateRequirementCheck): # Load the profiles - profiles = context.profiles - logger.debug("The profiles: %r", profiles) + _ = context.profiles def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: str): @@ -296,15 +295,14 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: with pytest.raises(DuplicateRequirementCheck): # Load the profiles - profiles = context.profiles - logger.debug("The profiles: %r", profiles) + _ = context.profiles def test_validation_rejects_duplicate_check_identity_with_overrides_enabled(fake_profiles_path: str): settings = ValidationSettings( - profiles_path=fake_profiles_path, + profiles_path=Path(fake_profiles_path), profile_identifier="invalid-duplicated-shapes", - rocrate_uri=ValidROC().wrroc_paper, + rocrate_uri=URI(ValidROC().wrroc_paper), enable_profile_inheritance=True, allow_requirement_check_override=True, ) @@ -353,6 +351,22 @@ def test_check_name_and_severity_are_unique_within_each_profile(): assert len(identities) == len(checks), profile.identifier +def test_profile_checks_return_structured_cached_results(): + profile = next( + item + for item in Profile.load_profiles(Path("rocrate_validator/profiles"), severity=Severity.OPTIONAL) + if item.identifier == "ro-crate-1.2" + ) + + first = profile.validate_checks() + second = profile.validate_checks() + + assert first is second + assert len(first) == 1 + assert first[0].check_id == "unique-requirement-check-identity" + assert first[0].passed + + def test_check_name_and_severity_match_parent_override(check_overriding_profiles_path: str): profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) parent = next(item for item in profiles if item.identifier == "a") diff --git a/tests/unit/test_validation_settings.py b/tests/unit/test_validation_settings.py index 7f4284e11..51a7ab1d5 100644 --- a/tests/unit/test_validation_settings.py +++ b/tests/unit/test_validation_settings.py @@ -84,6 +84,11 @@ def test_validation_settings_disable_inherited_profiles_issue_reporting(): settings = ValidationSettings() # type: ignore[call-arg] assert settings.disable_inherited_profiles_issue_reporting is False + +def test_validation_settings_profile_checks(): + settings = ValidationSettings(validate_profile_checks=False) # type: ignore[call-arg] + assert settings.validate_profile_checks is False + settings = ValidationSettings(disable_inherited_profiles_issue_reporting=True) # type: ignore[call-arg] assert settings.disable_inherited_profiles_issue_reporting is True From a4eb3fd2868b91a511bb993b73910ad0b36225de Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 15:55:06 +0200 Subject: [PATCH 104/271] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20add=20profile?= =?UTF-8?q?=20consistency=20check=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/profiles.py | 56 ++++++++++++++++++++++ tests/test_cli.py | 27 +++++++++++ 2 files changed, 83 insertions(+) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index ca639bd40..0e6385ff8 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -156,6 +156,62 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA handle_error(e, console) +@profiles.command("check") +@click.argument("profile-identifier", type=click.STRING, default=DEFAULT_PROFILE_IDENTIFIER, required=True) +@click.option( + "--no-paging", is_flag=True, help="Disable paging", default=False, show_default=True, hidden=sys.platform == "win32" +) +@click.pass_context +def check_profile(ctx, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, no_paging: bool = False): + """Check the consistency of a profile and its inherited profiles.""" + console = ctx.obj["console"] + pager = ctx.obj["pager"] + interactive = ctx.obj["interactive"] + enable_pager = not no_paging and interactive and sys.platform != "win32" + failed = False + + try: + profile = services.get_profile( + profile_identifier, + profiles_path=ctx.obj["profiles_path"], + extra_profiles_path=ctx.obj["extra_profiles_path"], + ) + profiles = [*profile.inherited_profiles, profile] + results = [ + (checked_profile, result) for checked_profile in profiles for result in checked_profile.validate_checks() + ] + + table = Table( + title=f" Profile checks: {profile.identifier}", + title_style="italic bold cyan", + title_justify="left", + header_style="bold cyan", + border_style="bright_black", + ) + table.add_column("Profile", style="magenta bold") + table.add_column("Check", style="cyan") + table.add_column("Status", justify="center") + table.add_column("Message") + table.add_column("Details") + + for checked_profile, result in results: + status = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]" + failed |= not result.passed + details = ", ".join(f"{key}={value}" for key, value in result.details.items()) + table.add_row(checked_profile.identifier, result.check_id, status, result.message, details) + + with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: + console.print(get_app_header_rule()) + console.print(Padding(table, (0, 1))) + + except SystemExit: + raise + except Exception as e: + handle_error(e, console) + if failed: + raise click.exceptions.Exit(1) + + @profiles.command("describe") @click.option( "-v", diff --git a/tests/test_cli.py b/tests/test_cli.py index 56700943b..da97418d6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -361,6 +361,33 @@ def test_profiles_list(cli_runner: CliRunner): assert "ro-crate-1.1" in result.output # Check for a known profile +def test_profiles_check(cli_runner: CliRunner): + result = cli_runner.invoke(cli, ["profiles", "check", "ro-crate", "--no-paging"]) + + assert result.exit_code == 0 + assert "Profile checks: ro-crate-1.2" in result.output + assert "unique-requirement-check-identity" in result.output + assert "PASS" in result.output + + +def test_profiles_check_reports_invalid_profile(cli_runner: CliRunner, fake_profiles_path: Path): + result = cli_runner.invoke( + cli, + [ + "profiles", + "--extra-profiles-path", + str(fake_profiles_path), + "check", + "invalid-duplicated-shapes", + "--no-paging", + ], + ) + + assert result.exit_code == 1 + assert "unique-requirement-check-identity" in result.output + assert "FAIL" in result.output + + def test_validate_no_auto_profile_falls_back_to_1_2(cli_runner: CliRunner): """ With auto-detection disabled the base `ro-crate` profile is used, and that From f430b2b95604be431956e4cdc5f08229a853722a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 15:55:13 +0200 Subject: [PATCH 105/271] =?UTF-8?q?feat(cli):=20=E2=9A=99=EF=B8=8F=20allow?= =?UTF-8?q?=20skipping=20automatic=20profile=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7376d5929..0b9b95246 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -140,6 +140,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True, ) +@click.option( + "--no-profile-checks", + is_flag=True, + help="Skip consistency checks on validation profiles", + default=False, + show_default=True, +) @click.option( "-l", "--requirement-severity", @@ -270,6 +277,7 @@ def validate( skip_availability_check: bool = False, no_auto_profile: bool = False, disable_profile_inheritance: bool = False, + no_profile_checks: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, skip_checks: list[str] | None = None, @@ -352,6 +360,7 @@ def validate( "creation_time": creation_time, "enforce_availability": enforce_availability, "skip_availability_check": skip_availability_check, + "validate_profile_checks": not no_profile_checks, } # Print the application header From 1a1972e3f7060830e06670e40c7506ea5370f81a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 17:31:32 +0200 Subject: [PATCH 106/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20prioritize=20overlay=20profile=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index b358f55d2..d04ee0f6d 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -648,8 +648,14 @@ def __load_profiles__(self) -> list[Profile]: if not self.inheritance_enabled: return [profile] - # Set the profiles to validate against as the target profile and its inherited profiles - profiles = [*profile.inherited_profiles, profile] + # Validate overlay profiles before their sources. An overlay contains + # the checks whose behavior changed locally; those checks must get the + # first chance to fail (for example, malformed JSON must be reported by + # the target profile before an inherited metadata check parses it). + if profile.rule_overlay_of: + profiles = [profile, *profile.inherited_profiles] + else: + profiles = [*profile.inherited_profiles, profile] # Validate check identities only for profiles participating in this run. # Profile listing and discovery may still inspect intentionally invalid From dbea5738c6adfd56eca8ee1ed2595d0a78c75bba Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 18:24:30 +0200 Subject: [PATCH 107/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20resolve=20overlay=20target=20profile=20independentl?= =?UTF-8?q?y=20of=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index d04ee0f6d..c9b06ddad 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -704,7 +704,11 @@ def target_profile(self) -> Profile: """ profiles = self.profiles assert len(profiles) > 0, "No profiles to validate" - return self.profiles[-1] + # Overlay validation visits the target before its sources so that + # target-specific fail-fast checks run first; resolve by identifier + # instead of relying on the order of the profiles list. + target = next((profile for profile in profiles if profile.identifier == self.profile_identifier), None) + return target or profiles[-1] def is_rule_overlay_source(self, profile: Profile) -> bool: """Return whether ``profile`` is composed into the validation target.""" From 59a6630320b4303b0ef6c22020371dafb1b01891 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 25 Sep 2026 16:54:46 +0200 Subject: [PATCH 108/271] =?UTF-8?q?feat(profile-model):=20=F0=9F=A7=A9=20v?= =?UTF-8?q?alidate=20rule=20overlay=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile_check.py | 70 ++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models/profile_check.py b/rocrate_validator/models/profile_check.py index 9b0369818..4285f0ce6 100644 --- a/rocrate_validator/models/profile_check.py +++ b/rocrate_validator/models/profile_check.py @@ -86,10 +86,78 @@ def run(self, profile: Profile) -> ProfileCheckResult: ) +class RuleOverlayConsistency(ProfileCheck): + """Ensure rule overlay sources can be composed without ambiguity.""" + + identifier = "rule-overlay-consistency" + description = "Rule overlay sources must be loaded direct parents with distinct check identities" + + def run(self, profile: Profile) -> ProfileCheckResult: + sources = profile.rule_overlay_of + if not sources: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="Profile does not declare rule overlay sources", + ) + + declared_parent_uris = set(profile.is_profile_of) + loaded_parents = {parent.uri: parent for parent in profile.parents} + for source_uri in sources: + if source_uri not in declared_parent_uris: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Rule overlay source is not a direct parent", + details={"source": source_uri}, + ) + if source_uri not in loaded_parents: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Rule overlay source profile is not loaded", + details={"source": source_uri}, + ) + + identities: dict[tuple[str, Severity], str] = {} + for source_uri in sources: + source_profile = loaded_parents[source_uri] + for requirement in source_profile.requirements: + for check in requirement.get_checks(): + identity = (check.name, check.severity) + previous_source = identities.get(identity) + if previous_source is not None and previous_source != source_uri: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Rule overlay sources contain an ambiguous check identity", + details={ + "name": check.name, + "severity": check.severity.name, + "sources": f"{previous_source}, {source_uri}", + }, + ) + identities[identity] = source_uri + + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="Rule overlay sources are consistent", + ) + + class ProfileCheckSuite: """Run the registered consistency checks for a profile.""" - DEFAULT_CHECKS: ClassVar[tuple[type[ProfileCheck], ...]] = (UniqueRequirementCheckIdentity,) + DEFAULT_CHECKS: ClassVar[tuple[type[ProfileCheck], ...]] = ( + UniqueRequirementCheckIdentity, + RuleOverlayConsistency, + ) def __init__(self, checks: tuple[type[ProfileCheck], ...] | None = None): self._checks = tuple(check() for check in (checks or self.DEFAULT_CHECKS)) From efe73b8baff6591b8bc1ef79664199bf5c98a0e7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 25 Sep 2026 17:20:09 +0200 Subject: [PATCH 109/271] =?UTF-8?q?fix(profile-model):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20validate=20disabled=20overrides=20through=20profile?= =?UTF-8?q?=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile.py | 28 +++++++++--------- rocrate_validator/models/profile_check.py | 35 ++++++++++++++++++++++- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 44b13cfc9..a88329117 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -37,7 +37,12 @@ ProfileSpecificationNotFound, ) from rocrate_validator.models._logging import logger -from rocrate_validator.models.profile_check import ProfileCheckFailure, ProfileCheckResult, ProfileCheckSuite +from rocrate_validator.models.profile_check import ( + NoRequirementCheckOverrides, + ProfileCheckFailure, + ProfileCheckResult, + ProfileCheckSuite, +) from rocrate_validator.models.severity import Severity from rocrate_validator.utils.collections import MapIndex, MultiIndexMap @@ -297,11 +302,13 @@ def is_transitive_profile_of(self) -> list[str]: @property def rule_overlay_of(self) -> list[str]: """ - Profiles whose validation rules are composed into this profile. + Direct parent profiles whose validation rules are overlaid by this profile. This implementation relationship is deliberately separate from ``prof:isProfileOf``: ordinary profile inheritance retains its source identity, while overlay rules are reported in the target namespace. + Profile consistency checks require every value to resolve to a loaded + direct parent and reject ambiguous check identities across sources. """ return cast("list[str]", self.__get_specification_property__("ruleOverlayOf", VALIDATOR_NS, pop_first=False)) @@ -711,19 +718,12 @@ def load_profiles( # Check for overridden checks if not allow_requirement_check_override: - # Navigate the profiles to check for overridden checks. - # If the override is not enabled in the settings raise an error. - profiles_checks = set() - # Search for duplicated checks in the profiles + override_check_suite = ProfileCheckSuite(checks=(NoRequirementCheckOverrides,)) for profile in profiles: - profile_checks = [_ for r in profile.get_requirements() for _ in r.get_checks()] - for check in profile_checks: - # If the check is already present in the list of checks, - # raise an error if the override is not enabled. - if check in profiles_checks: - raise DuplicateRequirementCheck(check.name, profile.identifier) - # Add the check to the list of checks - profiles_checks.add(check) + result = override_check_suite.run(profile)[0] + if not result.passed: + identity = f"{result.details['name']} [{result.details['severity']}]" + raise DuplicateRequirementCheck(identity, profile.identifier) # order profiles according to the number of profiles they depend on: # i.e, first the profiles that do not depend on any other profile diff --git a/rocrate_validator/models/profile_check.py b/rocrate_validator/models/profile_check.py index 4285f0ce6..11295592a 100644 --- a/rocrate_validator/models/profile_check.py +++ b/rocrate_validator/models/profile_check.py @@ -151,6 +151,38 @@ def run(self, profile: Profile) -> ProfileCheckResult: ) +class NoRequirementCheckOverrides(ProfileCheck): + """Ensure a profile does not replace checks defined by direct parents.""" + + identifier = "no-requirement-check-overrides" + description = "Requirement check overrides must not be present" + + def run(self, profile: Profile) -> ProfileCheckResult: + for requirement in profile.requirements: + for check in requirement.get_checks(): + overridden_checks = check.overrides + if overridden_checks: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Requirement check override is disabled", + details={ + "name": check.name, + "severity": check.severity.name, + "sources": ", ".join( + overridden.requirement.profile.identifier for overridden in overridden_checks + ), + }, + ) + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="No requirement check overrides found", + ) + + class ProfileCheckSuite: """Run the registered consistency checks for a profile.""" @@ -160,7 +192,8 @@ class ProfileCheckSuite: ) def __init__(self, checks: tuple[type[ProfileCheck], ...] | None = None): - self._checks = tuple(check() for check in (checks or self.DEFAULT_CHECKS)) + selected_checks = checks if checks is not None else self.DEFAULT_CHECKS + self._checks = tuple(check() for check in selected_checks) def run(self, profile: Profile) -> tuple[ProfileCheckResult, ...]: """Return one result for every registered profile check.""" From 53a6e2c961d629a75ca993d6f60fae6fb7da587a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 25 Sep 2026 17:21:21 +0200 Subject: [PATCH 110/271] =?UTF-8?q?test(profile):=20=E2=9C=85=20cover=20ov?= =?UTF-8?q?erlay=20consistency=20and=20override=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 1 + tests/unit/requirements/test_profiles.py | 104 ++++++++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index da97418d6..df58ca8aa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -367,6 +367,7 @@ def test_profiles_check(cli_runner: CliRunner): assert result.exit_code == 0 assert "Profile checks: ro-crate-1.2" in result.output assert "unique-requirement-check-identity" in result.output + assert "rule-overlay-consistency" in result.output assert "PASS" in result.output diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 4d93f5ad3..761b8c4d2 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -15,7 +15,7 @@ import logging from dataclasses import replace from pathlib import Path -from typing import Any +from typing import Any, cast import pytest from rdflib import Graph, Literal, Namespace @@ -33,6 +33,11 @@ Validator, ) from rocrate_validator.models.events import RequirementCheckValidationEvent +from rocrate_validator.models.profile_check import ( + NoRequirementCheckOverrides, + ProfileCheckSuite, + RuleOverlayConsistency, +) from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry @@ -362,9 +367,100 @@ def test_profile_checks_return_structured_cached_results(): second = profile.validate_checks() assert first is second - assert len(first) == 1 - assert first[0].check_id == "unique-requirement-check-identity" - assert first[0].passed + assert {result.check_id for result in first} == { + "unique-requirement-check-identity", + "rule-overlay-consistency", + } + assert all(result.passed for result in first) + + +def test_disabling_check_override_rejects_parent_match(check_overriding_profiles_path: str): + with pytest.raises(DuplicateRequirementCheck, match=r"\[REQUIRED\]"): + Profile.load_profiles( + check_overriding_profiles_path, + severity=Severity.OPTIONAL, + allow_requirement_check_override=False, + ) + + +def test_requirement_check_override_policy_is_not_a_default_profile_check(): + assert NoRequirementCheckOverrides not in ProfileCheckSuite.DEFAULT_CHECKS + + +def test_requirement_check_override_policy_returns_structured_failure(check_overriding_profiles_path: str): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overriding_profile = next(item for item in profiles if item.identifier == "b") + + result = NoRequirementCheckOverrides().run(overriding_profile) + + assert not result.passed + assert result.check_id == "no-requirement-check-overrides" + assert result.details == { + "name": "Check S", + "severity": "REQUIRED", + "sources": "a", + } + + +def test_rule_overlay_sources_are_consistent(check_overriding_profiles_path: str): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overlay = next(item for item in profiles if item.identifier == "b") + + result = RuleOverlayConsistency().run(overlay) + + assert result.passed + + +def test_rule_overlay_source_must_be_a_direct_parent(check_overriding_profiles_path: str, monkeypatch): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overlay = next(item for item in profiles if item.identifier == "d") + original_rule_overlay_of = cast("Any", Profile.rule_overlay_of).fget + assert original_rule_overlay_of is not None + monkeypatch.setattr( + Profile, + "rule_overlay_of", + property( + lambda profile: ( + ["https://w3id.org/a"] + if profile.identifier == overlay.identifier + else original_rule_overlay_of(profile) + ) + ), + ) + + result = RuleOverlayConsistency().run(overlay) + + assert not result.passed + assert result.message == "Rule overlay source is not a direct parent" + assert result.details == {"source": "https://w3id.org/a"} + + +def test_multiple_rule_overlay_sources_require_distinct_check_identities( + check_overriding_profiles_path: str, + monkeypatch, +): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overlay = next(item for item in profiles if item.identifier == "y") + original_rule_overlay_of = cast("Any", Profile.rule_overlay_of).fget + assert original_rule_overlay_of is not None + monkeypatch.setattr( + Profile, + "rule_overlay_of", + property( + lambda profile: ( + ["https://w3id.org/e", "https://w3id.org/f"] + if profile.identifier == overlay.identifier + else original_rule_overlay_of(profile) + ) + ), + ) + + result = RuleOverlayConsistency().run(overlay) + + assert not result.passed + assert result.message == "Rule overlay sources contain an ambiguous check identity" + assert result.details["name"] == "Check S" + assert result.details["severity"] == "REQUIRED" def test_check_name_and_severity_match_parent_override(check_overriding_profiles_path: str): From 12485e8984873e35672c0be805ba7a8d8252215d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 25 Sep 2026 17:23:28 +0200 Subject: [PATCH 111/271] =?UTF-8?q?docs(api):=20=F0=9F=93=9D=20document=20?= =?UTF-8?q?composed=20profile=20check=20identities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/10_api.rst | 51 ++++++++++++++++++++++++++++++++++- rocrate_validator/services.py | 16 +++++------ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/docs/10_api.rst b/docs/10_api.rst index 715d3ca41..467d64fc5 100644 --- a/docs/10_api.rst +++ b/docs/10_api.rst @@ -75,7 +75,7 @@ The validator's core is built on the main set of classes shown in the following RO-Crate ----------------------------------------- -.. autoclass:: rocrate_validator.models.ROCrate +.. autoclass:: rocrate_validator.rocrate.ROCrate :members: Profiles, Requirements, and Checks @@ -116,6 +116,55 @@ Validation .. autoclass:: rocrate_validator.models.CheckIssue :members: +.. _api-composed-profile-identities: + +Composed profile check identities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a validation target uses ``prof:isProfileOf`` without an overlay, an +inherited check keeps the identifier and profile of the source that defines it. +When the target also declares ``validator:ruleOverlayOf``, the API exposes two +identities for checks reused from the overlaid source: + +* the **source identity** identifies the physical implementation and remains + unchanged; and +* the **effective identity** is the profile and identifier used to report the + check in the current validation. + +For example, if an inherited check from +``example-profile-1.2`` has source identifier +``example-profile-1.2_``, an overlay targeting +``example-profile-1.3`` reports it as: + +.. code-block:: json + + { + "identifier": "example-profile-1.3_", + "profile": "example-profile-1.3", + "source_identifier": "example-profile-1.2_", + "source_profile": "example-profile-1.2" + } + +The ``CheckIssue`` properties ``identifier`` and ``profile_identifier`` return +the effective identity. ``source_profile_identifier`` returns the profile that +physically defines the check. The same distinction is present in +``CheckIssue.to_dict()`` / ``to_json()`` through the ``identifier``, +``profile``, ``source_identifier`` and ``source_profile`` keys. + +For a check implementation redefined locally by 1.3, ``source_identifier`` +remains the physical identifier assigned to the 1.3 check, while the effective +identifier may reuse the relative suffix of the replaced 1.2 check. This keeps +the logical check stable without mutating either ``RequirementCheck`` object. + +``RequirementCheck.identifier`` always refers to the source identity. Code +that needs the context-local overlay identity should use +``ValidationContext.effective_check_identifier(check)`` and +``ValidationContext.effective_check_profile(check)``. + +``RequirementCheckValidationEvent`` exposes the same distinction through +``effective_identifier`` / ``effective_profile_identifier`` and +``source_identifier`` / ``source_profile_identifier``. + .. autoclass:: rocrate_validator.events.Event :members: diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 2462e440e..b8fbc89df 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -207,11 +207,11 @@ def get_profiles( :param allow_requirement_check_override: a flag to enable or disable the requirement check override (default: ``True``). If ``True``, the requirement check of a profile ``A`` can be overridden - by the requirement check of a profile extension ``B`` (i.e., when ``B extends A``) - if they share the same name. + by the requirement check of a direct profile extension ``B`` (i.e., when + ``B prof:isProfileOf A``) if they share the same name and severity. If ``False``, a profile extension ``B`` can only - add new requirements to the profile ``A`` (i.e., checks with name not present in ``A``) - and an error is raised if a check with the same name is found in both profiles. + add new requirements to the profile ``A`` (i.e., checks whose name and severity + pair is not present in ``A``), and an error is raised if an override is found. :type allow_requirement_check_override: bool :return: the list of profiles @@ -251,11 +251,11 @@ def get_profile( :param allow_requirement_check_override: a flag to enable or disable the requirement check override (default: ``True``). If ``True``, the requirement check of a profile ``A`` can be overridden - by the requirement check of a profile extension ``B`` (i.e., when ``B extends A``) - if they share the same name. + by the requirement check of a direct profile extension ``B`` (i.e., when + ``B prof:isProfileOf A``) if they share the same name and severity. If ``False``, a profile extension ``B`` can only - add new requirements to the profile ``A`` (i.e., checks with name not present in ``A``) - and an error is raised if a check with the same name is found in both profiles. + add new requirements to the profile ``A`` (i.e., checks whose name and severity + pair is not present in ``A``), and an error is raised if an override is found. :type allow_requirement_check_override: bool :return: the profile From 3dd0b793296a699312d621c14c23f75e7f5a768f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 25 Sep 2026 17:26:38 +0200 Subject: [PATCH 112/271] =?UTF-8?q?docs(profile):=20=F0=9F=93=9D=20explain?= =?UTF-8?q?=20profile=20composition=20mechanisms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/11_writing_a_profile.rst | 444 +++++++++++++++++++++++++++----- docs/12_validation_profiles.rst | 10 +- docs/_static/custom.css | 24 ++ docs/conf.py | 3 + 4 files changed, 422 insertions(+), 59 deletions(-) create mode 100644 docs/_static/custom.css diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 3a226eb43..23d847511 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -97,33 +97,323 @@ These instructions assume you are familiar with code development using Python an #. When your profile & tests are written, open a pull request to contribute it back to the repository! -Overriding inherited checks ---------------------------- - -When a profile inherits from another profile (via ``prof:isProfileOf`` / -``prof:isTransitiveProfileOf``), it automatically receives every check -declared by its ancestors. The validator additionally supports -**override-by-name**: a child profile can replace an inherited check by -declaring a new check with the **same name**. - -This allows an extension profile to *redefine* the content of an inherited -check — for example, to make a constraint stricter or looser, change its -severity, or, as described in the next section, fully deactivate it. +Writing validation checks +------------------------- -Override-by-name is enabled by default. It can be disabled via the -``allow_requirement_check_override`` validation setting (CLI / API), which -will raise an error on duplicate check names instead. +A validation profile can contain two kinds of executable checks. SHACL checks +describe constraints over the crate's RDF graph; Python checks implement +procedural logic for cases that are not conveniently expressed in SHACL. Both +types can be assigned a severity and can participate in profile inheritance, +overrides, overlays and deactivation. SHACL checks ^^^^^^^^^^^^ +A SHACL ``NodeShape`` or ``PropertyShape`` becomes a validation check. Its +``sh:name`` is the human-readable check name used when matching an inherited +check for an override. The shape's target and constraints determine what is +validated; ``sh:severity`` or the profile directory determines its severity. + +For example, this shape requires every ``schema:Dataset`` to have a ``name``: + +.. code-block:: turtle + + @prefix schema: . + @prefix sh: . + + + a sh:NodeShape ; + sh:targetClass schema:Dataset ; + sh:property [ + sh:path schema:name ; + sh:name "Root data entity has a name" ; + sh:minCount 1 ; + sh:severity sh:Violation ; + ] . + +Python checks +^^^^^^^^^^^^^ + +Python checks are declared with the ``@check`` decorator in a requirement +module. The decorator's ``name`` and severity identify the check in the same +way as ``sh:name`` and severity identify a SHACL check. The function receives +the validation context and returns the check result or raises a validation +error according to the Python check API. + +For example, the following requirement defines a Python check with the same +kind of identity as the SHACL example above: + +.. code-block:: python + + from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext + from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + @requirement(name="Root metadata checks") + class RootMetadataChecks(PyFunctionCheck): + @check(name="Root data entity has a name", severity=Severity.REQUIRED) + def check_name(self, context: ValidationContext) -> CheckResultValue: + # A check can be skipped before inspecting the crate. + if context.settings.metadata_only: + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + + root_entity = context.ro_crate.metadata.get_root_data_entity() + if root_entity is None: + # None is normalized to CheckResult.SKIPPED. Use it when the + # check has no applicable value to inspect; record_skip() is + # preferable when the caller can provide a reason. + return None + + if not root_entity.get_property("name"): + # A failed check normally records the diagnostic and returns + # False. The check framework associates the issue with self. + context.result.add_issue("The root data entity has no name", self) + return False + + # True is normalized to CheckResult.PASSED. + return True + +The return annotation is ``CheckResultValue`` because the framework accepts +three equivalent forms: ``True``/``False`` are normalized to passed/failed, +``CheckResult.SKIPPED`` (or the other ``CheckResult`` members) expresses an +explicit status, and ``None`` is normalized to skipped. When a check fails, +call ``context.result.add_issue(...)`` before returning ``False`` so that the +result contains an actionable diagnostic. When it is skipped, prefer +``context.record_skip(...)`` when a reason and category should be exposed to +API and CLI consumers. + +.. _profile-composition: + +Composing validation profiles +----------------------------- + +RO-Crate Validator provides four complementary mechanisms for composing the +rules of related profiles: + +#. **Ordinary inheritance** applies the checks of a source profile when + validating against a more specialized target profile. +#. A **check override** replaces one inherited implementation with a local one. +#. A **rule overlay** keeps reusing the source implementations but attributes + them to the target in validation output. +#. **Deactivation** explicitly suppresses an inherited check. + +These mechanisms can be combined, but each answers a different question: +which profiles participate, which implementation runs, how a check is +presented, or whether it runs at all. + +The examples below use ``example-profile-1.2`` as source profile ``A`` and +``example-profile-1.3`` as target profile ``B``. + +.. container:: profile-composition-table + + .. list-table:: Profile composition mechanisms + :class: profile-composition-mechanisms + :header-rows: 1 + :widths: 5 20 30 45 + + * - # + - Mechanism + - How it is selected + - Effect + * - 1 + - Ordinary inheritance + - ``B prof:isProfileOf A`` + - Applies the rules of both ``A`` and ``B`` while preserving their + source profiles and identifiers. + * - 2 + - Check override + - A check in ``B`` with the same ``(name, severity)`` as a check in + ``A`` + - Executes the implementation from ``B`` instead of the matching + implementation from ``A``. + * - 3 + - Rule overlay + - ``B prof:isProfileOf A`` plus ``B validator:ruleOverlayOf A`` + - Presents rules reused from ``A`` with effective profile and + identifiers based on ``B``, while retaining their source provenance. + * - 4 + - Deactivation + - A local deactivation declaration in ``B`` + - Skips the matching inherited check from ``A``. + +Replacing a bundled profile through ``--extra-profiles-path`` is not profile +composition: it loads a local profile in place of the predefined profile. See +:doc:`12_validation_profiles` for that separate mechanism. + +Ordinary profile inheritance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Suppose that target profile ``B`` declares ``B prof:isProfileOf A``. RO-Crate +Validator interprets this relationship as ordinary profile inheritance: ``B`` +is a specialization of ``A`` (in other words, ``B`` *is an* ``A`` in the +validator's conformance model). Consequently, validating a crate against ``B`` +means applying both the rules defined by ``B`` and those defined by ``A``. The +validator reuses ``A``'s SHACL and Python checks directly, without copying them +into ``B``'s directory. The reused rules, and therefore their identifiers, +remain attributed to profile ``A``. + +In the concrete example, most 1.2 rules still apply to 1.3, so profile 1.3 can +reuse them and define only its new or changed rules. Its ``profile.ttl`` +declares the inheritance relationship: + +.. code-block:: turtle + + @prefix prof: . + + + a prof:Profile ; + prof:hasToken "example-profile-1.3" ; + prof:isProfileOf . + +In this example, RO-Crate Validator treats +``example-profile-1.3 prof:isProfileOf example-profile-1.2`` as a validation +dependency. It does **not** copy the 1.2 directory into the 1.3 directory, nor +does it merge their descriptions into a single +:class:`~rocrate_validator.models.profile.Profile` object. When validating +against 1.3, the validator instead: + +#. resolves profile 1.2 and any ancestors it declares from the profiles + available to the current validation session; +#. loads 1.2 requirements from the 1.2 directory and 1.3 requirements from the + 1.3 directory; +#. runs those requirements against the same crate; and +#. keeps each requirement and check linked to the profile and source file that + defined it. + +Consequently, both profile directories must be available to the validator. The +1.3 directory only needs its own ``profile.ttl``, resources and new or changed +checks. Unchanged checks continue to be loaded from the 1.2 directory at +validation time. + +For example, suppose 1.2 defines these checks: + +* ``Metadata file is valid JSON``; +* ``Root data entity has a name``; and +* ``Root data entity has a license``. + +Suppose 1.3 adds a new ``Metadata uses the 1.3 JSON-LD context`` check. With +ordinary inheritance, a validation targeting 1.3 executes: + +* the new ``Metadata uses the 1.3 JSON-LD context`` check from 1.3; and +* the unchanged JSON, name and license checks directly from the 1.2 directory. + +The new context check and its identifier belong to profile 1.3. The reused +JSON, name and license checks and their identifiers remain attributed to +profile 1.2. No source file is copied and profile 1.2 is not modified. + +.. _profile-rule-overlays: + +Rule overlays +^^^^^^^^^^^^^ + +A rule overlay lets a target profile report inherited checks as part of its +own rule set without copying their implementations. It changes the effective +profile and identifiers used in validation output, not which checks are +selected or executed. Profile 1.3 declares 1.2 as both its parent and its +overlay source: + +.. code-block:: turtle + + @prefix prof: . + @prefix validator: . + + + a prof:Profile ; + prof:hasToken "example-profile-1.3" ; + prof:isProfileOf ; + validator:ruleOverlayOf . + +.. note:: + + ``validator:ruleOverlayOf`` is an RO-Crate Validator extension, not a + property defined by the W3C Profiles Vocabulary. + + +For example, the inherited ``Root data entity has a license`` check is still +implemented by the 1.2 object, but its validation issue and event use 1.3 as +the effective profile. Their source fields continue to identify profile 1.2, +so API consumers can distinguish where a check came from and how it is +reported. The underlying 1.2 object is not mutated. The corresponding result +and event fields are described in :ref:`api-composed-profile-identities`. + +When the local 1.3 context check overrides the inherited 1.2 context check, the +overlay also rebases the effective identifier. For example, +``example-profile-1.2_`` becomes +``example-profile-1.3_`` while the replacement's physical identifier +remains available as its source identifier. This preserves a stable relative +identity across profile versions. + +An overlay source must be a direct parent. It does not implicitly overlay that +parent's ancestors. + +If a target overlays more than one direct parent, those source profiles must +not define checks with the same ``(name, severity)`` pair. Otherwise, a local +check in the target could match more than one source check and the validator +could not determine which implementation it overrides. + +The source and target profiles remain separate: unchanged checks stay in the +source directory, and each check keeps using the ontology and ``sh:prefixes`` +resources from the profile that defined it. If a changed 1.3 term mapping +alters a check's meaning, redefine that check locally or provide mappings that +preserve its semantics. + +.. note:: + + **When to use a rule overlay** + + Use a rule overlay when: + + * the target represents a new version or close variant of the source + profile; + * most validation rules are semantically unchanged; and + * inherited rules should be reported as part of the target profile. + + If reused checks should remain visibly owned by 1.2, use ordinary + inheritance alone. Do not use an overlay to replace a bundled profile with + a local copy; use the separate ``--extra-profiles-path`` mechanism + described in :doc:`12_validation_profiles`. + +Overlay validation processes 1.3 checks before checks loaded from 1.2. In the +example, the 1.3 context check can reject incompatible metadata before an +inherited 1.2 check attempts to consume it during fail-fast validation. + +Overriding inherited checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Suppose 1.2 defines a ``Metadata uses the expected JSON-LD context`` check and +1.3 directly inherits 1.2 through ``prof:isProfileOf``. Profile 1.3 can replace +that check by declaring a local check with the same **name and severity**. This +is a **check override**: the validator executes the 1.3 implementation and +does not execute the matching 1.2 implementation. The mechanism works with +ordinary inheritance as well as rule overlays. + +The pair ``(name, severity)`` is the check identity. For example, if the 1.2 +context check has ``REQUIRED`` severity but the 1.3 check with the same name +has ``RECOMMENDED`` severity, no override occurs and both checks remain active. +To change severity intentionally, 1.3 must deactivate the original +``REQUIRED`` check and add a distinct ``RECOMMENDED`` check. + +An override is resolved against direct parents only. Suppose 1.2 inherits the +context check from a base profile and 1.3 only declares +``1.3 prof:isProfileOf 1.2``. A check declared locally by 1.3 cannot override +that base-profile check. Its defining base profile must also be listed as a +direct parent of 1.3. This restriction keeps override resolution deterministic +in multiple-inheritance hierarchies. + +Check override is enabled by default. If the API setting +``allow_requirement_check_override`` is set to ``False``, the matching 1.2 and +1.3 context checks cause an error instead of selecting the 1.3 implementation. + +Overriding SHACL checks +~~~~~~~~~~~~~~~~~~~~~~~ + Each SHACL ``NodeShape`` / ``PropertyShape`` becomes a check whose name is -its ``sh:name``. To override an inherited check, declare a shape in the -extension profile with the **same** ``sh:name`` as the inherited one: +its ``sh:name``. To override the inherited 1.2 check, declare a shape in 1.3 +with the **same** ``sh:name`` and severity: .. code-block:: turtle - # Parent profile + # Source profile A (example-profile-1.2) ro:ShapeC a sh:NodeShape ; sh:name "The Shape C" ; @@ -138,7 +428,7 @@ extension profile with the **same** ``sh:name`` as the inherited one: .. code-block:: turtle - # Extension profile — overrides the inherited PropertyShape by sh:name + # Target profile B (example-profile-1.3) -- same sh:name and severity ro:ShapeC a sh:NodeShape ; sh:name "The Shape C" ; @@ -149,65 +439,84 @@ extension profile with the **same** ``sh:name`` as the inherited one: sh:path rdf:type ; sh:minCount 1 ; sh:maxCount 1 ; - sh:message "Stricter override from extension profile" ; + sh:message "Stricter override from profile 1.3" ; ] . Both top-level shapes and ``PropertyShape`` entries nested inside a parent ``NodeShape`` (i.e., declared inline, without an absolute IRI) can be overridden this way. -Python checks -^^^^^^^^^^^^^ +Overriding Python checks +~~~~~~~~~~~~~~~~~~~~~~~~ Python checks declared via the ``@check`` decorator are matched by their -``name`` argument. To override an inherited Python check, declare a new -function with the same ``name`` in the extension profile: +``name`` and severity arguments. To override an inherited 1.2 Python check, +declare a new function with the same identity in profile 1.3: .. code-block:: python - # In the extension profile's checks module - from rocrate_validator.requirements.python import check - - @check(name="Check Metadata File Descriptor entity existence") - def overridden_check(self, ctx): - # New implementation that replaces the inherited one - ... + # In target profile B's checks module (example-profile-1.3) + from rocrate_validator.models import CheckResultValue, Severity, ValidationContext + from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + @requirement(name="Metadata context checks") + class MetadataContextChecks(PyFunctionCheck): + @check(name="Metadata uses the expected JSON-LD context", severity=Severity.REQUIRED) + def check_context(self, context: ValidationContext) -> CheckResultValue: + metadata = context.ro_crate.metadata.as_dict() + actual_context = metadata.get("@context") + expected_context = "https://w3id.org/ro/crate/1.3/context" + if actual_context != expected_context: + context.result.add_issue( + f"Expected JSON-LD context {expected_context!r}, got {actual_context!r}", + self, + ) + return False + return True + +The function keeps the inherited check's name and severity, so only its +implementation changes: profile 1.3 accepts the 1.3 context while the 1.2 +implementation is not executed. + +Rule overlays and check overrides are complementary, not interchangeable. An +overlay determines the effective profile and identifier used for an inherited +check; an override determines which implementation is executed when the target +redefines that check. Deactivating inherited checks ------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A child profile can also **fully deactivate** a check inherited from one of -its ancestors. A deactivated check is skipped during validation and -reported as such in the validation result. This is useful when an extension -profile relaxes the parent's expectations, or replaces a coarse-grained -check with a more specific one declared elsewhere in the same profile. +Suppose a 1.2 check no longer applies to 1.3, rather than merely requiring a +different implementation. Profile 1.3 can **fully deactivate** that inherited +check. The validator then skips it and records it as deactivated in the +validation result. This is useful when 1.3 relaxes a 1.2 expectation or +replaces a coarse-grained 1.2 check with more specific 1.3 checks. -SHACL checks -^^^^^^^^^^^^ +Deactivating SHACL checks +~~~~~~~~~~~~~~~~~~~~~~~~~ Two complementary mechanisms are supported, depending on whether the shape to disable has an absolute IRI of its own. **Shape with an absolute IRI** (e.g. a top-level ``NodeShape`` or a named -``PropertyShape``): reference the shape by IRI from the extension profile -and mark it as deactivated, without redeclaring it. +``PropertyShape``): profile 1.3 references the 1.2 shape by IRI and marks it as +deactivated, without redeclaring it. .. code-block:: turtle - # Extension profile - sh:deactivated true . + # Target profile B (example-profile-1.3) + sh:deactivated true . **Nested ``PropertyShape`` without an absolute IRI** (a property declared -inline inside a parent ``NodeShape``): use the override-by-name mechanism +inline inside a parent ``NodeShape``): use the check override mechanism described in the previous section. Declare a new ``PropertyShape`` in the -extension profile with the same ``sh:name`` as the one to disable, and set -``sh:deactivated true`` on it. This overrides the parent's -``PropertyShape``, and the validator reports the resulting check as -deactivated. +1.3 profile with the same identity as the one to disable, and set +``sh:deactivated true`` on it. This overrides the 1.2 ``PropertyShape``, and +the validator reports the resulting 1.3 check as deactivated. .. code-block:: turtle - # Extension profile — disables the inherited PropertyShape by sh:name + # Target profile B (example-profile-1.3) -- disables the inherited check ro:ShapeC a sh:NodeShape ; sh:name "The Shape C" ; @@ -221,30 +530,49 @@ deactivated. .. note:: - Cross-profile deactivation is scoped to the shape's transitive - descendants: a ``sh:deactivated true`` triple declared by a profile - that does not inherit (directly or transitively) from the shape's - owning profile is ignored. This prevents unrelated profiles loaded in - the same process from interfering with one another. + Profile 1.3 can deactivate the 1.2 shape because it is a descendant of 1.2. + If an unrelated profile ``C`` declares the same ``sh:deactivated true`` + triple but does not inherit from 1.2, the declaration is ignored. This + prevents unrelated profiles loaded in the same process from interfering + with one another. -Python checks -^^^^^^^^^^^^^ +Deactivating Python checks +~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``@check`` decorator accepts a ``deactivated`` flag, mirroring SHACL's -``sh:deactivated``. Combined with override-by-name, an extension profile -can disable an inherited Python check by redeclaring it with +``sh:deactivated``. Combined with check override, profile 1.3 can disable an +inherited 1.2 Python check by redeclaring it with ``deactivated=True``: .. code-block:: python from rocrate_validator.requirements.python import check + # In target profile B's checks module (example-profile-1.3) @check(name="Check Metadata File Descriptor entity existence", deactivated=True) def disabled(self, ctx): # Body is irrelevant — the check is skipped during validation. return True +Checking a profile during development +------------------------------------- + +Run the profile consistency checks before publishing a new or overlaid +profile: + +.. code-block:: bash + + rocrate-validator profiles check example-profile-1.3 + +The command checks the selected profile and its inherited profiles and reports +each consistency check as ``PASS`` or ``FAIL``. In particular, it detects +duplicate ``(name, severity)`` identities within a profile, invalid or unloaded +overlay sources, and ambiguous identities contributed by multiple overlay +sources. Validation runs perform these profile checks automatically; +``validate --no-profile-checks`` can disable that automatic step when +necessary. + Running validator & tests during profile development ---------------------------------------------------- diff --git a/docs/12_validation_profiles.rst b/docs/12_validation_profiles.rst index 4902050e8..9d093382a 100644 --- a/docs/12_validation_profiles.rst +++ b/docs/12_validation_profiles.rst @@ -45,10 +45,18 @@ Behavior ^^^^^^^^ * Profiles provided via ``--extra-profiles-path`` are **loaded in addition to** the system’s predefined profiles. -* If an additional profile has the **same name** as a predefined profile, the additional profile **overrides** the predefined one. +* If an additional profile has the **same profile URI** as a predefined + profile, the additional profile **overrides** the predefined one. This mechanism allows you to: * **Add new custom validation profiles** to implement project-specific checks. * **Modify existing profiles** without altering the system’s predefined configuration files. * **Maintain a clear separation** between standard validation logic and project-specific customizations. + +.. note:: + + This is whole-profile replacement: a locally loaded profile takes the place + of a predefined profile with the same URI. It is different from a + :ref:`profile rule overlay `, which composes a target + profile from an inherited source profile and only redefines selected checks. diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 000000000..ab81ff48f --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,24 @@ +/* Keep the composition summary within the documentation content column. */ +.rst-content .profile-composition-table { + width: 100%; +} + +.rst-content .profile-composition-table .wy-table-responsive { + overflow-x: visible; +} + +.rst-content .profile-composition-table table.profile-composition-mechanisms { + table-layout: fixed; + width: 100%; +} + +.rst-content .profile-composition-table th, +.rst-content .profile-composition-table td { + overflow-wrap: break-word; + white-space: normal; +} + +.rst-content .profile-composition-table th:first-child, +.rst-content .profile-composition-table td:first-child { + text-align: center; +} diff --git a/docs/conf.py b/docs/conf.py index 9f2896fe5..2402e9861 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -117,6 +117,9 @@ # html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +html_css_files = ["custom.css"] + autodoc_member_order = "bysource" autosummary_generate = True From 4beec92cedc2b4759b95c5ff4ce414b260f16ca2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 25 Sep 2026 17:28:08 +0200 Subject: [PATCH 113/271] =?UTF-8?q?fix(docs):=20=F0=9F=A9=B9=20align=20REA?= =?UTF-8?q?DME=20include=20line=20ranges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/1_installation.rst | 2 +- docs/2_usage_cli.rst | 2 +- docs/3_usage_api.rst | 2 +- docs/ack.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/1_installation.rst b/docs/1_installation.rst index 6a476edf9..eba3d6ec3 100644 --- a/docs/1_installation.rst +++ b/docs/1_installation.rst @@ -28,5 +28,5 @@ Installation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 33 + :start-line: 34 :end-line: 92 diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 62951f5b1..b9bb522c8 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -29,7 +29,7 @@ CLI Validation .. Include the README section for the CLI validation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 93 + :start-line: 94 :end-line: 120 .. seealso:: diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index aa86c0c40..ad034a9eb 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -27,7 +27,7 @@ Programmatic Validation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 121 + :start-line: 122 :end-line: 162 .. seealso:: diff --git a/docs/ack.rst b/docs/ack.rst index cc486da36..e8d5b726e 100644 --- a/docs/ack.rst +++ b/docs/ack.rst @@ -20,4 +20,4 @@ Acknowledgements .. "## Acknowledgements" heading is skipped: the page title above replaces it). .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 182 + :start-line: 227 From a73f90da12b10eca5681497d20cc9432f145245f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 11:39:45 +0200 Subject: [PATCH 114/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20preserve=20source-first=20overlay=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/requirement.py | 73 +++++++++++++++++++++++-- rocrate_validator/models/validation.py | 31 ++++++++--- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index c1dc9867b..12ae1e0ab 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -359,18 +359,81 @@ def __dependency_skip_reason__(check, context: ValidationContext) -> str | None: def __record_dependency_skip__(check, context: ValidationContext, message: str) -> None: Requirement.__record_skipped_check__(check, context, message, SkipCategory.DEPENDENCY) - def __execute_check__(self, check, context, all_passed): - from rocrate_validator.models.events import ( # noqa: PLC0415 - RequirementCheckValidationEvent, - ) + @staticmethod + def __resolve_execution_check__( + check: RequirementCheck, + context: ValidationContext, + ) -> RequirementCheck | None: + """ + Resolve the check that must execute in the current source-profile slot. - if check.overridden and check.requirement.profile.identifier != context.profile_identifier: + Ordinary checks are returned unchanged. When a source Python check is + replaced by the active overlay, its target-local replacement is returned + so it runs at the same point in the general-to-specific traversal. SHACL + replacements return ``None`` because their shapes are collected across + profiles and evaluated together when validation reaches the target. + + ``None`` also indicates that the replacement has already run or that no + replacement belonging to the active target can be resolved. + """ + if not check.overridden or check.requirement.profile.identifier == context.profile_identifier: + return check + + from rocrate_validator.requirements.python import PyFunctionCheck # noqa: PLC0415 + + # Python checks execute independently, so run the target replacement + # in the source check's slot to preserve general-to-specific ordering. + # SHACL checks are different: their shapes are collected across all + # profiles and evaluated once at the target profile. Executing a SHACL + # replacement here would start that validation too early. + if not isinstance(check, PyFunctionCheck): + return None + + replacement = context.effective_check_replacement(check) + if replacement is None: logger.debug( "Skipping check '%s' because overridden by '%r'", check.identifier, [_.identifier for _ in check.overridden_by], ) + return None + if context.result.get_check_result(replacement) is not None: + return None + logger.debug( + "Executing target replacement '%s' at source check '%s'", + replacement.identifier, + check.identifier, + ) + return replacement + + def __execute_check__( + self, + check: RequirementCheck, + context: ValidationContext, + all_passed: bool, + ) -> tuple[bool, bool]: + """ + Execute one effective check and update the enclosing requirement state. + + Overlay replacements are resolved before execution, and a replacement + already executed in its source slot is not run again when the target + profile is visited. The returned tuple contains the accumulated pass + state and whether fail-fast processing must stop after this check. + """ + from rocrate_validator.models.events import ( # noqa: PLC0415 + RequirementCheckValidationEvent, + ) + + # An overlay replacement may already have run in the source profile's + # slot. Keep the target pass a no-op instead of executing the same + # Python check twice. + if context.result.get_check_result(check) is not None: + return all_passed, False + + execution_check = self.__resolve_execution_check__(check, context) + if execution_check is None: return all_passed, False + check = execution_check if check.deactivated: logger.debug("Skipping check '%s' because deactivated", check.identifier) self.__record_skipped_check__(check, context, "Check is deactivated", SkipCategory.DEACTIVATED) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index c9b06ddad..555d4d1b7 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -648,14 +648,12 @@ def __load_profiles__(self) -> list[Profile]: if not self.inheritance_enabled: return [profile] - # Validate overlay profiles before their sources. An overlay contains - # the checks whose behavior changed locally; those checks must get the - # first chance to fail (for example, malformed JSON must be reported by - # the target profile before an inherited metadata check parses it). - if profile.rule_overlay_of: - profiles = [profile, *profile.inherited_profiles] - else: - profiles = [*profile.inherited_profiles, profile] + # Visit profiles from the most general source to the target. This is + # important for both dependency ordering and SHACL composition: source + # shapes are collected before the target profile performs the single + # validation run over the complete merged shapes graph. Python checks + # that are replaced by an overlay are dispatched at their source slot. + profiles = [*profile.inherited_profiles, profile] # Validate check identities only for profiles participating in this run. # Profile listing and discovery may still inspect intentionally invalid @@ -737,6 +735,23 @@ def effective_check_profile(self, check: RequirementCheck) -> Profile: self.target_profile if self.is_rule_overlay_source(check.requirement.profile) else check.requirement.profile ) + def effective_check_replacement(self, check: RequirementCheck) -> RequirementCheck | None: + """ + Return the target-local check replacing ``check``, if any. + + Replacement is resolved against the effective target profile rather + than the source profile's sibling list. This keeps execution scoped + to the profile selected for the current validation and avoids treating + unrelated profiles loaded in the same registry as active overrides. + """ + target_profile = self.target_profile + if check.requirement.profile == target_profile: + return None + candidate = target_profile.get_requirement_check(check.name, check.severity) + if candidate is not None and check in candidate.overrides: + return candidate + return None + def get_profile_by_token(self, token: str) -> list[Profile]: """ Get the profile by token from the profiles to validate against From 996b52aa561acb6c00f0c24c911f57bd409919b3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:22:49 +0200 Subject: [PATCH 115/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20resolve=20inherited=20overlay=20check=20dependencie?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/requirement.py | 83 ++++++++++++++++++++----- rocrate_validator/models/validation.py | 18 ++++++ 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 12ae1e0ab..57ceab9a1 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -334,18 +334,20 @@ def record_skipped_checks(cls, requirements: list[Requirement], context: Validat cls.__record_skipped_checks__(requirement.get_checks(), context, message, SkipCategory.NOT_REACHED) @staticmethod - def __dependency_skip_reason__(check, context: ValidationContext) -> str | None: + def __dependency_skip_reason__(check: RequirementCheck, context: ValidationContext) -> str | None: + """ + Explain why ``check`` cannot run because a dependency did not pass. + + Dependency lookup is delegated to the validation context so an overlay + can resolve inherited checks and target-local replacements. ``None`` + means that every declared dependency has already passed. + """ if not check.depends_on: return None blocked_dependencies = [] for dependency_name in check.depends_on: - dependency = check.requirement.profile.get_requirement_check(dependency_name) - if dependency is None: - raise CheckDependencyError( - f"check {check.name!r} depends on unknown check {dependency_name!r}", - check.requirement.profile.identifier, - ) + dependency = context.resolve_dependency_check(check, dependency_name) dependency_result = context.result.get_check_result(dependency) if dependency_result is not CheckResult.PASSED: result_name = dependency_result.value if dependency_result else "not processed" @@ -664,7 +666,7 @@ def ok_file(p: Path) -> bool: ), reverse=False, ) - requirements = RequirementLoader.order_by_dependencies(requirements) + requirements = RequirementLoader.order_by_dependencies(requirements, profile=profile) # assign order numbers to requirements for i, requirement in enumerate(requirements): requirement._order_number = i + 1 @@ -680,6 +682,28 @@ def _check_index(requirements: list[Requirement]) -> dict[str, list[RequirementC checks_by_name.setdefault(check.name, []).append(check) return checks_by_name + @classmethod + def effective_check_index(cls, profile: Profile) -> dict[str, list[RequirementCheck]]: + """ + Index the checks active in ``profile`` after inherited replacements. + + Checks are collected from the target and all inherited profiles. A + source check is excluded only when another check in this composition + explicitly overrides it; unrelated matches remain in the index so a + name-only dependency is correctly rejected as ambiguous. + """ + profiles = (profile, *profile.inherited_profiles) + requirements = [requirement for source_profile in profiles for requirement in source_profile.requirements] + checks = [check for requirement in requirements for check in requirement.get_checks()] + check_ids = {id(check) for check in checks} + shadowed_ids = { + id(overridden) for check in checks for overridden in check.overrides if id(overridden) in check_ids + } + return { + name: [check for check in matches if id(check) not in shadowed_ids] + for name, matches in cls._check_index(requirements).items() + } + @staticmethod def _resolve_dependency( check: RequirementCheck, @@ -728,8 +752,21 @@ def _topological_order( return [nodes[index] for index in ordered_indices] @classmethod - def order_by_dependencies(cls, requirements: list[Requirement]) -> list[Requirement]: - """Apply check dependencies while preserving the baseline requirement order.""" + def order_by_dependencies( + cls, + requirements: list[Requirement], + *, + profile: Profile | None = None, + ) -> list[Requirement]: + """ + Order local checks and requirements according to their dependencies. + + Dependencies within ``requirements`` produce topological-order edges. + A dependency supplied by an inherited profile is already guaranteed to + execute earlier in the profile traversal, so it is accepted without + adding a local edge. Unknown local dependencies and cycles still fail + profile loading. + """ if not requirements: return [] @@ -743,8 +780,20 @@ def order_by_dependencies(cls, requirements: list[Requirement]) -> list[Requirem check_edges: dict[int, set[int]] = {} for check in checks: for dependency_name in check.depends_on: + inherited_dependency = ( + dependency_name not in checks_by_name + and profile is not None + and any( + inherited.get_requirement_check(dependency_name) is not None + for inherited in profile.inherited_profiles + ) + ) + if inherited_dependency: + continue dependency = cls._resolve_dependency(check, dependency_name, checks_by_name) - dependency_requirement_index = requirement_indices[id(dependency.requirement)] + dependency_requirement_index = requirement_indices.get(id(dependency.requirement)) + if dependency_requirement_index is None: + continue source_requirement_index = requirement_indices[id(requirement)] if dependency_requirement_index == source_requirement_index: check_edges.setdefault(check_indices[id(dependency)], set()).add(check_indices[id(check)]) @@ -774,7 +823,14 @@ def dependency_closure( *, include_dependencies: bool = True, ) -> list[Requirement]: - """Return selected requirements plus their transitive check dependencies.""" + """ + Return selected requirements plus transitive effective dependencies. + + Dependency names are resolved against the composed profile, allowing a + target-local requirement to pull in requirements declared by inherited + profiles. When ``include_dependencies`` is false, the original + selection is returned in dependency-safe order without expanding it. + """ selected: list[Requirement] = [] selected_ids: set[int] = set() queue = list(requirements) @@ -786,8 +842,7 @@ def dependency_closure( selected_ids.add(id(requirement)) selected.append(requirement) - profile_requirements = requirement.profile.requirements - checks_by_name = cls._check_index(profile_requirements) + checks_by_name = cls.effective_check_index(requirement.profile) for check in requirement.get_checks(): for dependency_name in check.depends_on: dependency = cls._resolve_dependency(check, dependency_name, checks_by_name) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 555d4d1b7..80ae79e31 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -21,6 +21,7 @@ from rdflib import Graph from rocrate_validator.errors import ( + CheckDependencyError, ProfileNotFound, ROCrateMetadataNotFoundError, ) @@ -752,6 +753,23 @@ def effective_check_replacement(self, check: RequirementCheck) -> RequirementChe return candidate return None + def resolve_dependency_check(self, check: RequirementCheck, dependency_name: str) -> RequirementCheck: + """ + Resolve a named dependency in the active overlay composition. + + Resolution uses the target profile's effective check set, which includes + inherited checks while excluding definitions shadowed by a more specific + replacement. Exactly one effective match is required; an absent or + ambiguous dependency is a profile-definition error. + """ + matches = RequirementLoader.effective_check_index(self.target_profile).get(dependency_name, []) + if len(matches) != 1: + raise CheckDependencyError( + f"check {check.name!r} depends on unknown or ambiguous check {dependency_name!r}", + check.requirement.profile.identifier, + ) + return matches[0] + def get_profile_by_token(self, token: str) -> list[Profile]: """ Get the profile by token from the profiles to validate against From 709e75641dfc2ed8115154d7ff15f0578d5d252a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:26:39 +0200 Subject: [PATCH 116/271] =?UTF-8?q?fix(validation):=20=F0=9F=9B=A1?= =?UTF-8?q?=EF=B8=8F=20honor=20effective=20overlay=20check=20skips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/requirement.py | 6 +---- rocrate_validator/models/validation.py | 14 +++++++++++ .../requirements/shacl/validator.py | 25 ++++++++++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 57ceab9a1..96af9af68 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -210,11 +210,7 @@ def _do_validate_(self, context: ValidationContext) -> bool: len(self._checks), ) all_passed = True - checks_to_perform = [ - _ - for _ in self._checks - if not context.settings.skip_checks or _.identifier not in context.settings.skip_checks - ] + checks_to_perform = [check for check in self._checks if not context.is_check_skipped(check)] configured_skips = [check for check in self._checks if check not in checks_to_perform] for check in configured_skips: context.result._record_check_result( diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 80ae79e31..d397b146d 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -736,6 +736,20 @@ def effective_check_profile(self, check: RequirementCheck) -> Profile: self.target_profile if self.is_rule_overlay_source(check.requirement.profile) else check.requirement.profile ) + def is_check_skipped(self, check: RequirementCheck) -> bool: + """ + Return whether validation settings skip ``check`` under a supported identity. + + A check contributed by an overlay source retains its source identifier + while exposing a target-local effective identifier. Accepting either + identity lets callers address the check without mutating its provenance. + """ + skipped_identifiers = self.settings.skip_checks + if not skipped_identifiers: + return False + check_identifiers = {check.identifier, self.effective_check_identifier(check)} + return bool(check_identifiers.intersection(skipped_identifiers)) + def effective_check_replacement(self, check: RequirementCheck) -> RequirementCheck | None: """ Return the target-local check replacing ``check``, if any. diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 74eb48cf0..9dbd2332d 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -123,6 +123,15 @@ def __init__(self, context: ValidationContext): self._ontology_graph: Graph = Graph() def __set_current_validation_profile__(self, profile: Profile) -> bool: + """ + Load ``profile`` into the shared SHACL context when not yet processed. + + Ontology and shape graphs are accumulated across the validation + composition. Shapes configured as skipped, or replaced by active + overrides, are removed before the profile registry is merged. + + :return: ``True`` when the profile was loaded, otherwise ``False`` + """ if profile.identifier not in self._processed_profiles: # augment the ontology graph with the profile ontology ontology_graph = self.__load_ontology_graph__(profile.path) @@ -134,15 +143,23 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: profile_shapes_graph = profile_registry.shapes_graph logger.debug("Loaded shapes: %s", profile_shapes) - # enable overriding of checks - if self.settings.allow_requirement_check_override: + # Filter shapes that must not participate in the combined SHACL run. + if self.settings.skip_checks or self.settings.allow_requirement_check_override: from rocrate_validator.requirements.shacl.requirements import SHACLRequirement # noqa: PLC0415 for requirement in [_ for _ in profile.requirements if isinstance(_, SHACLRequirement)]: for check in requirement.get_checks(): - if check.overridden and check.requirement.profile != self.target_profile: + if self.base_context.is_check_skipped(check): + profile_shapes_graph -= cast("Any", check).shape.graph + profile_shapes.pop(cast("Any", check).shape.key, None) + continue + if ( + self.settings.allow_requirement_check_override + and check.overridden + and check.requirement.profile != self.target_profile + ): profile_shapes_graph -= cast("Any", check).shape.graph - profile_shapes.pop(cast("Any", check).shape.key) + profile_shapes.pop(cast("Any", check).shape.key, None) # add the shapes to the registry self._shapes_registry.extend(profile_shapes, profile_shapes_graph) From 22455d3be43fc6425ddb76161d8d67d7ed53b660 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:37:07 +0200 Subject: [PATCH 117/271] =?UTF-8?q?fix(shacl):=20=F0=9F=9B=A1=EF=B8=8F=20l?= =?UTF-8?q?oad=20overlay=20profile=20graphs=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/shacl/validator.py | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 9dbd2332d..6daac3f0c 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -15,7 +15,6 @@ # pylint: disable=cyclic-import # lazy imports break the cycle at runtime (see PLC0415 noqa markers in requirements) from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING, Any, cast # pylint: disable=unused-import import pyshacl @@ -23,6 +22,8 @@ from rdflib.term import Node, URIRef if TYPE_CHECKING: + from pathlib import Path + from pyshacl.pytypes import GraphLike from rocrate_validator.constants import ( @@ -102,10 +103,11 @@ def check(self) -> RequirementCheck: class SHACLValidationContext(ValidationContext): def __init__(self, context: ValidationContext): + """Initialize a SHACL context shared by all profiles in ``context``.""" super().__init__(context.validator, context.settings) self._base_context: ValidationContext = context - # reference to the ontology path - self._ontology_path: Path | None = None + # ontology paths resolved for each profile and optional filename + self._ontology_paths: dict[tuple[Path, str], Path] = {} # reference to the contextual ShapeRegistry instance self._shapes_registry: ShapesRegistry = ShapesRegistry() @@ -113,6 +115,11 @@ def __init__(self, context: ValidationContext): # processed profiles self._processed_profiles: dict[str, bool] = {} + # profiles whose ontology and shapes have already been merged. A + # profile may expose several SHACL checks, but its graphs must be + # loaded only once before the combined validation is executed. + self._loaded_profiles: set[str] = set() + # reference to the current validation profile self._current_validation_profile: Profile | None = None @@ -124,15 +131,22 @@ def __init__(self, context: ValidationContext): def __set_current_validation_profile__(self, profile: Profile) -> bool: """ - Load ``profile`` into the shared SHACL context when not yet processed. + Select ``profile`` and load its graphs once into the shared context. - Ontology and shape graphs are accumulated across the validation - composition. Shapes configured as skipped, or replaced by active - overrides, are removed before the profile registry is merged. + Loading and processing are separate states: source-profile checks may + enter this context repeatedly while their execution is deferred to the + target profile. Ontology and shape graphs are therefore merged only on + the first visit, whereas selecting an already processed profile raises + :class:`SHACLValidationAlreadyProcessed`. - :return: ``True`` when the profile was loaded, otherwise ``False`` + :return: ``True`` when the profile is ready to be processed + :raises SHACLValidationAlreadyProcessed: If validation already ran for + ``profile`` """ - if profile.identifier not in self._processed_profiles: + if profile.identifier in self._processed_profiles: + raise SHACLValidationAlreadyProcessed(profile.identifier, self.get_validation_result(profile)) + + if profile.identifier not in self._loaded_profiles: # augment the ontology graph with the profile ontology ontology_graph = self.__load_ontology_graph__(profile.path) if ontology_graph: @@ -163,12 +177,12 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: # add the shapes to the registry self._shapes_registry.extend(profile_shapes, profile_shapes_graph) - # set the current validation profile - self._current_validation_profile = profile - # return True if the profile should be processed - return True - # return False if the profile has already been processed - return False + self._loaded_profiles.add(profile.identifier) + + # Select the profile on every entry: repeated source-profile checks are + # deferred individually even though their graphs are already loaded. + self._current_validation_profile = profile + return True def __unset_current_validation_profile__(self) -> None: self._current_validation_profile = None @@ -210,9 +224,11 @@ def shapes_graph(self) -> Graph: return self.shapes_registry.shapes_graph def __get_ontology_path__(self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE) -> Path: - if not self._ontology_path: - self._ontology_path = Path(f"{profile_path}/{ontology_filename}") - return self._ontology_path + """Return the cached ontology path for a profile and filename pair.""" + key = (profile_path, ontology_filename) + if key not in self._ontology_paths: + self._ontology_paths[key] = profile_path / ontology_filename + return self._ontology_paths[key] def __get_data_graph_base__(self) -> str | None: """ From 44b6cee4ec6197e11de385dff41d952ff6cb4503 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:38:47 +0200 Subject: [PATCH 118/271] =?UTF-8?q?test(profile):=20=E2=9C=85=20preserve?= =?UTF-8?q?=20source-first=20overlay=20traversal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/requirements/test_profiles.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 761b8c4d2..f15db31ad 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -561,6 +561,21 @@ def test_rule_overlay_effective_identity_is_context_local(check_overriding_profi assert suppressed_context.result.statistics.total_checks == context.result.statistics.total_checks +def test_rule_overlay_keeps_source_before_target(check_overriding_profiles_path: str): + """Verify that overlay composition retains general-to-specific traversal.""" + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + ) + + context = ValidationContext(Validator(settings), settings) + + assert [profile.identifier for profile in context.profiles] == ["a", "b"] + + def test_normally_inherited_check_keeps_source_identity(check_overriding_profiles_path: str): settings = ValidationSettings( profiles_path=Path(check_overriding_profiles_path), From e80460d4b2a8ba52f32bca3f9f60145a8edc7a29 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:44:59 +0200 Subject: [PATCH 119/271] =?UTF-8?q?test(validation):=20=E2=9C=85=20cover?= =?UTF-8?q?=20inherited=20overlay=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/requirements/test_dependencies.py | 44 ++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/unit/requirements/test_dependencies.py b/tests/unit/requirements/test_dependencies.py index 508877086..cd3a83824 100644 --- a/tests/unit/requirements/test_dependencies.py +++ b/tests/unit/requirements/test_dependencies.py @@ -15,15 +15,32 @@ import pytest from rocrate_validator.errors import CheckDependencyError -from rocrate_validator.models import CheckResult, RequirementCheck, RequirementLoader +from rocrate_validator.models import CheckResult, RequirementCheck, RequirementLoader, Severity class _Profile: - identifier = "test" - - def __init__(self): + def __init__(self, identifier: str = "test", parents: tuple["_Profile", ...] = ()) -> None: + """Create a minimal profile with optional inherited parents.""" + self.identifier = identifier + self.parents = list(parents) self.requirements = [] + @property + def inherited_profiles(self) -> list["_Profile"]: + """Return the profiles contributing checks to this test profile.""" + return self.parents + + def get_requirement_check(self, name: str, severity: Severity | None = None) -> RequirementCheck | None: + """Return the unique local check matching ``name`` and ``severity``.""" + matches = [ + check + for requirement in self.requirements + for check in requirement.get_checks() + if check.name == name and (severity is None or check.severity == severity) + ] + assert len(matches) <= 1 + return matches[0] if matches else None + class _Requirement: def __init__(self, profile, name): @@ -79,6 +96,25 @@ def test_order_by_dependencies_reorders_requirements(): assert base.depends_on == () +def test_inherited_dependency_is_resolved_from_effective_profile() -> None: + """Resolve a target check dependency supplied by an overlay source.""" + source = _Profile("source") + target = _Profile("target", parents=(source,)) + source_requirement = _Requirement(source, "Source") + target_requirement = _Requirement(target, "Target") + dependency = _check(source_requirement, "base") + _check(target_requirement, "dependent", ("base",)) + + assert RequirementLoader.order_by_dependencies([target_requirement], profile=target) == [target_requirement] + + closure = RequirementLoader.dependency_closure([target_requirement]) + assert set(closure) == {source_requirement, target_requirement} + assert RequirementLoader.effective_check_index(target)["base"] == [dependency] + + with pytest.raises(CheckDependencyError, match="was not selected"): + RequirementLoader.dependency_closure([target_requirement], include_dependencies=False) + + @pytest.mark.parametrize( ("dependency_name", "message"), [ From 681461a6119b894fd9cc41249d16813d21515d33 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:48:17 +0200 Subject: [PATCH 120/271] =?UTF-8?q?test(validation):=20=E2=9C=85=20cover?= =?UTF-8?q?=20overlay=20check=20skip=20identities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/requirements/test_profiles.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index f15db31ad..e700ca2b3 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -576,6 +576,35 @@ def test_rule_overlay_keeps_source_before_target(check_overriding_profiles_path: assert [profile.identifier for profile in context.profiles] == ["a", "b"] +def test_rule_overlay_check_can_be_skipped_by_source_or_effective_identifier( + check_overriding_profiles_path: str, +) -> None: + """Accept either check identity when configuring an overlay skip.""" + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + source_profile = next(profile for profile in context.profiles if profile.identifier == "a") + source_check = source_profile.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert source_check is not None + supported_identifiers = ( + source_check.identifier, + context.effective_check_identifier(source_check), + ) + + for identifier in supported_identifiers: + skip_settings = replace(settings, skip_checks=[identifier]) + skip_context = ValidationContext(Validator(skip_settings), skip_settings) + skip_source = next(profile for profile in skip_context.profiles if profile.identifier == "a") + skip_check = skip_source.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert skip_check is not None + assert skip_context.is_check_skipped(skip_check) + + def test_normally_inherited_check_keeps_source_identity(check_overriding_profiles_path: str): settings = ValidationSettings( profiles_path=Path(check_overriding_profiles_path), From 37e8c119c5ead3e8df80e1c0c7948d4a91bf9723 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:51:31 +0200 Subject: [PATCH 121/271] =?UTF-8?q?test(shacl):=20=E2=9C=85=20cover=20one-?= =?UTF-8?q?time=20overlay=20graph=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/requirements/test_profiles.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index e700ca2b3..a3492790f 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -41,6 +41,10 @@ from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry +from rocrate_validator.requirements.shacl.validator import ( + SHACLValidationAlreadyProcessed, + SHACLValidationContext, +) from tests.ro_crates import InvalidFileDescriptorEntity, ValidROC # set up logging @@ -605,6 +609,43 @@ def test_rule_overlay_check_can_be_skipped_by_source_or_effective_identifier( assert skip_context.is_check_skipped(skip_check) +def test_shacl_overlay_profiles_are_loaded_once_before_processing( + check_overriding_profiles_path: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Load every overlay profile graph once while allowing deferred re-entry.""" + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + ) + context = ValidationContext(Validator(settings), settings) + source = next(profile for profile in context.profiles if profile.identifier == "a") + target = next(profile for profile in context.profiles if profile.identifier == "b") + shacl_context = SHACLValidationContext.get_instance(context) + loaded_paths: list[Path] = [] + + def load_ontology(profile_path: Path) -> Graph: + """Record ontology loads without parsing external graph content.""" + loaded_paths.append(profile_path) + return Graph() + + monkeypatch.setattr(shacl_context, "__load_ontology_graph__", load_ontology) + + assert shacl_context.__set_current_validation_profile__(source) + assert shacl_context.__set_current_validation_profile__(source) + assert shacl_context.__set_current_validation_profile__(target) + assert loaded_paths == [source.path, target.path] + assert shacl_context.__get_ontology_path__(source.path) == source.path / "ontology.ttl" + assert shacl_context.__get_ontology_path__(target.path) == target.path / "ontology.ttl" + + shacl_context.current_validation_result = True + with pytest.raises(SHACLValidationAlreadyProcessed): + shacl_context.__set_current_validation_profile__(target) + + def test_normally_inherited_check_keeps_source_identity(check_overriding_profiles_path: str): settings = ValidationSettings( profiles_path=Path(check_overriding_profiles_path), From 9299997171c927b1b1d3045bd85c2e1e6f79eef1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 15:53:51 +0200 Subject: [PATCH 122/271] =?UTF-8?q?test(validation):=20=E2=9C=85=20adapt?= =?UTF-8?q?=20test=20contexts=20to=20validation=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_dependency_propagation.py | 11 +++++++++++ tests/unit/test_requirement_lifecycle.py | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/tests/unit/test_dependency_propagation.py b/tests/unit/test_dependency_propagation.py index 7e0ff00ac..5b69ca399 100644 --- a/tests/unit/test_dependency_propagation.py +++ b/tests/unit/test_dependency_propagation.py @@ -114,6 +114,17 @@ def __init__(self, profile, skip_checks=(), fail_fast=False): self.fail_fast = fail_fast self.aborted = False + @staticmethod + def resolve_dependency_check(check: RequirementCheck, dependency_name: str) -> RequirementCheck: + """Resolve dependencies using the minimal profile API exercised here.""" + dependency = check.requirement.profile.get_requirement_check(dependency_name) + assert dependency is not None + return dependency + + def is_check_skipped(self, check: RequirementCheck) -> bool: + """Return whether the test settings explicitly skip ``check``.""" + return check.identifier in self.settings.skip_checks + def _check(requirement, name, result, depends_on=(), deactivated=False): check = _Check(requirement, name, result, depends_on=depends_on) diff --git a/tests/unit/test_requirement_lifecycle.py b/tests/unit/test_requirement_lifecycle.py index 4383163e4..3d21a5b7a 100644 --- a/tests/unit/test_requirement_lifecycle.py +++ b/tests/unit/test_requirement_lifecycle.py @@ -174,6 +174,12 @@ def __init_checks__(self): context.aborted = False context.maybe_warn_offline_cache_miss = lambda exc: False + def is_check_skipped(_candidate: object) -> bool: + """Report that the isolated execution test has no configured skips.""" + return False + + context.is_check_skipped = is_check_skipped + def fail_check(*args): raise RuntimeError("unexpected check failure") From a7fabdb47b2d974894039fb80b4730b65a9d4292 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 16:42:09 +0200 Subject: [PATCH 123/271] =?UTF-8?q?feat(core):=20=F0=9F=A7=A9=20expose=20e?= =?UTF-8?q?ffective=20check=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/__init__.py | 4 +- rocrate_validator/models/profile.py | 118 +++++++++++++++++++++++++ rocrate_validator/models/validation.py | 18 +--- 3 files changed, 123 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index 4bf018d88..247f6d1a6 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -20,7 +20,7 @@ RequirementValidationEvent, ValidationEvent, ) -from rocrate_validator.models.profile import Profile +from rocrate_validator.models.profile import EffectiveRequirementCheck, Profile, RequirementCheckRelation from rocrate_validator.models.profile_check import ( ProfileCheck, ProfileCheckFailure, @@ -65,6 +65,7 @@ "CheckResult", "CheckResultValue", "CustomEncoder", + "EffectiveRequirementCheck", "LevelCollection", "Profile", "ProfileCheck", @@ -74,6 +75,7 @@ "ProfileValidationEvent", "Requirement", "RequirementCheck", + "RequirementCheckRelation", "RequirementCheckValidationEvent", "RequirementLevel", "RequirementLoader", diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index a88329117..b6c684c1b 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -15,6 +15,8 @@ from __future__ import annotations import re +from dataclasses import dataclass +from enum import Enum from functools import total_ordering from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, cast @@ -52,6 +54,50 @@ from rocrate_validator.models.requirement import Requirement, RequirementCheck +class RequirementCheckRelation(Enum): + """How a requirement check became part of an effective profile.""" + + DEFINED_LOCALLY = "defined_locally" + INHERITED = "inherited" + REPLACES = "replaces" + + +@dataclass(frozen=True) +class EffectiveRequirementCheck: + """ + A requirement check as exposed by a specific validation profile. + + The source check is never mutated. ``identifier`` and ``profile`` are the + identity under which the check is reported by the effective profile, while + the ``source_*`` properties retain the implementation provenance. + """ + + check: RequirementCheck + identifier: str + profile: Profile + relation: RequirementCheckRelation + replaces: tuple[RequirementCheck, ...] = () + + @property + def source_identifier(self) -> str: + return self.check.identifier + + @property + def source_profile(self) -> Profile: + return self.check.requirement.profile + + def to_dict(self) -> dict[str, Any]: + """Return a serializable representation of the effective check.""" + return { + "identifier": self.identifier, + "profile": self.profile.identifier, + "source_identifier": self.source_identifier, + "source_profile": self.source_profile.identifier, + "relation": self.relation.value, + "replaces": [check.identifier for check in self.replaces], + } + + @total_ordering class Profile: """ @@ -450,6 +496,78 @@ def get_requirement_check( raise DuplicateRequirementCheck(identity, self.identifier) return checks[0] if checks else None + @staticmethod + def __check_identity__(check: RequirementCheck) -> tuple[str, Severity]: + return check.name, check.severity + + def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequirementCheck: + """ + Return the provenance and reporting identity of ``check`` in this profile. + + A check defined by this profile replaces direct-parent checks with the + same name and severity. Checks composed through ``ruleOverlayOf`` are + reported in this profile's namespace; ordinary inherited checks retain + their source identity. + """ + source_profile = check.requirement.profile + if source_profile == self: + replaced_checks = tuple(check.overrides) + identity_check = check + overlay_replacements = tuple( + parent_check + for parent_check in replaced_checks + if parent_check.requirement.profile.uri in self.rule_overlay_of + ) + if len(overlay_replacements) == 1: + identity_check = overlay_replacements[0] + relative_identifier = identity_check.relative_identifier.split(" ", maxsplit=1)[-1] + return EffectiveRequirementCheck( + check=check, + identifier=f"{self.identifier}_{relative_identifier}", + profile=self, + relation=( + RequirementCheckRelation.REPLACES if replaced_checks else RequirementCheckRelation.DEFINED_LOCALLY + ), + replaces=replaced_checks, + ) + + if source_profile.uri in self.rule_overlay_of: + relative_identifier = check.relative_identifier.split(" ", maxsplit=1)[-1] + return EffectiveRequirementCheck( + check=check, + identifier=f"{self.identifier}_{relative_identifier}", + profile=self, + relation=RequirementCheckRelation.INHERITED, + ) + + return EffectiveRequirementCheck( + check=check, + identifier=check.identifier, + profile=source_profile, + relation=RequirementCheckRelation.INHERITED, + ) + + def get_effective_requirement_checks(self) -> tuple[EffectiveRequirementCheck, ...]: + """ + Return the checks that form this profile, including inherited checks. + + More-specific definitions shadow inherited checks with the same + ``(name, severity)`` identity. The returned objects keep source and + effective identities separate, so callers do not need a validation + context and never have to mutate the source model. + """ + effective_checks: list[EffectiveRequirementCheck] = [] + seen_identities: set[tuple[str, Severity]] = set() + for source_profile in (self, *self.inherited_profiles): + for requirement in source_profile.requirements: + for check in requirement.get_checks(): + identity = self.__check_identity__(check) + if identity in seen_identities: + continue + seen_identities.add(identity) + effective_checks.append(self.effective_requirement_check(check)) + return tuple(effective_checks) + def validate_checks(self) -> tuple[ProfileCheckResult, ...]: """Run and cache the registered consistency checks for this profile.""" cache_key = (self.path.resolve(), self.severity) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index d397b146d..db1ce7f2d 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -716,25 +716,11 @@ def is_rule_overlay_source(self, profile: Profile) -> bool: def effective_check_identifier(self, check: RequirementCheck) -> str: """Return a context-local identifier without mutating the source check.""" - if not self.is_rule_overlay_source(check.requirement.profile): - return check.identifier - identity_check = check - if check.requirement.profile == self.target_profile: - overlay_overrides = [ - parent_check - for parent_check in check.overrides - if parent_check.requirement.profile.uri in self.target_profile.rule_overlay_of - ] - if len(overlay_overrides) == 1: - identity_check = overlay_overrides[0] - relative_identifier = identity_check.relative_identifier.split(" ", maxsplit=1)[-1] - return f"{self.target_profile.identifier}_{relative_identifier}" + return self.target_profile.effective_requirement_check(check).identifier def effective_check_profile(self, check: RequirementCheck) -> Profile: """Return the reporting profile for a check in this validation.""" - return ( - self.target_profile if self.is_rule_overlay_source(check.requirement.profile) else check.requirement.profile - ) + return self.target_profile.effective_requirement_check(check).profile def is_check_skipped(self, check: RequirementCheck) -> bool: """ From b4108c160a0eb51a34e0d91e7b16d91520e477fc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 16:42:42 +0200 Subject: [PATCH 124/271] =?UTF-8?q?feat(cli):=20=F0=9F=8E=A8=20render=20ef?= =?UTF-8?q?fective=20check=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/profiles.py | 180 ++++++++++----------- 1 file changed, 86 insertions(+), 94 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 0e6385ff8..01a94bab5 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -17,7 +17,6 @@ from pathlib import Path from typing import Any -from rich.align import Align from rich.markdown import Markdown from rich.padding import Padding from rich.panel import Panel @@ -28,7 +27,14 @@ from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import LevelCollection, Profile, RequirementCheck, RequirementLevel, Severity +from rocrate_validator.models import ( + EffectiveRequirementCheck, + LevelCollection, + Profile, + RequirementCheckRelation, + RequirementLevel, + Severity, +) from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.colors import get_severity_color from rocrate_validator.utils.io_helpers.output.text.layout.report import get_app_header_rule @@ -268,10 +274,10 @@ def describe_profile( # Single-check view if check_identifier: - check = __resolve_check__(profile, check_identifier) + effective_check = __resolve_check__(profile, check_identifier) with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: console.print(get_app_header_rule()) - __describe_check__(console, profile, check, verbose=verbose) + __describe_check__(console, profile, effective_check, verbose=verbose) return # Set the subheader title @@ -404,24 +410,25 @@ def __verbose_describe_profile__(profile): table_rows = [] levels_list = set() count_checks = 0 - for requirement in profile.requirements: - # skip hidden requirements - if requirement.hidden: + for effective_check in profile.get_effective_requirement_checks(): + check = effective_check.check + if check.requirement.hidden: continue - # add the requirement to the list - for check in requirement.get_checks(): - color = get_severity_color(check.severity) - level_info = f"[{color}]{check.severity.name}[/{color}]" - levels_list.add(level_info) - override = __format_overrides__(check.overrides, label="overrides") if check.overrides else None - - description_table = Table(show_header=False, show_footer=False, show_lines=False, show_edge=False) - if override: - description_table.add_row(Align(Padding(override, (0, 0, 1, 0)), align="right")) - description_table.add_row(Markdown(check.description.strip())) - - table_rows.append((check.identifier, check.name, description_table, level_info)) - count_checks += 1 + color = get_severity_color(check.severity) + level_info = f"[{color}]{check.severity.name}[/{color}]" + levels_list.add(level_info) + relation = __format_check_relation__(effective_check) + table_rows.append( + ( + effective_check.identifier, + check.name, + relation, + effective_check.source_identifier, + Markdown(check.description.strip()), + level_info, + ) + ) + count_checks += 1 table = Table( show_header=True, @@ -432,12 +439,18 @@ def __verbose_describe_profile__(profile): show_footer=False, show_lines=True, caption_style="italic bold", - caption=f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}", + caption=( + f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}\n" + "[cyan](†)[/cyan] Effective ID is the identity exposed by this profile; " + "Source ID identifies the check implementation" + ), ) # Define columns - table.add_column("Identifier", style="cyan bold", justify="right") + table.add_column("Effective ID (†)", style="cyan bold", justify="right") table.add_column("Name", style="magenta bold", justify="left") + table.add_column("Relation", justify="left") + table.add_column("Source ID (†)", style="green", justify="right") table.add_column("Description", style="white italic") table.add_column("Severity (*)", style="bold", justify="center") @@ -450,84 +463,73 @@ def __verbose_describe_profile__(profile): _CHECK_ID_RE = re.compile(r"^(?P\d+)\.(?P\d+)$") -def __resolve_check__(profile: Profile, check_identifier: str) -> RequirementCheck: +def __resolve_check__(profile: Profile, check_identifier: str) -> EffectiveRequirementCheck: """ - Resolve a check identifier to a RequirementCheck instance. + Resolve a check identifier to its effective profile view. Accepts either the relative form ``.`` or the full form ``_.``. """ raw = check_identifier.strip() - relative = raw - prefix = f"{profile.identifier}_" + effective_checks = tuple( + item for item in profile.get_effective_requirement_checks() if not item.check.requirement.hidden + ) if "_" in raw: - if not raw.startswith(prefix): + match = next((item for item in effective_checks if item.identifier == raw), None) + if match is None: raise click.BadParameter( - f"Check identifier '{raw}' does not belong to profile '{profile.identifier}'.", + f"Check identifier '{raw}' is not part of effective profile '{profile.identifier}'.", param_hint="CHECK_IDENTIFIER", ) - relative = raw[len(prefix) :] + return match - match = _CHECK_ID_RE.match(relative) - if not match: + if not _CHECK_ID_RE.match(raw): raise click.BadParameter( f"Invalid check identifier '{check_identifier}'. " f"Expected '.' (e.g. '1.2') or " f"'_.' (e.g. '{profile.identifier}_1.2').", param_hint="CHECK_IDENTIFIER", ) - req_number = int(match.group("req")) - check_number = int(match.group("check")) - - requirement = next( - (r for r in profile.requirements if not r.hidden and r.order_number == req_number), - None, - ) - if requirement is None: + matches = [item for item in effective_checks if item.identifier.rsplit("_", maxsplit=1)[-1] == raw] + if not matches: raise click.BadParameter( - f"No requirement #{req_number} in profile '{profile.identifier}'. " - f"Run `rocrate-validator profiles describe {profile.identifier}` to list requirements.", + f"No effective check '{raw}' in profile '{profile.identifier}'. " + f"Run `rocrate-validator profiles describe {profile.identifier} -v` to list checks.", param_hint="CHECK_IDENTIFIER", ) - check = next( - (c for c in requirement.get_checks() if c.order_number == check_number), - None, - ) - if check is None: + if len(matches) > 1: raise click.BadParameter( - f"No check #{check_number} in requirement #{req_number} of profile " - f"'{profile.identifier}'. Run `rocrate-validator profiles describe " - f"{profile.identifier} -v` to list checks.", + f"Relative check identifier '{raw}' is ambiguous in effective profile '{profile.identifier}'. " + "Use one of the full effective identifiers shown by the verbose profile description.", param_hint="CHECK_IDENTIFIER", ) - return check + return matches[0] -def __format_overrides__(checks: list, label: str) -> str: - """ - Format an "overrides" / "overridden by" Rich-styled string for a list of checks. - """ - parts = [] - for co in checks: - severity_color = get_severity_color(co.severity) - parts.append( - f"[bold][magenta]{co.requirement.profile.identifier}[/magenta] " - f"[{severity_color}]{co.relative_identifier}[/{severity_color}][/bold]" - ) - return f"[bold red]{label}:[/bold red] " + ", ".join(parts) +def __format_check_relation__(effective_check: EffectiveRequirementCheck) -> str: + """Format the provenance relation of an effective check.""" + if effective_check.relation == RequirementCheckRelation.DEFINED_LOCALLY: + return "[bold green]Defined locally[/bold green]" + if effective_check.relation == RequirementCheckRelation.INHERITED: + return f"[bold cyan]Inherited[/bold cyan] from [magenta]{effective_check.source_profile.identifier}[/magenta]" + replaced = ", ".join(check.identifier for check in effective_check.replaces) + return f"[bold yellow]Replaces[/bold yellow] [magenta]{replaced}[/magenta]" -def __describe_check__(console, profile: Profile, check: RequirementCheck, verbose: bool = False) -> None: +def __describe_check__( + console, profile: Profile, effective_check: EffectiveRequirementCheck, verbose: bool = False +) -> None: """ Render a single requirement check. """ + check = effective_check.check severity_color = get_severity_color(check.severity) requirement = check.requirement header = ( f"[bold cyan]Profile:[/bold cyan] " f"[italic magenta]{profile.identifier}[/italic magenta]\n" - f"[bold cyan]Identifier:[/bold cyan] " - f"[italic green]{check.identifier}[/italic green]\n" + f"[bold cyan]Effective ID:[/bold cyan] " + f"[italic green]{effective_check.identifier}[/italic green]\n" f"[bold cyan]Name:[/bold cyan] [italic]{check.name}[/italic]\n" f"[bold cyan]Severity:[/bold cyan] " f"[bold {severity_color}]{check.severity.name}[/bold {severity_color}]\n" @@ -537,7 +539,7 @@ def __describe_check__(console, profile: Profile, check: RequirementCheck, verbo if requirement.path: header += f"\n[bold cyan]Source file:[/bold cyan] [italic green]{shorten_path(requirement.path)}[/italic green]" - title = f"[bold][cyan]Check:[/cyan] [magenta italic]{check.identifier}[/magenta italic][/bold]" + title = f"[bold][cyan]Check:[/cyan] [magenta italic]{effective_check.identifier}[/magenta italic][/bold]" console.print( Padding( Panel(header, title=title, padding=(1, 1, 1, 1), title_align="left", border_style="cyan"), @@ -554,34 +556,24 @@ def __describe_check__(console, profile: Profile, check: RequirementCheck, verbo ) console.print(Padding(description_panel, (1, 1, 0, 1))) - if check.overrides: - overrides_text = __format_overrides__(check.overrides, label="overrides") - console.print( - Padding( - Panel( - overrides_text, - title="[bold cyan]Overrides[/bold cyan]", - title_align="left", - border_style="bright_black", - padding=(1, 1, 1, 1), - ), - (1, 1, 0, 1), - ) - ) - if check.overridden_by: - overridden_text = __format_overrides__(check.overridden_by, label="overridden by") - console.print( - Padding( - Panel( - overridden_text, - title="[bold cyan]Overridden by[/bold cyan]", - title_align="left", - border_style="bright_black", - padding=(1, 1, 1, 1), - ), - (1, 1, 0, 1), - ) + provenance = ( + f"[bold cyan]Relation:[/bold cyan] {__format_check_relation__(effective_check)}\n" + f"[bold cyan]Effective ID:[/bold cyan] [green]{effective_check.identifier}[/green]\n" + f"[bold cyan]Source ID:[/bold cyan] [green]{effective_check.source_identifier}[/green]\n" + f"[bold cyan]Source profile:[/bold cyan] [magenta]{effective_check.source_profile.identifier}[/magenta]" + ) + console.print( + Padding( + Panel( + provenance, + title="[bold cyan]Provenance[/bold cyan]", + title_align="left", + border_style="bright_black", + padding=(1, 1, 1, 1), + ), + (1, 1, 0, 1), ) + ) if verbose: snippet = check.get_source_snippet() From 6746561b49adb0902492af8049c04eb83a077efd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 16:43:12 +0200 Subject: [PATCH 125/271] =?UTF-8?q?test(profile):=20=F0=9F=A7=AA=20cover?= =?UTF-8?q?=20effective=20check=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/profiles.py | 2 +- .../profiles/effective_checks/a/checks.py | 15 ++++++ .../profiles/effective_checks/a/profile.ttl | 7 +++ .../profiles/effective_checks/b/checks.py | 10 ++++ .../profiles/effective_checks/b/profile.ttl | 10 ++++ tests/test_cli.py | 50 ++++++++++++++++++- tests/unit/requirements/test_profiles.py | 46 +++++++++++++++++ 7 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 tests/data/profiles/effective_checks/a/checks.py create mode 100644 tests/data/profiles/effective_checks/a/profile.ttl create mode 100644 tests/data/profiles/effective_checks/b/checks.py create mode 100644 tests/data/profiles/effective_checks/b/profile.ttl diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 01a94bab5..967c1e2c3 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -252,7 +252,7 @@ def describe_profile( * full: _. (e.g. "ro-crate-1.2_1.2") With -v on a single check, the source code of the check is shown. - """ + """ # ruff: ignore[escape-sequence-in-docstring] # Get the console console = ctx.obj["console"] diff --git a/tests/data/profiles/effective_checks/a/checks.py b/tests/data/profiles/effective_checks/a/checks.py new file mode 100644 index 000000000..89b448a1c --- /dev/null +++ b/tests/data/profiles/effective_checks/a/checks.py @@ -0,0 +1,15 @@ +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + +@requirement(name="Source checks") +class SourceChecks(PyFunctionCheck): + @check(name="Shared check") + def shared_check(self, context: ValidationContext) -> bool: + """Check implemented by both the source and overlay profiles.""" + return True + + @check(name="Inherited check") + def inherited_check(self, context: ValidationContext) -> bool: + """Check implemented only by the source profile.""" + return True diff --git a/tests/data/profiles/effective_checks/a/profile.ttl b/tests/data/profiles/effective_checks/a/profile.ttl new file mode 100644 index 000000000..c21d8fdf2 --- /dev/null +++ b/tests/data/profiles/effective_checks/a/profile.ttl @@ -0,0 +1,7 @@ +@prefix prof: . +@prefix rdfs: . + + + a prof:Profile ; + rdfs:label "Effective checks source profile" ; + prof:hasToken "effective-a" . diff --git a/tests/data/profiles/effective_checks/b/checks.py b/tests/data/profiles/effective_checks/b/checks.py new file mode 100644 index 000000000..98c2496d6 --- /dev/null +++ b/tests/data/profiles/effective_checks/b/checks.py @@ -0,0 +1,10 @@ +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + +@requirement(name="Overlay checks") +class OverlayChecks(PyFunctionCheck): + @check(name="Shared check") + def shared_check(self, context: ValidationContext) -> bool: + """Local implementation replacing the source profile check.""" + return True diff --git a/tests/data/profiles/effective_checks/b/profile.ttl b/tests/data/profiles/effective_checks/b/profile.ttl new file mode 100644 index 000000000..615f2170e --- /dev/null +++ b/tests/data/profiles/effective_checks/b/profile.ttl @@ -0,0 +1,10 @@ +@prefix prof: . +@prefix rdfs: . +@prefix validator: . + + + a prof:Profile ; + rdfs:label "Effective checks overlay profile" ; + prof:isProfileOf ; + validator:ruleOverlayOf ; + prof:hasToken "effective-b" . diff --git a/tests/test_cli.py b/tests/test_cli.py index df58ca8aa..49d14891a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -474,6 +474,8 @@ def test_profiles_describe_verbose(cli_runner: CliRunner): result = cli_runner.invoke(cli, ["profiles", "describe", _DESCRIBE_TEST_PROFILE, "-v", "--no-paging"]) assert result.exit_code == 0 assert check.identifier in result.output + assert "Effective ID" in result.output + assert "Source ID" in result.output def test_describe_check_relative_id(cli_runner: CliRunner): @@ -498,7 +500,7 @@ def test_describe_check_unknown(cli_runner: CliRunner): """An out-of-range check id produces a usage error with a hint.""" result = cli_runner.invoke(cli, ["profiles", "describe", _DESCRIBE_TEST_PROFILE, "99.99", "--no-paging"]) assert result.exit_code == 2 - assert "No requirement #99" in result.output + assert "No effective check '99.99'" in result.output def test_describe_check_bad_format(cli_runner: CliRunner): @@ -514,7 +516,51 @@ def test_describe_check_profile_mismatch(cli_runner: CliRunner): cli, ["profiles", "describe", _DESCRIBE_TEST_PROFILE, "some-other-profile_1.1", "--no-paging"] ) assert result.exit_code == 2 - assert "does not belong to profile" in result.output + assert "is not part of effective profile" in result.output + + +def test_describe_overlay_profile_distinguishes_inherited_and_replaced_checks(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, + [ + "profiles", + "--profiles-path", + "tests/data/profiles/effective_checks", + "describe", + "effective-b", + "-v", + "--no-paging", + ], + env={"COLUMNS": "160"}, + ) + + assert result.exit_code == 0, result.output + assert "Replaces effective-a_1.2" in result.output + assert "Inherited from effective-a" in result.output + assert "effective-b_1.1" in result.output + assert "effective-a_1.1" in result.output + + +def test_describe_inherited_overlay_check_shows_provenance(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, + [ + "profiles", + "--profiles-path", + "tests/data/profiles/effective_checks", + "describe", + "effective-b", + "1.1", + "--no-paging", + ], + env={"COLUMNS": "160"}, + ) + + assert result.exit_code == 0, result.output + assert "Relation: Inherited from effective-a" in result.output + assert "Effective ID: effective-b_1.1" in result.output + assert "Source ID: effective-a_1.1" in result.output + assert "Source profile: effective-a" in result.output def test_describe_check_verbose_python(cli_runner: CliRunner): diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index a3492790f..ee87ca058 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -27,6 +27,7 @@ URI, CheckResult, Profile, + RequirementCheckRelation, Severity, ValidationContext, ValidationSettings, @@ -496,6 +497,51 @@ def test_check_name_and_severity_match_parent_override(check_overriding_profiles child_check.order_number = original_order +def test_effective_checks_expose_overlay_provenance(): + profiles = Profile.load_profiles("tests/data/profiles/effective_checks", severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "effective-b") + + effective_checks = profile.get_effective_requirement_checks() + by_name = {item.check.name: item for item in effective_checks} + + assert len(effective_checks) == 2 + replacement = by_name["Shared check"] + assert replacement.relation == RequirementCheckRelation.REPLACES + assert replacement.identifier == "effective-b_1.2" + assert replacement.source_identifier == "effective-b_1.1" + assert [check.identifier for check in replacement.replaces] == ["effective-a_1.2"] + + inherited = by_name["Inherited check"] + assert inherited.relation == RequirementCheckRelation.INHERITED + assert inherited.profile == profile + assert inherited.identifier == "effective-b_1.1" + assert inherited.source_identifier == "effective-a_1.1" + assert inherited.source_profile.identifier == "effective-a" + assert inherited.replaces == () + assert inherited.to_dict() == { + "identifier": "effective-b_1.1", + "profile": "effective-b", + "source_identifier": "effective-a_1.1", + "source_profile": "effective-a", + "relation": "inherited", + "replaces": [], + } + + +def test_effective_checks_retain_ordinary_inherited_identity(): + profiles = Profile.load_profiles(Path("rocrate_validator/profiles"), severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "process-run-crate-0.5") + inherited = next( + item + for item in profile.get_effective_requirement_checks() + if item.relation == RequirementCheckRelation.INHERITED + ) + + assert inherited.profile == inherited.source_profile + assert inherited.identifier == inherited.source_identifier + assert inherited.source_profile != profile + + def test_same_name_with_different_severity_does_not_override(): profiles = Profile.load_profiles( profiles_path=Path("rocrate_validator/profiles"), From 051dbcc5d0b4a5776914ad1a31765cbfef8e102c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 17:03:57 +0200 Subject: [PATCH 126/271] =?UTF-8?q?refactor(core):=20=F0=9F=A7=A9=20extrac?= =?UTF-8?q?t=20profile=20provenance=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/__init__.py | 3 +- rocrate_validator/models/profile.py | 50 ++------------ .../models/profile_provenance.py | 67 +++++++++++++++++++ 3 files changed, 73 insertions(+), 47 deletions(-) create mode 100644 rocrate_validator/models/profile_provenance.py diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index 247f6d1a6..fb64fc324 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -20,13 +20,14 @@ RequirementValidationEvent, ValidationEvent, ) -from rocrate_validator.models.profile import EffectiveRequirementCheck, Profile, RequirementCheckRelation +from rocrate_validator.models.profile import Profile from rocrate_validator.models.profile_check import ( ProfileCheck, ProfileCheckFailure, ProfileCheckResult, ProfileCheckSuite, ) +from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck, RequirementCheckRelation from rocrate_validator.models.requirement import ( Requirement, RequirementCheck, diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index b6c684c1b..c32aad882 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -15,8 +15,6 @@ from __future__ import annotations import re -from dataclasses import dataclass -from enum import Enum from functools import total_ordering from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, cast @@ -45,6 +43,10 @@ ProfileCheckResult, ProfileCheckSuite, ) +from rocrate_validator.models.profile_provenance import ( + EffectiveRequirementCheck, + RequirementCheckRelation, +) from rocrate_validator.models.severity import Severity from rocrate_validator.utils.collections import MapIndex, MultiIndexMap @@ -54,50 +56,6 @@ from rocrate_validator.models.requirement import Requirement, RequirementCheck -class RequirementCheckRelation(Enum): - """How a requirement check became part of an effective profile.""" - - DEFINED_LOCALLY = "defined_locally" - INHERITED = "inherited" - REPLACES = "replaces" - - -@dataclass(frozen=True) -class EffectiveRequirementCheck: - """ - A requirement check as exposed by a specific validation profile. - - The source check is never mutated. ``identifier`` and ``profile`` are the - identity under which the check is reported by the effective profile, while - the ``source_*`` properties retain the implementation provenance. - """ - - check: RequirementCheck - identifier: str - profile: Profile - relation: RequirementCheckRelation - replaces: tuple[RequirementCheck, ...] = () - - @property - def source_identifier(self) -> str: - return self.check.identifier - - @property - def source_profile(self) -> Profile: - return self.check.requirement.profile - - def to_dict(self) -> dict[str, Any]: - """Return a serializable representation of the effective check.""" - return { - "identifier": self.identifier, - "profile": self.profile.identifier, - "source_identifier": self.source_identifier, - "source_profile": self.source_profile.identifier, - "relation": self.relation.value, - "replaces": [check.identifier for check in self.replaces], - } - - @total_ordering class Profile: """ diff --git a/rocrate_validator/models/profile_provenance.py b/rocrate_validator/models/profile_provenance.py new file mode 100644 index 000000000..fca71d436 --- /dev/null +++ b/rocrate_validator/models/profile_provenance.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from rocrate_validator.models.profile import Profile + from rocrate_validator.models.requirement import RequirementCheck + + +class RequirementCheckRelation(Enum): + """How a requirement check became part of an effective profile.""" + + DEFINED_LOCALLY = "defined_locally" + INHERITED = "inherited" + REPLACES = "replaces" + + +@dataclass(frozen=True) +class EffectiveRequirementCheck: + """ + A requirement check as exposed by a specific validation profile. + + The source check is never mutated. ``identifier`` and ``profile`` are the + identity under which the check is reported by the effective profile, while + the ``source_*`` properties retain the implementation provenance. + """ + + check: RequirementCheck + identifier: str + profile: Profile + relation: RequirementCheckRelation + replaces: tuple[RequirementCheck, ...] = () + + @property + def source_identifier(self) -> str: + return self.check.identifier + + @property + def source_profile(self) -> Profile: + return self.check.requirement.profile + + def to_dict(self) -> dict[str, Any]: + """Return a serializable representation of the effective check.""" + return { + "identifier": self.identifier, + "profile": self.profile.identifier, + "source_identifier": self.source_identifier, + "source_profile": self.source_profile.identifier, + "relation": self.relation.value, + "replaces": [check.identifier for check in self.replaces], + } From 8df06f12df83a2c3e62fce1b96fc879c5587ecfe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 17:05:01 +0200 Subject: [PATCH 127/271] =?UTF-8?q?perf(validation):=20=E2=9A=A1=20cache?= =?UTF-8?q?=20effective=20check=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index db1ce7f2d..55b76d630 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -48,6 +48,7 @@ from rocrate_validator.utils.http import find_offline_cache_miss if TYPE_CHECKING: + from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck from rocrate_validator.utils.uri import URI @@ -328,6 +329,9 @@ def __init__(self, validator: Validator, settings: ValidationSettings): self._properties: dict = {} # URLs already reported as missing from the HTTP cache during this run self._offline_cache_misses_warned: set[str] = set() + # Effective identities are requested by several reporting consumers + # for the same check during one validation run. + self._effective_check_cache: dict[tuple[int, int], EffectiveRequirementCheck] = {} # flag set when the validation must be aborted because the metadata # cannot be read (e.g. the file descriptor is not valid JSON) self._aborted: bool = False @@ -716,11 +720,21 @@ def is_rule_overlay_source(self, profile: Profile) -> bool: def effective_check_identifier(self, check: RequirementCheck) -> str: """Return a context-local identifier without mutating the source check.""" - return self.target_profile.effective_requirement_check(check).identifier + return self.effective_check_provenance(check).identifier def effective_check_profile(self, check: RequirementCheck) -> Profile: """Return the reporting profile for a check in this validation.""" - return self.target_profile.effective_requirement_check(check).profile + return self.effective_check_provenance(check).profile + + def effective_check_provenance(self, check: RequirementCheck) -> EffectiveRequirementCheck: + """Return and cache the effective identity of ``check`` in this context.""" + target_profile = self.target_profile + cache_key = (id(target_profile), id(check)) + provenance = self._effective_check_cache.get(cache_key) + if provenance is None: + provenance = target_profile.effective_requirement_check(check) + self._effective_check_cache[cache_key] = provenance + return provenance def is_check_skipped(self, check: RequirementCheck) -> bool: """ From f00ea8c11f73cd62f180814e52b11f72fac1ea7a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 17:07:43 +0200 Subject: [PATCH 128/271] =?UTF-8?q?fix(profile):=20=F0=9F=9B=A1=EF=B8=8F?= =?UTF-8?q?=20reject=20ambiguous=20inherited=20identities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/profiles.py | 2 +- rocrate_validator/models/profile.py | 27 +++++++++++++++---- .../profiles/effective_checks/a/checks.py | 4 +-- .../profiles/effective_checks/b/checks.py | 2 +- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 967c1e2c3..01a94bab5 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -252,7 +252,7 @@ def describe_profile( * full: _. (e.g. "ro-crate-1.2_1.2") With -v on a single check, the source code of the check is shown. - """ # ruff: ignore[escape-sequence-in-docstring] + """ # Get the console console = ctx.obj["console"] diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index c32aad882..87b61fd86 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -515,15 +515,32 @@ def get_effective_requirement_checks(self) -> tuple[EffectiveRequirementCheck, . context and never have to mutate the source model. """ effective_checks: list[EffectiveRequirementCheck] = [] - seen_identities: set[tuple[str, Severity]] = set() + seen_identities: dict[tuple[str, Severity], tuple[RequirementCheck, int]] = {} for source_profile in (self, *self.inherited_profiles): for requirement in source_profile.requirements: for check in requirement.get_checks(): identity = self.__check_identity__(check) - if identity in seen_identities: - continue - seen_identities.add(identity) - effective_checks.append(self.effective_requirement_check(check)) + previous = seen_identities.get(identity) + if previous is not None: + previous_check, previous_index = previous + previous_profile = previous_check.requirement.profile + # A transitive ancestor is shadowed by the nearer + # profile already selected. Unrelated parents form an + # ambiguous effective identity and must not be hidden. + if source_profile in previous_profile.inherited_profiles: + continue + if previous_profile in source_profile.inherited_profiles: + effective_checks[previous_index] = self.effective_requirement_check(check) + seen_identities[identity] = (check, previous_index) + else: + profiles = {previous_profile.identifier, source_profile.identifier} + raise DuplicateRequirementCheck( + f"{check.name} [{check.severity.name}] from {', '.join(sorted(profiles))}", + self.identifier, + ) + else: + seen_identities[identity] = (check, len(effective_checks)) + effective_checks.append(self.effective_requirement_check(check)) return tuple(effective_checks) def validate_checks(self) -> tuple[ProfileCheckResult, ...]: diff --git a/tests/data/profiles/effective_checks/a/checks.py b/tests/data/profiles/effective_checks/a/checks.py index 89b448a1c..f9a7fb23b 100644 --- a/tests/data/profiles/effective_checks/a/checks.py +++ b/tests/data/profiles/effective_checks/a/checks.py @@ -5,11 +5,11 @@ @requirement(name="Source checks") class SourceChecks(PyFunctionCheck): @check(name="Shared check") - def shared_check(self, context: ValidationContext) -> bool: + def shared_check(self, _context: ValidationContext) -> bool: """Check implemented by both the source and overlay profiles.""" return True @check(name="Inherited check") - def inherited_check(self, context: ValidationContext) -> bool: + def inherited_check(self, _context: ValidationContext) -> bool: """Check implemented only by the source profile.""" return True diff --git a/tests/data/profiles/effective_checks/b/checks.py b/tests/data/profiles/effective_checks/b/checks.py index 98c2496d6..70430ed6a 100644 --- a/tests/data/profiles/effective_checks/b/checks.py +++ b/tests/data/profiles/effective_checks/b/checks.py @@ -5,6 +5,6 @@ @requirement(name="Overlay checks") class OverlayChecks(PyFunctionCheck): @check(name="Shared check") - def shared_check(self, context: ValidationContext) -> bool: + def shared_check(self, _context: ValidationContext) -> bool: """Local implementation replacing the source profile check.""" return True From 40e7fe4e07cdef914f99e41434158cf271c98ad3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 17:52:25 +0200 Subject: [PATCH 129/271] =?UTF-8?q?refactor(cli):=20=F0=9F=8E=A8=20standar?= =?UTF-8?q?dize=20profile=20and=20check=20colors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/profiles.py | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 01a94bab5..d71bb9bd9 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -194,7 +194,7 @@ def check_profile(ctx, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, no_ header_style="bold cyan", border_style="bright_black", ) - table.add_column("Profile", style="magenta bold") + table.add_column("Profile", style="hot_pink bold") table.add_column("Check", style="cyan") table.add_column("Status", justify="center") table.add_column("Message") @@ -281,7 +281,7 @@ def describe_profile( return # Set the subheader title - subheader_title = f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]" + subheader_title = f"[bold][cyan]Profile:[/cyan] [hot_pink italic]{profile.identifier}[/hot_pink italic][/bold]" # Set the subheader content subheader_content = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" @@ -392,7 +392,7 @@ def __compacted_describe_profile__(profile): # Define columns table.add_column("#", style="cyan bold", justify="right") - table.add_column("Name", style="magenta bold", justify="left") + table.add_column("Name", style="hot_pink bold", justify="left") table.add_column("Description", style="white italic") table.add_column("# REQUIRED", style=__requirement_level_style__(LevelCollection.REQUIRED), justify="center") table.add_column("# RECOMMENDED", style=__requirement_level_style__(LevelCollection.RECOMMENDED), justify="center") @@ -448,9 +448,9 @@ def __verbose_describe_profile__(profile): # Define columns table.add_column("Effective ID (†)", style="cyan bold", justify="right") - table.add_column("Name", style="magenta bold", justify="left") + table.add_column("Name", style="hot_pink bold", justify="left") table.add_column("Relation", justify="left") - table.add_column("Source ID (†)", style="green", justify="right") + table.add_column("Source ID (†)", style="hot_pink italic", justify="right") table.add_column("Description", style="white italic") table.add_column("Severity (*)", style="bold", justify="center") @@ -508,11 +508,14 @@ def __resolve_check__(profile: Profile, check_identifier: str) -> EffectiveRequi def __format_check_relation__(effective_check: EffectiveRequirementCheck) -> str: """Format the provenance relation of an effective check.""" if effective_check.relation == RequirementCheckRelation.DEFINED_LOCALLY: - return "[bold green]Defined locally[/bold green]" + return "[bold yellow]Defined locally[/bold yellow]" if effective_check.relation == RequirementCheckRelation.INHERITED: - return f"[bold cyan]Inherited[/bold cyan] from [magenta]{effective_check.source_profile.identifier}[/magenta]" + return ( + f"[bold cyan]Inherited[/bold cyan] from " + f"[italic hot_pink]{effective_check.source_profile.identifier}[/italic hot_pink]" + ) replaced = ", ".join(check.identifier for check in effective_check.replaces) - return f"[bold yellow]Replaces[/bold yellow] [magenta]{replaced}[/magenta]" + return f"[bold orange1]Replaces[/bold orange1] [italic hot_pink]{replaced}[/italic hot_pink]" def __describe_check__( @@ -527,19 +530,19 @@ def __describe_check__( header = ( f"[bold cyan]Profile:[/bold cyan] " - f"[italic magenta]{profile.identifier}[/italic magenta]\n" + f"[italic hot_pink]{profile.identifier}[/italic hot_pink]\n" f"[bold cyan]Effective ID:[/bold cyan] " f"[italic green]{effective_check.identifier}[/italic green]\n" - f"[bold cyan]Name:[/bold cyan] [italic]{check.name}[/italic]\n" + f"[bold cyan]Name:[/bold cyan] [italic hot_pink]{check.name}[/italic hot_pink]\n" f"[bold cyan]Severity:[/bold cyan] " f"[bold {severity_color}]{check.severity.name}[/bold {severity_color}]\n" f"[bold cyan]Requirement:[/bold cyan] " - f"[italic]#{requirement.order_number} — {requirement.name}[/italic]" + f"[italic hot_pink]#{requirement.order_number} — {requirement.name}[/italic hot_pink]" ) if requirement.path: header += f"\n[bold cyan]Source file:[/bold cyan] [italic green]{shorten_path(requirement.path)}[/italic green]" - title = f"[bold][cyan]Check:[/cyan] [magenta italic]{effective_check.identifier}[/magenta italic][/bold]" + title = f"[bold][cyan]Check:[/cyan] [hot_pink italic]{effective_check.identifier}[/hot_pink italic][/bold]" console.print( Padding( Panel(header, title=title, padding=(1, 1, 1, 1), title_align="left", border_style="cyan"), @@ -558,9 +561,10 @@ def __describe_check__( provenance = ( f"[bold cyan]Relation:[/bold cyan] {__format_check_relation__(effective_check)}\n" - f"[bold cyan]Effective ID:[/bold cyan] [green]{effective_check.identifier}[/green]\n" - f"[bold cyan]Source ID:[/bold cyan] [green]{effective_check.source_identifier}[/green]\n" - f"[bold cyan]Source profile:[/bold cyan] [magenta]{effective_check.source_profile.identifier}[/magenta]" + f"[bold cyan]Effective ID:[/bold cyan] [italic cyan]{effective_check.identifier}[/italic cyan]\n" + f"[bold cyan]Source ID:[/bold cyan] [italic hot_pink]{effective_check.source_identifier}[/italic hot_pink]\n" + f"[bold cyan]Source profile:[/bold cyan] " + f"[italic hot_pink]{effective_check.source_profile.identifier}[/italic hot_pink]" ) console.print( Padding( From a66112f6d4258fa35b562c1e98ebcabf75fb9030 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 18:51:17 +0200 Subject: [PATCH 130/271] =?UTF-8?q?refactor(profile):=20=F0=9F=A7=B9=20rem?= =?UTF-8?q?ove=20duplicated=20RO-Crate=201.3=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.3/may/4_data_entity_metadata.ttl | 89 --- .../ro-crate/1.3/may/61_license_entity.ttl | 65 -- .../1.3/may/6_contextual_entity_metadata.ttl | 82 -- .../1.3/must/0_file_descriptor_context.py | 114 +++ .../1.3/must/0_file_descriptor_format.py | 727 ------------------ .../1.3/must/1_file-descriptor_metadata.ttl | 88 --- .../ro-crate/1.3/must/1_ro_crate_preview.py | 55 -- .../1.3/must/2_root_data_entity_cite_as.py | 93 --- .../1.3/must/2_root_data_entity_haspart.ttl | 40 - .../1.3/must/2_root_data_entity_identifier.py | 53 -- .../1.3/must/2_root_data_entity_metadata.ttl | 170 ---- .../1.3/must/2_root_identifier_property.py | 53 -- .../1.3/must/4_data_entity_metadata.py | 335 -------- .../1.3/must/4_data_entity_metadata.ttl | 227 ------ .../1.3/must/5_web_data_entity_metadata.ttl | 49 -- .../1.3/must/6_contextual_entity_metadata.ttl | 219 ------ .../1.3/must/9_schema_org_iri_protocol.ttl | 45 -- .../1.3/should/0_entity_identifier_format.py | 153 ---- .../ro-crate/1.3/should/0_entity_metadata.ttl | 76 -- .../1.3/should/1_file_descriptor_name.py | 59 -- .../should/2_root_data_entity_conformsto.ttl | 47 -- .../1.3/should/2_root_data_entity_funding.ttl | 94 --- .../should/2_root_data_entity_identifier.py | 193 ----- .../should/2_root_data_entity_identifier.ttl | 65 -- .../should/2_root_data_entity_metadata.ttl | 30 - .../should/2_root_data_entity_publisher.ttl | 36 - .../should/3_ro_crate_preview_exclusion.ttl | 29 - .../1.3/should/4_data_entity_license.py | 154 ---- .../1.3/should/4_data_entity_metadata.py | 205 ----- .../1.3/should/4_data_entity_metadata.ttl | 211 ----- .../1.3/should/4_dataset_data_entity.ttl | 70 -- .../1.3/should/4_missing_file_local_path.py | 132 ---- .../1.3/should/4_referenced_rocrate.py | 126 --- .../1.3/should/4_referenced_rocrate.ttl | 203 ----- .../1.3/should/4_web_dataset_distribution.py | 104 --- .../1.3/should/5_web_data_entity_metadata.ttl | 46 -- .../should/6_contextual_entity_metadata.ttl | 268 ------- .../1.3/should/6_organization_metadata.ttl | 117 --- .../should/9_schema_org_id_compatibility.ttl | 58 -- 39 files changed, 114 insertions(+), 4866 deletions(-) delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl create mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_cite_as.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/must/9_schema_org_iri_protocol.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl delete mode 100644 rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl deleted file mode 100644 index 72ee24c48..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/may/4_data_entity_metadata.ttl +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix sh: . -@prefix xsd: . -@prefix owl: . -@prefix schema: . -@prefix validator: . - -ro-crate:FileDataEntityWebOptionalProperties a sh:NodeShape ; - sh:name "File Data Entity with web presence: OPTIONAL properties" ; - sh:description """A File Data Entity which have a corresponding web presence, - for instance a landing page that describes the file, including persistence identifiers (e.g. DOI), - resolving to an intermediate HTML page instead of the downloadable file directly. - These can included for File Data Entities as additional metadata by using the properties: - `identifier`, `url`, `subjectOf`and `mainEntityOfPage`""" ; - sh:targetClass ro-crate:File ; - # Check if the Web-based Data Entity has a contentSize property - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "File Data Entity: optional formal `identifier` (e.g. DOI)" ; - sh:description """Check if the File Data Entity has a formal identifier string such as a DOI""" ; - sh:path schema:identifier ; - sh:datatype xsd:anyURI ; - sh:severity sh:Info ; - sh:message """The File Data Entity MAY have a formal identifier specified through an `identifier` property""" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "File Data Entity: optional `url` property" ; - sh:description """Check if the File Data Entity has an optional `download` link""" ; - sh:path schema:url ; - sh:datatype xsd:anyURI ; - sh:severity sh:Info ; - sh:message """The File Data Entity MAY use a `url` property to denote a `download` link""" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "File Data Entity: optional `subjectOf` property" ; - sh:description """Check if the File Data Entity includes a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; - sh:path schema:subjectOf ; - sh:class schema:WebPage, schema:CreativeWork ; - sh:severity sh:Info ; - sh:message """The File Data Entity MAY include a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "File Data Entity: optional `mainEntityOfPage` property" ; - sh:description """Check if the File Data Entity has a `mainEntityOfPage` property""" ; - sh:path schema:mainEntityOfPage ; - sh:class schema:WebPage, schema:CreativeWork ; - sh:severity sh:Info ; - sh:message """The File Data Entity MAY have a `mainEntityOfPage` property""" ; - ] . - - -ro-crate:DirectoryDataEntityWebOptionalDistribution a sh:NodeShape ; - sh:name "Directory Data Entity: OPTIONAL `distribution` property" ; - sh:description """A Directory Data Entity MAY have a `distribution` property to denote the distribution of the files within the directory""" ; - sh:targetClass ro-crate:Directory ; - # Check if the Web-based Data Entity has a contentSize property - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "Directory Data Entity: optional `distribution` property" ; - sh:description """Check if the Directory Data Entity has a `distribution` property""" ; - sh:path schema:distribution ; - sh:datatype xsd:anyURI ; - sh:severity sh:Info ; - sh:message """The Directory Data Entity MAY have a `distribution` property to denote the distribution of the files within the directory""" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl deleted file mode 100644 index 8c85ea1a7..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/may/61_license_entity.ttl +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix xsd: . - - -ro-crate:LicenseOptionalAllowedValues a sh:NodeShape ; - sh:name "Root Data Entity: optional properties" ; - sh:description """Define the optional properties for the Root Data Entity (e.g., license)""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:name "License" ; - sh:description """Check if the RO-Crate has a license property with a URI or a textual description""" ; - sh:message """MAY have a URI (eg for Creative Commons or Open Source licenses). - MAY, if necessary be a textual description of how the RO-Crate may be used.""" ; - sh:minCount 1 ; - sh:nodeKind sh:IRIOrLiteral ; - sh:path schema_org:license ; - sh:or ( - [ sh:dataType xsd:string ] - [ sh:dataType xsd:anyURI ] - ) ; - ]. - -ro-crate:LicenseDefinition a sh:NodeShape ; - sh:name "License definition" ; - sh:description """Contextual entity representing a license with a name and description."""; - sh:targetClass schema_org:license ; - sh:property [ - a sh:PropertyShape ; - sh:name "License name" ; - sh:description "The license MAY have a name" ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:nodeKind sh:Literal ; - sh:path schema_org:name ; - sh:message "Missing license name" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "License description" ; - sh:description """The license MAY have a description""" ; - sh:maxCount 1; - sh:minCount 1 ; - sh:nodeKind sh:Literal ; - sh:path schema_org:description ; - sh:message "Missing license description" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl deleted file mode 100644 index 842e1c885..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/may/6_contextual_entity_metadata.ttl +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix rdf: . -@prefix schema: . -@prefix sh: . - -# RO-Crate 1.3, Encoding Format Entity — MAY include `WebPageElement` in -# `@type` if the `@id` links to a section of a webpage (i.e. the URI carries -# a fragment identifier such as `https://example.org/page.html#section-foo`). -# This is purely optional; the shape emits an `sh:Info` suggestion — never a -# violation — so users can see the permissive recommendation without their -# crate being marked invalid. -ro-crate:EncodingFormatOptionalWebPageElement a sh:NodeShape ; - sh:name "Encoding format: OPTIONAL `WebPageElement` type for section references" ; - sh:description """An encoding format contextual entity whose `@id` links to - a section of a webpage (the URI contains a fragment identifier) MAY - include `WebPageElement` in its `@type` - (RO-Crate 1.3, Encoding Format Entity).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?s schema:encodingFormat ?this . - FILTER(isIRI(?this)) - FILTER(CONTAINS(STR(?this), "#")) - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Encoding format: OPTIONAL `WebPageElement` @type" ; - sh:description "Check if the encoding format entity includes `WebPageElement` in its `@type`." ; - sh:path rdf:type ; - sh:hasValue schema:WebPageElement ; - sh:severity sh:Info ; - sh:message "An encoding format entity whose `@id` is a section of a webpage MAY include `WebPageElement` in its `@type`" ; - ] . - -# RO-Crate 1.3, Software Applications / ComputerLanguage — `alternateName` -# MAY be present if there is a shorter colloquial name for the software or -# language (e.g. `"alternateName": "GNU Bourne-Again Shell"` for Bash). -# Emits an `sh:Info` suggestion when the property is missing. -ro-crate:SoftwareApplicationOrComputerLanguageOptionalAlternateName a sh:NodeShape ; - sh:name "SoftwareApplication or ComputerLanguage: OPTIONAL `alternateName`" ; - sh:description """A contextual entity representing a `SoftwareApplication` - or a `ComputerLanguage` MAY declare an `alternateName` property to - provide a shorter colloquial name for the software or language - (RO-Crate 1.3, Provenance — Software Applications).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ?t . - FILTER(?t IN (schema:SoftwareApplication, schema:ComputerLanguage)) - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "SoftwareApplication/ComputerLanguage: OPTIONAL `alternateName`" ; - sh:description "Check if the entity declares an `alternateName` property." ; - sh:path schema:alternateName ; - sh:minCount 1 ; - sh:severity sh:Info ; - sh:message "A SoftwareApplication or ComputerLanguage MAY declare an `alternateName` property to provide a shorter colloquial name" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py new file mode 100644 index 000000000..3985f4986 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py @@ -0,0 +1,114 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from http import HTTPStatus +from typing import Any +from urllib.parse import urljoin + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError + +logger = logging.getLogger(__name__) + +_HANDLED_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) + + +@requirement(name="File Descriptor JSON-LD format") +class FileDescriptorJsonLdContext(PyFunctionCheck): + """Validate the RO-Crate 1.3 JSON-LD context reference.""" + + def __get_remote_context__(self, context_uri: str) -> object: + raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json, application/json"}) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'") + + content_type = raw_data.headers.get("Content-Type", "") + if "application/ld+json" not in content_type and "application/json" not in content_type: + link_header = raw_data.headers.get("Link", "") + match = re.search(r'<([^>]+)>;\s*rel="alternate";\s*type="application/(ld\+json|json)"', link_header) + if not match: + raise RuntimeError(f"Unable to retrieve the JSON-LD context from '{context_uri}'") + alternate_url = match.group(1) + if not alternate_url.startswith("http"): + alternate_url = urljoin(context_uri, alternate_url) + raw_data = HttpRequester().get( + alternate_url, + headers={"Accept": "application/ld+json, application/json"}, + ) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError(f"Unable to retrieve the JSON-LD context from alternate URL '{alternate_url}'") + + jsonld_context = raw_data.json()["@context"] + assert isinstance(jsonld_context, dict) + return jsonld_context + + def __check_remote_context__(self, context_uri: str) -> bool: + try: + return isinstance(self.__get_remote_context__(context_uri), dict) + except OfflineCacheMissError: + raise + except (OSError, RuntimeError, *_HANDLED_ERRORS): + logger.exception("Unexpected error during file descriptor context validation") + return False + + def __check_contexts__(self, context: ValidationContext, jsonld_context: object) -> bool: + is_valid = True + if isinstance(jsonld_context, str) and not self.__check_remote_context__(jsonld_context): + context.result.add_issue(f'Unable to retrieve the JSON-LD context "{jsonld_context}"', self) + is_valid = False + if isinstance(jsonld_context, list): + for nested_context in jsonld_context: + if not self.__check_contexts__(context, nested_context): + is_valid = False + return is_valid + + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) + def check_context(self, context: ValidationContext) -> CheckResultValue: + """Require the RO-Crate 1.3 JSON-LD context and validate it.""" + expected_context = "https://w3id.org/ro/crate/1.3/context" + try: + json_dict: dict[str, Any] = context.ro_crate.metadata.as_dict() + jsonld_context = json_dict.get("@context") + if "@context" not in json_dict: + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" does not contain a context', self + ) + return False + if not ( + jsonld_context == expected_context + or (isinstance(jsonld_context, list) and expected_context in jsonld_context) + ): + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" ' + f'does not reference the required context "{expected_context}"', + self, + ) + return False + return self.__check_contexts__(context, jsonld_context) + except ROCrateMetadataNotFoundError: + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_ERRORS: + logger.exception("Unexpected error during file descriptor context validation") + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py deleted file mode 100644 index bba7b3888..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_format.py +++ /dev/null @@ -1,727 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import re -from http import HTTPStatus -from typing import Any -from urllib.parse import urljoin - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError - -# set up logging -logger = logging.getLogger(__name__) - -_HANDLED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) -_HANDLED_REMOTE_CONTEXT_ERRORS = (*_HANDLED_METADATA_ERRORS, OSError, RuntimeError) - -# Property keys whose string values are literals (e.g. names, dates, URLs) rather -# than references to other entities, so a matching @id must not be flagged. -_LITERAL_KEYS = frozenset( - { - "name", - "description", - "encodingFormat", - "contentSize", - "datePublished", - "keywords", - "creditText", - "contentUrl", - "copyrightNotice", - "version", - "softwareVersion", - "value", - "propertyID", - "actionStatus", - "error", - "startTime", - "endTime", - "url", - "identifier", - } -) - - -@requirement(name="File Descriptor existence") -class FileDescriptorExistence(PyFunctionCheck): - """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" - - @check(name="File Descriptor Existence") - def test_existence(self, context: ValidationContext) -> CheckResultValue: - """ - Check if the file descriptor is present in the RO-Crate - """ - if context.settings.metadata_only: - logger.debug("Skipping file descriptor existence check in metadata-only mode") - context.record_skip(self, "metadata-only mode", "configured") - return CheckResult.SKIPPED - if not context.ro_crate.has_descriptor(): - message = f'file descriptor "{context.rel_fd_path}" is not present' - context.result.add_issue(message, self) - return False - return True - - @check(name="File Descriptor size check", depends_on=("File Descriptor Existence",)) - def test_size(self, context: ValidationContext) -> CheckResultValue: - """ - Check if the file descriptor is not empty - """ - if context.settings.metadata_only: - logger.debug("Skipping file descriptor existence check in metadata-only mode") - context.record_skip(self, "metadata-only mode", "configured") - return CheckResult.SKIPPED - if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: - context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) - return False - return True - - -@requirement(name="File Descriptor UTF-8 encoding") -class FileDescriptorEncodingCheck(PyFunctionCheck): - """ - The file descriptor MUST be UTF-8 encoded - """ - - @check(name="File Descriptor UTF-8 encoding", depends_on=("File Descriptor Existence",)) - def check(self, context: ValidationContext) -> CheckResultValue: - """ - Check if the file descriptor is UTF-8 encoded - """ - if context.settings.metadata_only: - logger.debug("Skipping file descriptor encoding check in metadata-only mode") - context.record_skip(self, "metadata-only mode", "configured") - return CheckResult.SKIPPED - try: - raw_data = context.ro_crate.metadata.as_bytes() - if isinstance(raw_data, str): - return True - raw_data.decode("utf-8") - return True - except UnicodeDecodeError: - context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) - context.abort_validation("file descriptor is not UTF-8 encoded") - if logger.isEnabledFor(logging.DEBUG): - logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) - return False - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - - -@requirement(name="File Descriptor JSON format") -class FileDescriptorJsonFormat(PyFunctionCheck): - """ - The file descriptor MUST be a valid JSON file - """ - - @check(name="File Descriptor JSON format", depends_on=("File Descriptor UTF-8 encoding",)) - def check(self, context: ValidationContext) -> CheckResultValue: - """Check if the file descriptor is in the correct format""" - try: - logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) - context.ro_crate.metadata.as_dict() - return True - except json.JSONDecodeError as e: - context.result.add_issue( - f'RO-Crate file descriptor "{context.rel_fd_path}" is not valid JSON: ' - f"{e.msg} at line {e.lineno}, column {e.colno} (char {e.pos})", - self, - ) - if logger.isEnabledFor(logging.DEBUG): - logger.debug("RO-Crate file descriptor is not valid JSON", exc_info=True) - # The metadata cannot be parsed: abort to avoid false positives downstream. - context.abort_validation(f"file descriptor is not valid JSON: {e}") - return False - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - context.result.add_issue( - f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self - ) - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - -@requirement(name="File Descriptor JSON-LD format") -class FileDescriptorJsonLdFormat(PyFunctionCheck): - """ - The file descriptor MUST be a valid JSON-LD file - """ - - def __get_remote_context__(self, context_uri: str) -> object: - raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json, application/json"}) - if raw_data.status_code != HTTPStatus.OK: - raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'", self) - logger.debug(f"Retrieved context from {context_uri}") - - content_type = raw_data.headers.get("Content-Type", "") - is_valid_content_type = "application/ld+json" in content_type or "application/json" in content_type - if not is_valid_content_type: - logger.debug( - f"The retrieved context from {context_uri} " - f"does not have a Content-Type of application/ld+json or application/json: " - f"the actual Content-Type is {content_type}. " - ) - link_header = raw_data.headers.get("Link", "") - logger.debug(f"Checking Link header for alternate JSON-LD context: {link_header}") - has_alternate_link = 'rel="alternate"' in link_header and ( - 'type="application/ld+json"' in link_header or 'type="application/json"' in link_header - ) - - if has_alternate_link: - logger.debug(f"Found alternate link for JSON-LD context in Link header: {link_header}") - match = re.search(r'<([^>]+)>;\s*rel="alternate";\s*type="application/(ld\+json|json)"', link_header) - if match: - alternate_url = match.group(1) - if not alternate_url.startswith("http"): - alternate_url = urljoin(context_uri, alternate_url) - logger.debug(f"Trying to retrieve JSON-LD context from alternate URL: {alternate_url}") - raw_data = HttpRequester().get( - alternate_url, headers={"Accept": "application/ld+json, application/json"} - ) - if raw_data.status_code != HTTPStatus.OK: - raise RuntimeError( - f"Unable to retrieve the JSON-LD context from alternate URL '{alternate_url}'", self - ) - logger.debug(f"Retrieved context from alternate URL {alternate_url}") - content_type = raw_data.headers.get("Content-Type", "") - if "application/ld+json" not in content_type and "application/json" not in content_type: - raise RuntimeError( - f"The retrieved context from alternate URL {alternate_url} " - "does not have a Content-Type of application/ld+json or application/json: " - f"the actual Content-Type is {content_type}. ", - self, - ) - else: - logger.debug(f"No valid alternate link found in Link header: {link_header}") - raise RuntimeError( - f"Unable to retrieve the JSON-LD context from {context_uri} and no valid " - f"alternate link found in Link header: {link_header}", - self, - ) - else: - logger.debug(f"No alternate link for JSON-LD context found in Link header: {link_header}") - raise RuntimeError( - f"Unable to retrieve the JSON-LD context from {context_uri} " - f"and no alternate link found in Link header: {link_header}", - self, - ) - - jsonLD = raw_data.json()["@context"] - assert isinstance(jsonLD, dict) - return jsonLD - - def __check_remote_context__(self, context_uri: str) -> bool: - try: - jsonLD = self.__get_remote_context__(context_uri) - assert isinstance(jsonLD, dict) - return True - except OfflineCacheMissError: - raise - except _HANDLED_REMOTE_CONTEXT_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - def __check_contexts__(self, context: ValidationContext, jsonld_context: object) -> bool: - """Get the keys of the context URI""" - is_valid = True - # if the context is a string, check if it is a valid URI - if isinstance(jsonld_context, str) and not self.__check_remote_context__(jsonld_context): - context.result.add_issue(f'Unable to retrieve the JSON-LD context "{jsonld_context}"', self) - is_valid = False - - # if the context is a dictionary, get the keys of the dictionary - if isinstance(jsonld_context, dict): - logger.debug(f"Detected dictionary context: {jsonld_context}") - - # if the context is a list of contexts, get the keys of each context - if isinstance(jsonld_context, list): - for ctx in jsonld_context: - if not self.__check_contexts__(context, ctx): - is_valid = False - # return if the context is valid - return is_valid - - @check( - name="File Descriptor @context property validation", - depends_on=("File Descriptor JSON format",), - ) - def check_context(self, context: ValidationContext) -> CheckResultValue: - """ - Check if the file descriptor contains - the @context property and it is a valid JSON-LD context - """ - try: - json_dict = context.ro_crate.metadata.as_dict() - if "@context" not in json_dict: - context.result.add_issue( - f'RO-Crate file descriptor "{context.rel_fd_path}" does not contain a context', self - ) - return False - - expected_context = "https://w3id.org/ro/crate/1.3/context" - jsonld_context = json_dict.get("@context") - - def has_expected_context(ctx: object) -> bool: - if isinstance(ctx, str): - return ctx == expected_context - if isinstance(ctx, list): - return expected_context in ctx - return False - - if not has_expected_context(jsonld_context): - context.result.add_issue( - f'RO-Crate file descriptor "{context.rel_fd_path}" ' - f'does not reference the required context "{expected_context}"', - self, - ) - return False - - # Check if the context is valid - return self.__check_contexts__(context, json_dict["@context"]) - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - def _check_flat_value_object(self, context: ValidationContext, entity: dict, fail_fast: bool) -> bool: - """Validate the JSON-LD `@value` object constraints; returns False on violation.""" - result = True - has_language = "@language" in entity - has_type = "@type" in entity - if has_language and has_type: - context.result.add_issue( - f'entity "{entity.get("@id", entity)}" is not a valid value object: ' - "@language and @type cannot coexist.", - self, - ) - result = False - if fail_fast: - return False - if has_language and not isinstance(entity["@value"], str): - context.result.add_issue( - f'entity "{entity.get("@id", entity)}" is not a valid value object: ' - "if @language is present, @value must be a string.", - self, - ) - result = False - if fail_fast: - return False - return result - - def _check_flat_non_root_dict(self, context: ValidationContext, entity: dict, fail_fast: bool) -> bool: - """A non-root dict must be either an `@id` reference or a `@value` object.""" - result = True - if "@id" in entity and "@value" in entity: - context.result.add_issue( - f'entity "{entity.get("@id", entity)}" contains both @id and @value: ' - "an object with an @value represents a value object, which is a literal value such as " - "a string, number, date, or language-tagged string. This object is not an identifiable " - "resource, but a simple literal value.", - self, - ) - result = False - if fail_fast: - return False - - if "@value" in entity: - if not self._check_flat_value_object(context, entity, fail_fast): - result = False - if fail_fast: - return False - # Every remaining entity with more than just @id must be a node object - elif "@id" not in entity or len(entity) > 1: - context.result.add_issue( - f'entity "{entity.get("@id", entity)}" is not a valid node object reference: ' - "it MUST have only @id, but no other properties.", - self, - ) - result = False - if fail_fast: - return False - return result - - def _is_entity_flat(self, context: ValidationContext, entity: Any, is_first: bool, fail_fast: bool) -> bool: - """Recursively check that `entity` corresponds to a flattened JSON-LD object.""" - result = True - if isinstance(entity, dict): - if not is_first: - return self._check_flat_non_root_dict(context, entity, fail_fast) - for elem in entity.values(): - if not self._is_entity_flat(context, elem, False, fail_fast): - result = False - if fail_fast: - return False - elif isinstance(entity, list): - for element in entity: - if not self._is_entity_flat(context, element, False, fail_fast): - result = False - if fail_fast: - return False - return result - - @check( - name="File Descriptor JSON-LD must be flattened", - depends_on=("File Descriptor JSON format",), - ) - def check_flattened(self, context: ValidationContext) -> CheckResultValue: - """Check if the file descriptor is flattened""" - try: - fail_fast = bool(context.settings.abort_on_first) - json_dict = context.ro_crate.metadata.as_dict() - result = True - for entity in json_dict["@graph"]: - if not self._is_entity_flat(context, entity, True, fail_fast): - context.result.add_issue( - f'RO-Crate file descriptor "{context.rel_fd_path}" ' - f'is not fully flattened at entity "{entity.get("@id", entity)}"', - self, - ) - result = False - if fail_fast: - return False - return result - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - @check( - name="Validation of the @id property of the file descriptor entities", - depends_on=("File Descriptor JSON format",), - ) - def check_identifiers(self, context: ValidationContext) -> CheckResultValue: - """Check if the file descriptor entities have the @id property""" - try: - json_dict = context.ro_crate.metadata.as_dict() - for entity in json_dict["@graph"]: - if "@id" not in entity: - context.result.add_issue( - f'Entity "{entity.get("name", None) or entity}" ' - f'of RO-Crate "{context.rel_fd_path}" ' - "file descriptor does not contain the @id attribute", - self, - ) - context.abort_validation("file descriptor entity does not contain the @id attribute") - return False - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - @check( - name="Validation of the @type property of the file descriptor entities", - depends_on=("File Descriptor JSON format",), - ) - def check_types(self, context: ValidationContext) -> CheckResultValue: - """Check if the file descriptor entities have the @type property""" - try: - json_dict = context.ro_crate.metadata.as_dict() - for entity in json_dict["@graph"]: - if "@type" not in entity: - context.result.add_issue( - f'Entity "{entity.get("name", None) or entity}" ' - f'of RO-Crate "{context.rel_fd_path}" ' - "file descriptor does not contain the @type attribute", - self, - ) - return False - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor type check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - @check( - name="Validation of unique @id values", - depends_on=("File Descriptor JSON format",), - ) - def check_unique_identifiers(self, context: ValidationContext) -> CheckResultValue: - try: - json_dict = context.ro_crate.metadata.as_dict() - identifiers = [entity.get("@id") for entity in json_dict.get("@graph", [])] - duplicates = {i for i in identifiers if i is not None and identifiers.count(i) > 1} - if duplicates: - context.result.add_issue( - f"Duplicate @id values detected in RO-Crate metadata: {sorted(duplicates)}", self - ) - return False - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping unique identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, key: str | None = None) -> str | None: - """ - Return an issue message if `value` references another entity by bare - string instead of an `{"@id": ...}` object, recursing into lists and - nested dictionaries; return None when no violation is found. - """ - if isinstance(value, str): - if key not in _LITERAL_KEYS and value in identifiers: - return f"Entity '{entity_id}' references '{value}' as a string; use {{\"@id\": \"{value}\"}}" - return None - if isinstance(value, list): - children: Any = value - elif isinstance(value, dict): - if "@value" in value or "@id" in value: - return None - children = value.values() - else: - return None - for child in children: - message = self._find_reference_issue(child, entity_id, identifiers, key) - if message: - return message - return None - - @check( - name="Validation of entity references", - depends_on=("File Descriptor JSON format",), - ) - def check_entity_references(self, context: ValidationContext) -> CheckResultValue: - try: - json_dict = context.ro_crate.metadata.as_dict() - graph = json_dict.get("@graph", []) - identifiers = {entity.get("@id") for entity in graph if entity.get("@id")} - - for entity in graph: - entity_id = entity.get("@id") - for key, value in entity.items(): - if key in ("@id", "@type", "@context"): - continue - message = self._find_reference_issue(value, entity_id, identifiers, key) - if message: - context.result.add_issue(message, self) - return False - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping entity reference check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - @check( - name="Validation of subject and keyword properties", - depends_on=("File Descriptor JSON format",), - ) - def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue: - try: - json_dict = context.ro_crate.metadata.as_dict() - graph = json_dict.get("@graph", []) - for entity in graph: - entity_id = entity.get("@id") - if "subject" in entity or "dct:subject" in entity or "dcterms:subject" in entity: - context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'about' for subjects", self) - return False - if "keyword" in entity: - context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'keywords'", self) - return False - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping subject and keyword check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - def __get_context_keys__(self, context: object) -> set: - """Get the keys of the context URI""" - if isinstance(context, str): - return self.__get_remote_context_keys__(context) - - # if the context is a dictionary, get the keys of the dictionary - if isinstance(context, dict): - return set(context.keys()) - - # if the context is a list of contexts, get the keys of each context - if isinstance(context, list): - keys = set() - for ctx in context: - keys.update(self.__get_context_keys__(ctx)) - return keys - return set() - - def __get_remote_context_keys__(self, context_uri: str) -> set: - """Get the keys of the context URI""" - - logger.debug(f"Retrieving context from {context_uri}...") - # Try to retrieve the context - raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json"}) - if raw_data.status_code != HTTPStatus.OK: - raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'") - - logger.debug(f"Retrieved context from {context_uri}") - - # Get the keys of the context - jsonLD = raw_data.json() - jsonLD_ctx = jsonLD["@context"] - if not isinstance(jsonLD_ctx, dict): - raise TypeError("The context is not a dictionary") - return set(jsonLD_ctx.keys()) - - def __check_entity_keys__( - self, entity: object, context_keys: set, unexpected_keys: dict[str, int] | None = None - ) -> dict[str, int]: - """Check if the entity is in the correct format""" - - def add_unexpected_key(k: str, u_keys: dict) -> None: - """Add a key to the unexpected keys dictionary""" - u_keys[k] = u_keys.get(k, 0) + 1 - - # Keys that should be skipped - SKIP_KEYS = {"@id", "@type", "@context", "@value", "@language"} - - # Ensure unexpected_keys is initialized - if unexpected_keys is None: - unexpected_keys = {} - - # If the entity is a dictionary, check each key - if isinstance(entity, dict): - for k, v in entity.items(): - if k not in context_keys and k not in SKIP_KEYS: - logger.debug(f"Key {k} not in context keys") - add_unexpected_key(k, unexpected_keys) - if isinstance(v, (dict, list)): - self.__check_entity_keys__(v, context_keys, unexpected_keys) - - # If the entity is a list, check each element - elif isinstance(entity, list): - for elem in entity: - self.__check_entity_keys__(elem, context_keys, unexpected_keys) - - return unexpected_keys - - @check( - name="Validation of the compaction format of the file descriptor", - depends_on=("File Descriptor JSON format",), - ) - def check_compaction(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 - """Check if the file descriptor is in the **compacted** JSON-LD format""" - try: - logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) - json_dict = context.ro_crate.metadata.as_dict() - logger.debug(f"JSONLD keys:{json_dict.keys()}") - - jsonld_context = json_dict.get("@context", None) - logger.debug(f"Context: {jsonld_context}") - - try: - context_keys = self.__get_context_keys__(jsonld_context) - logger.debug(f"{context_keys}") - except OfflineCacheMissError: - raise - except _HANDLED_REMOTE_CONTEXT_ERRORS as e: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - context.result.add_issue(str(e), self) - return False - - unexpected_keys = self.__check_entity_keys__(json_dict.get("@graph", []), context_keys) - logger.debug(f"Unexpected keys: {unexpected_keys}") - if len(unexpected_keys) > 0: - for k, v in unexpected_keys.items(): - logger.debug(f"Key {k} appears {v} times") - # Add the correct suffix to the message - suffix = "s" if v > 1 else "" - # Check if k is a term or a URI - if k.startswith("http"): - context.result.add_issue( - f'The The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "' - "because the compacted format requires simple terms as keys " - "(see https://www.w3.org/TR/json-ld-api/#compaction for more details).", - self, - ) - else: - context.result.add_issue( - f'The {v} occurrence{suffix} of the JSON-LD key "{k}" ' - f"{'is' if v == 1 else 'are'} not allowed in the compacted format " - "because it is not present in the @context of the document", - self, - ) - return False - - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except UnicodeDecodeError: - context.record_skip(self, "descriptor encoding check reported the failure", "exception") - return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS as e: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - context.result.add_issue(f"Unexpected error: {e}", self) - return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl deleted file mode 100644 index 7912fc66d..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_file-descriptor_metadata.ttl +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix dct: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix validator: . - -ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape ; - sh:name "Identify the RO-Crate Metadata File Descriptor" ; - sh:description """The RO-Crate Metadata File Descriptor entity describes the RO-Crate itself, and it is named as `*-ro-crate-metadata.json`. - It can be identified by name according to the RO-Crate specification - available at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.3/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?crate a schema:Dataset . - ?this schema:about ?crate . - FILTER(contains(str(?this), "ro-crate-metadata.json")) - } - """ - ] ; - - # Expand data graph with triples from the file data entity - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:ROCrateMetadataFileDescriptor ; - ] . - -ro-crate:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor entity existence" ; - sh:description "The RO-Crate JSON-LD MUST contain a Metadata File Descriptor entity typed as `schema:CreativeWork`" ; - sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; - sh:property [ - a sh:PropertyShape ; - sh:name "RO-Crate Metadata File Descriptor entity existence" ; - sh:description """Check if the RO-Crate Metadata File Descriptor entity exists, - i.e., if there exists an entity with @id matching *-ro-crate-metadata.json and type `schema:CreativeWork`""" ; - sh:path rdf:type ; - sh:hasValue ro-crate:ROCrateMetadataFileDescriptor ; - sh:minCount 1 ; - sh:message "The root of the document MUST have a Metadata File Descriptor entity" ; - ] . - -ro-crate:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor REQUIRED properties" ; - sh:description """RO-Crate Metadata Descriptor MUST be defined - according with the requirements details defined in - [RO-Crate Metadata File Descriptor](https://www.researchobject.org/ro-crate/1.3/root-data-entity.html#ro-crate-metadata-file-descriptor)""" ; - sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; - sh:property [ - a sh:PropertyShape ; - sh:name "Metadata File Descriptor entity type" ; - sh:description "Check if the RO-Crate Metadata File Descriptor has `@type` CreativeWork, as per schema.org" ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:path rdf:type ; - sh:hasValue schema_org:CreativeWork ; - sh:message "The RO-Crate metadata file MUST be a CreativeWork, as per schema.org" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Metadata File Descriptor entity: `about` property" ; - sh:description """Check if the RO-Crate Metadata File Descriptor has an `about` property referencing the Root Data Entity""" ; - sh:maxCount 1 ; - sh:minCount 1 ; - sh:path schema_org:about ; - # sh:class ro-crate:RootDataEntity ; - sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py deleted file mode 100644 index f03f09ffc..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/1_ro_crate_preview.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -# set up logging -logger = logging.getLogger(__name__) - - -@requirement(name="RO-Crate Website") -class ROCrateWebsiteChecker(PyFunctionCheck): - """ - If present, the RO-Crate Website MUST be a valid HTML5 document. - """ - - @check(name="RO-Crate Website HTML5 doctype") - def check_preview_html(self, context: ValidationContext) -> CheckResultValue: - try: - if context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is detached", "returned") - return CheckResult.SKIPPED - except ROCrateMetadataNotFoundError: - logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - preview_path = Path("ro-crate-preview.html") - if not context.ro_crate.has_file(preview_path): - return True - try: - content = context.ro_crate.get_file_content(preview_path, binary_mode=False) - if isinstance(content, bytes): - content = content.decode("utf-8", errors="strict") - if " str | None: - """ - Resolve the `cite-as` value to the http(s) URL that must be checked, - or None when there is nothing to verify (absent value, non-URL literal, - or a reference without an `id`). - """ - if not cite_as_raw: - return None - # cite-as can be a plain string literal or an entity reference {"@id": "..."} - if isinstance(cite_as_raw, str): - cite_as_url = cite_as_raw - else: - entity_id = getattr(cite_as_raw, "id", None) - if not isinstance(entity_id, str): - return None - cite_as_url = entity_id - if not cite_as_url or not cite_as_url.startswith("http"): - return None - return cite_as_url - - @check(name="Root Data Entity: `cite-as` MUST reference a downloadable item") - def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultValue: - if ( - context.settings.skip_availability_check - or not (context.settings.creation_time or context.settings.enforce_availability) - or context.settings.metadata_only - ): - context.record_skip(self, "availability check is disabled or not applicable", "configured") - return CheckResult.SKIPPED - - try: - root_entity = context.ro_crate.metadata.get_root_data_entity() - cite_as_url = self._resolve_cite_as_url(root_entity.get_property("cite-as")) - if not cite_as_url: - return True - - result = check_downloadable(cite_as_url) - if not result.is_downloadable: - context.result.add_issue( - f"The `cite-as` value '{cite_as_url}' MUST ultimately provide " - f"the RO-Crate as a downloadable item" + (f": {result.reason}" if result.reason else ""), - self, - ) - return False - - logger.debug( - "cite-as '%s' is downloadable via %s (url: %s)", - cite_as_url, - result.via, - result.download_url, - ) - return True - - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except (AttributeError, OSError, TypeError, ValueError) as e: - context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) - return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl deleted file mode 100644 index 905ea1f55..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_haspart.ttl +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix schema_org: . -@prefix sh: . - -ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; - sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; - sh:description """The Root Data Entity MUST directly or indirectly reference - all Data Entities in the RO-Crate via hasPart (RO-Crate 1.3).""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT $this ?unreferenced - WHERE { - $this a ro-crate:RootDataEntity . - ?unreferenced a ro-crate:DataEntity . - FILTER(?unreferenced != $this) - FILTER NOT EXISTS { - $this schema:hasPart+ ?unreferenced - } - } - """ ; - sh:severity sh:Violation ; - sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; - sh:message "The Root Data Entity MUST reference all Data Entities via hasPart (directly or indirectly)" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py deleted file mode 100644 index 50f32b2e2..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_identifier.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -logger = logging.getLogger(__name__) - - -@requirement(name="Root Data Entity identifier restriction") -class RootDataEntityIdentifierChecker(PyFunctionCheck): - """ - In an attached RO-Crate, the Root Data Entity @id MUST be ./ or an absolute URI. - """ - - @check(name="Root Data Entity: REQUIRED value") - def check_identifier(self, context: ValidationContext) -> CheckResultValue: - try: - if context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is detached", "returned") - return CheckResult.SKIPPED - root_entity = context.ro_crate.metadata.get_root_data_entity() - if root_entity.id == "./": - return True - if re.match(r"^[A-Za-z][A-Za-z0-9+\.-]*:", root_entity.id): - return True - context.result.add_issue( - "The Root Data Entity MUST be a `Dataset` (as per `schema.org`) and use an IRI or `./` as identifier", - self, - ) - return False - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except (AttributeError, TypeError, ValueError) as e: - context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) - return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl deleted file mode 100644 index ad8c31f9f..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_data_entity_metadata.ttl +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix prof: . -@prefix sh: . -@prefix validator: . -@prefix xsd: . -@prefix dct: . - -# Selects the Root Data Entity (the Dataset that the metadata file is `about`). -ro-crate:MetadataAboutTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } - """ . - -ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape ; - sh:name "Identify the Root Data Entity of the RO-Crate" ; - 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. - It is a schema:Dataset and is indirectly identified by the about property of the metadata descriptor in the RO-Crate - (see the definition at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.3/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores)). - """ ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a schema:Dataset . - ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } - """ - ] ; - - # Expand data graph with triples from the file data entity - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:RootDataEntity ; - ] . - -ro-crate:RootDataEntityType a sh:NodeShape ; - sh:name "RO-Crate Root Data Entity type" ; - sh:description "The Root Data Entity MUST be a `Dataset` (as per `schema.org`)" ; - sh:target ro-crate:MetadataAboutTarget ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity type" ; - sh:description "Check if the Root Data Entity is a `Dataset` (as per `schema.org`)" ; - sh:path rdf:type ; - sh:hasValue schema_org:Dataset ; - sh:minCount 1 ; - sh:message """The Root Data Entity MUST be a `Dataset` (as per `schema.org`)""" ; - ] ; - # Validate that if the publisher is specified, it is an Organization or a Person - sh:property [ - sh:path schema_org:publisher ; - sh:severity sh:Violation ; - sh:name "Root Data Entity: `publisher` property" ; - sh:description """Check if the Root Data Entity has a `publisher` property of type `Organization` or `Person`.""" ; - sh:or ( - [ - sh:class schema_org:Organization - ] - [ - sh:class schema_org:Person - ] - ) ; - sh:message """The Root Data Entity MUST have a `publisher` property of type `Organization` or `Person`.""" ; - ] . - -ro-crate:RootDataEntityRequiredProperties a sh:NodeShape ; - sh:name "RO-Crate Root Data Entity REQUIRED properties" ; - sh:description "The Root Data Entity MUST have a `name`, `description`, `license` and `datePublished`" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: `name` property" ; - sh:description """Check if the Root Data Entity includes a `name` (as specified by schema.org) - to clearly identify the dataset and distinguish it from other datasets.""" ; - sh:minCount 1 ; - sh:nodeKind sh:Literal ; - sh:path schema_org:name ; - sh:message "The Root Data Entity MUST have a `name` property (as specified by schema.org)" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: `description` property" ; - sh:description """Check if the Root Data Entity includes a `description` (as specified by schema.org) - to provide a human-readable description of the dataset.""" ; - sh:minCount 1 ; - sh:nodeKind sh:Literal ; - sh:path schema_org:description ; - sh:message "The Root Data Entity MUST have a `description` property (as specified by schema.org)" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: `license` property" ; - sh:description """Check if the Root Data Entity includes a `license` property (as specified by schema.org) - to provide information about the license of the dataset.""" ; - sh:path schema_org:license ; - sh:minCount 1 ; - sh:nodeKind sh:IRIOrLiteral ; - sh:message """The Root Data Entity MUST have a `license` property (as specified by schema.org).""" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: `datePublished` property" ; - sh:description """Check if the Root Data Entity includes a `datePublished` (as specified by schema.org) - to provide the date when the dataset was published. The datePublished MUST be a valid ISO 8601 date.""" ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:nodeKind sh:Literal ; - sh:datatype xsd:string ; - sh:path schema_org:datePublished ; - sh:pattern "^([\\+-]?\\d{4})((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)?[0-5]\\d)?|24:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" ; - sh:message "The Root Data Entity MUST have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date" ; - ] . - -ro-crate:RootDataEntityOptionalConformsToProperty a sh:NodeShape ; - sh:name "RO-Crate Root Data Entity: optional `conformsTo` property value restriction" ; - sh:description """If the Root Data Entity includes a `conformsTo` property, its values MUST reference Profile entities.""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - sh:name "Root Data Entity: optional `conformsTo` property value restriction" ; - sh:description """Check if the Root Data Entity's `conformsTo` property values reference Profile entities.""" ; - sh:path dct:conformsTo ; - sh:class prof:Profile ; - sh:message """If the Root Data Entity includes a `conformsTo` property, its values MUST reference Profile entities.""" ; - ] . - -ro-crate:RootDataEntityVersionlessProfileProhibition a sh:NodeShape ; - sh:name "RO-Crate Root Data Entity: MUST NOT declare version-less conformsTo profile" ; - sh:description """The Root Data Entity MUST NOT declare the version-less generic RO-Crate profile URI - (https://w3id.org/ro/crate) in its conformsTo property. - Only versioned profile URIs (e.g., https://w3id.org/ro/crate/1.3) are permitted on the Root Data Entity. - The version-less URI is reserved for referenced RO-Crate data entities.""" ; - sh:target ro-crate:MetadataAboutTarget ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this dct:conformsTo . - } - """ ; - sh:name "Root Data Entity: MUST NOT declare version-less conformsTo" ; - sh:message "The Root Data Entity MUST NOT declare the version-less RO-Crate profile URI (https://w3id.org/ro/crate) in its conformsTo property. Use a versioned URI such as https://w3id.org/ro/crate/1.3 instead." ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py deleted file mode 100644 index 501ac7eb2..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/2_root_identifier_property.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -# set up logging -logger = logging.getLogger(__name__) - - -@requirement(name="Root Data Entity: identifier PropertyValue") -class RootIdentifierPropertyChecker(PyFunctionCheck): - """ - If the Root Data Entity identifier references a PropertyValue, it MUST include value. - """ - - @check(name="Root Data Entity: identifier PropertyValue value") - def check_identifier_values(self, context: ValidationContext) -> CheckResultValue: - try: - root = context.ro_crate.metadata.get_root_data_entity() - identifiers = root.get_property("identifier") - if identifiers is None: - return True - identifiers = identifiers if isinstance(identifiers, list) else [identifiers] - for identifier in identifiers: - if not hasattr(identifier, "has_type"): - continue - if not identifier.has_type("PropertyValue"): - continue - if not identifier.get_property("value"): - context.result.add_issue("PropertyValue identifiers MUST include a `value`", self) - return False - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except (AttributeError, TypeError, ValueError) as e: - context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) - return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py deleted file mode 100644 index a054e8e69..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.py +++ /dev/null @@ -1,335 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib -import re - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging -from rocrate_validator.utils.signposting import check_downloadable - -# set up logging -logger = logging.getLogger(__name__) - - -@requirement(name="Data Entity: REQUIRED resource availability") -class DataEntityRequiredChecker(PyFunctionCheck): - """ - Resources corresponding to local Data Entities MUST be present in the RO-Crate payload - """ - - @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 - """ - Check the presence of the Data Entity in the RO-Crate - """ - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if is_detached: - logger.debug("Skipping data entity payload checks for detached RO-Crate") - context.record_skip(self, "RO-Crate is detached", "returned") - return CheckResult.SKIPPED - # Skip the check in metadata-only mode - if context.settings.metadata_only: - logger.debug("Skipping file descriptor existence check in metadata-only mode") - context.record_skip(self, "metadata-only mode", "configured") - return CheckResult.SKIPPED - # Perform the check - result = True - try: - entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - assert entity.id is not None, "Entity has no @id" - logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) - try: - logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) - if entity.has_local_identifier(): - logger.debug( - "Ignoring the Data Entity '%s' as it is a local entity with a local identifier. " - "According to the RO-Crate specification, local entities with local identifiers " - "are not required to be included in the RO-Crate payload" - "(see https://github.com/ResearchObject/ro-crate/issues/400#issuecomment-2779152885 and " - "https://github.com/ResearchObject/ro-crate/pull/426 for more details)", - entity.id, - ) - continue - if not entity.has_relative_path(): - logger.debug( - "Ignoring the Data Entity '%s' as it is a local entity with an absolute path. " - "According to the RO-Crate specification, local entities with absolute paths " - "are not required to be included in the RO-Crate payload. " - "It is only recommended that they exist at the time of RO-Crate creation.", - entity.id, - ) - continue - if not entity.is_available(): - context.result.add_issue( - f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self - ) - result = False - except (AttributeError, OSError, TypeError, ValueError) as e: - context.result.add_issue( - f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self - ) - if logger.isEnabledFor(logging.DEBUG): - logger.debug(e, exc_info=True) - result = False - if not result and context.fail_fast: - return result - return result - - -@requirement(name="Detached RO-Crate: data entities MUST be web-based") -class DetachedDataEntityChecker(PyFunctionCheck): - """ - In a detached RO-Crate, all Data Entities MUST be web-based - resources (i.e., have an absolute URL as @id). - """ - - @check(name="Detached RO-Crate: data entities MUST be web-based") - def check_detached_entities(self, context: ValidationContext) -> CheckResultValue: - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if not is_detached: - context.record_skip(self, "RO-Crate is attached", "returned") - return CheckResult.SKIPPED - result = True - root_entity_id = None - with contextlib.suppress(ValueError): - root_entity_id = context.ro_crate.metadata.get_root_data_entity().id - try: - entities = context.ro_crate.metadata.get_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - if root_entity_id and entity.id == root_entity_id: - continue - if not entity.is_remote(): - context.result.add_issue( - f"Data Entity '{entity.id}' is not web-based, " - f"but in a detached RO-Crate all Data Entities " - f"MUST have an absolute URL as @id", - self, - ) - result = False - if context.fail_fast: - return False - return result - - -@requirement(name="Data Entity: identifier requirements") -class DataEntityIdentifierChecker(PyFunctionCheck): - """ - Data Entity identifiers must be valid URI references and use relative paths for payload files. - """ - - @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 - result = True - root_entity_id = None - root_entity_is_local = False - root_entity_absolute_path = None - with contextlib.suppress(ValueError): - root_data_entity = context.ro_crate.metadata.get_root_data_entity() - root_entity_id = root_data_entity.id - root_entity_is_local = ( - root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False - ) - root_entity_absolute_path = root_data_entity.id_as_path if root_data_entity.has_absolute_path() else None - try: - entities = context.ro_crate.metadata.get_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - if root_entity_id and entity.id == root_entity_id: - continue - if not root_entity_is_local and not entity.is_remote(): - context.result.add_issue( - f"Data Entity '{entity.id}' has a local identifier but the Root Data Entity " - "does not have a local identifier", - self, - ) - result = False - if context.fail_fast: - return False - if entity.has_local_identifier(): - continue - if "\\" in entity.id or " " in entity.id: - context.result.add_issue( - f"Data Entity '{entity.id}' has an invalid @id; use URI-compatible paths", self - ) - result = False - if context.fail_fast: - return False - if ( - root_entity_is_local - and not str(entity.id_as_path).startswith(str(root_entity_absolute_path)) - and not str(entity.id).startswith("./") - and (str(entity.id).startswith(("/", "file://"))) - ): - context.result.add_issue( - f"Data Entity '{entity.id}' MUST use a relative @id within the RO-Crate root", self - ) - result = False - if context.fail_fast: - return False - return result - - @check(name="Data Entity: relative @id for payload files") - def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if is_detached: - context.record_skip(self, "RO-Crate is detached", "returned") - return CheckResult.SKIPPED - result = True - try: - entities = context.ro_crate.metadata.get_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - if entity.has_local_identifier() or entity.is_remote(): - continue - if entity.has_absolute_path() and ( - context.ro_crate.has_file(entity.id_as_path) or context.ro_crate.has_directory(entity.id_as_path) - ): - context.result.add_issue( - f"Data Entity '{entity.id}' should use a relative @id within the RO-Crate root", self - ) - result = False - if context.fail_fast: - return False - return result - - -@requirement(name="Data Entity: citation references") -class DataEntityCitationChecker(PyFunctionCheck): - """ - Citation references must include an absolute URI. - """ - - @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> CheckResultValue: - result = True - try: - entities = context.ro_crate.metadata.get_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - citations = entity.get_property("citation") - if citations is None: - continue - citation_list = citations if isinstance(citations, list) else [citations] - for citation in citation_list: - if isinstance(citation, str): - citation_id = citation - elif hasattr(citation, "id"): - citation_id = citation.id - else: - context.result.add_issue( - f"Citation for Data Entity '{entity.id}' must reference a publication @id", self - ) - result = False - if context.fail_fast: - return False - continue - if not re.match(r"^[A-Za-z][A-Za-z0-9+\.-]*:", citation_id): - context.result.add_issue(f"Citation for Data Entity '{entity.id}' must be an absolute URI", self) - result = False - if context.fail_fast: - return False - return result - - -@requirement(name="Web-based Data Entity: REQUIRED availability") -class WebDataEntityRequiredChecker(PyFunctionCheck): - """ - Web-based Data Entities MUST be directly downloadable at the time of creation - (RO-Crate 1.3). Downloadability is verified via Signposting (rel=item, - rel=describedby), direct Content-Type inspection, and content negotiation. - """ - - @staticmethod - def _not_downloadable_message(entity_id: str, dl) -> str: - """Build the issue message for a web-based Data Entity that is not directly downloadable.""" - if dl.reason and "HTML" in dl.reason: - return ( - f"Web-based Data Entity '{entity_id}' references an HTML page " - f"(possible splash page or viewer application); " - f"it MUST be directly downloadable" - ) - msg = f"Web-based Data Entity '{entity_id}' is not directly downloadable" - if dl.reason: - msg += f": {dl.reason}" - return msg - - @check(name="Web-based Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> CheckResultValue: - if ( - context.settings.skip_availability_check - or not (context.settings.creation_time or context.settings.enforce_availability) - or context.settings.metadata_only - ): - context.record_skip(self, "availability check is disabled or not requested", "configured") - return CheckResult.SKIPPED - result = True - try: - entities = context.ro_crate.metadata.get_web_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - assert entity.id is not None, "Entity has no @id" - # Skip directory URIs: assumed available, not directly downloadable by spec - if entity.id.endswith("/"): - logger.debug("Skipping downloadability check for directory entity '%s'", entity.id) - continue - try: - dl = check_downloadable(entity.id) - if not dl.is_downloadable: - context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) - result = False - except (OSError, RuntimeError, TypeError, ValueError) as e: - context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) - result = False - if not result and context.fail_fast: - return result - return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl deleted file mode 100644 index cc91f01ed..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/4_data_entity_metadata.ttl +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix owl: . -@prefix validator: . - -ro-crate:DataEntityRequiredProperties a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; - sh:description """A Data Entity MUST be a `URI Path` relative to the ROCrate root, - or an absolute URI""" ; - sh:targetClass ro-crate:DataEntity ; - sh:property [ - sh:name "Data Entity: @id value restriction" ; - sh:description """Check if the Data Entity has an absolute or relative URI as `@id`""" ; - sh:path [ - sh:inversePath rdf:type - ] ; sh:nodeKind sh:IRI ; sh:severity sh:Violation ; sh:message """Data Entities MUST have an absolute or relative URI as @id.""" ; - ] . - -ro-crate:FileDataEntity a sh:NodeShape ; - sh:name "File Data Entity: REQUIRED properties" ; - sh:description """A File Data Entity MUST be a `File`. - `File` is an RO-Crate alias for the schema.org `MediaObject`. - The term `File` here is liberal, and includes "downloadable" resources where `@id` is an absolute URI. - """ ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?this a schema:MediaObject . - FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) - } - """ - ] ; - - # Irrelevant for validation, but useful for debugging - # sh:property [ - # sh:name "File Data Entity: REQUIRED type" ; - # sh:description """Check if the File Data Entity has `File` as `@type`. - # `File` is an RO-Crate alias for the schema.org `MediaObject`. - # """ ; - # sh:path rdf:type ; - # sh:or ( - # [ sh:hasValue ro-crate:File ] - # [ sh:hasValue schema_org:MediaObject ] - # ) ; - # sh:severity sh:Violation ; - # sh:message """File Data Entities MUST have "File" as a value for @type.""" ; - # ] ; - - # Expand data graph with triples to mark the matching entities as File instances - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:File ; - ] ; - # Expand data graph with triples from the file data entity - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:DataEntity ; - ] . - -ro-crate:DirectoryDataEntity a sh:NodeShape ; - sh:name "Directory Data Entity: REQUIRED properties" ; - sh:description """A Directory Data Entity MUST be of @type `Dataset`. - The term `directory` here includes HTTP file listings where `@id` is an absolute URI. - """ ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?this a schema:Dataset . - # Exclude all dataset entities that ends with `./#` - FILTER(?this != ?root) - FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) - } - """ - ] ; - - # Uncomment for debugging - # sh:property [ - # sh:name "Test Directory" ; - # sh:description """Data Entities representing directories MUST have "Directory" as a value for @type.""" ; - # sh:path rdf:type ; - # sh:hasValue ro-crate:File ; - # sh:severity sh:Violation ; - # ] ; - - # Expand data graph with triples to mark the matching entities as Directory instances - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:Directory ; - ] ; - - # Expand data graph with triples from the directory data entity - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:DataEntity ; - ] ; - - # Ensure that the directory data entity is a dataset - sh:property [ - sh:name "Directory Data Entity: REQUIRED type" ; - sh:description """Check if the Directory Data Entity has `Dataset` as `@type`.""" ; - sh:path rdf:type ; - sh:hasValue schema_org:Dataset ; - sh:severity sh:Violation ; - ] . - -ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; - sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; - sh:targetClass ro-crate:DataEntity ; - sh:property - [ - a sh:PropertyShape ; - sh:path [ sh:oneOrMorePath [ sh:inversePath schema_org:hasPart ] ] ; - sh:qualifiedValueShape [ sh:class ro-crate:RootDataEntity ] ; - sh:qualifiedMinCount 1 ; - sh:name "Data Entity MUST be directly referenced" ; - sh:description """Check if the Data Entity is linked, either directly or indirectly, to the `Root Data Entity` using the `hasPart` (as defined in `schema.org`) property" """ ; - sh:message "A Data Entity MUST be directly or indirectly linked to the `Root Data Entity` through the `hasPart` property" ; - ] . - -ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; - sh:name "Generic Data Entity: REQUIRED properties" ; - sh:description """A Data Entity other than a File or a Directory MUST be a `DataEntity`""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root ?metadatafile WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?root schema:hasPart ?this . - FILTER(?this != ?root) - FILTER(?this != ?metadatafile) - FILTER NOT EXISTS { - ?this a schema:MediaObject . - ?this a schema:Dataset . - } - } - """ - ] ; - - # Expand data graph with triples to mark the matching entities as GenericDataEntity instances - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:GenericDataEntity ; - ] ; - - # Expand data graph with triples to mark the matching entities as DataEntity instances - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:DataEntity ; - ] . - -# Uncomment for debugging -# ro-crate:TestGenericDataEntity a sh:NodeShape ; -# sh:disabled true ; -# sh:targetClass ro-crate:GenericDataEntity ; -# sh:name "Generic Data Entity: test invalid property"; -# sh:description """Check if the GenericDataEntity has the invalidProperty property""" ; -# sh:property [ -# sh:minCount 1 ; -# sh:maxCount 1 ; -# sh:path ro-crate:invalidProperty ; -# sh:severity sh:Violation ; -# sh:message "Testing the generic data entity"; -# sh:datatype xsd:string ; -# sh:message "Testing for the invalidProperty of the generic data entity"; -# ] . - -# Uncomment for debugging -# ro:testDirectory a sh:NodeShape ; -# sh:name "Definition of Test Directory" ; -# sh:description """A Test Directory is a digital object that is stored in a file format""" ; -# sh:targetClass ro-crate:Directory ; - -# sh:property [ -# sh:name "Test Directory instance" ; -# sh:description """Check if the Directory DataEntity instance has the fake property ro-crate:foo""" ; -# sh:path rdf:type ; -# sh:hasValue ro-crate:foo ; -# sh:severity sh:Violation ; -# ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl deleted file mode 100644 index d1a296bd3..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/5_web_data_entity_metadata.ttl +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix dct: . -@prefix schema_org: . -@prefix sh: . -@prefix owl: . -@prefix xsd: . -@prefix validator: . - - -ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; - sh:name "Web-based Data Entity: REQUIRED properties" ; - sh:description """A Web-based Data Entity is a `File` identified by an absolute URL""" ; - - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a schema:MediaObject . - FILTER(!contains(str(?this), "ro-crate-metadata.json")) - FILTER regex(str(?this), "^(https?|ftps?)://", "i") - } - """ - ] ; - - # Expand data graph with triples which identify the web-based data entity - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:WebDataEntity ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl deleted file mode 100644 index fb4268b33..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/6_contextual_entity_metadata.ttl +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix owl: . -@prefix rdf: . -@prefix ro: <./> . -@prefix ro-crate: . -@prefix schema: . -@prefix schema_org: . -@prefix sh: . -@prefix validator: . -@prefix xsd: . - -ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; - sh:name "Identify Contextual Entities" ; - sh:description """Mark entities as Contextual Entities if they are in the RO-Crate - metadata but are not Data Entities, not the Root Data Entity, - and not the Metadata File Descriptor. The query uses `?this ?p ?o` - (subject of any triple) — SHACL rules from earlier shapes (e.g. - CreativeWorkAuthorDefinition) tag referenced-but-undescribed entities - with rdf:type, so they appear as subjects and are therefore captured. - Vocabulary namespaces loaded from ontologies (RDF/RDFS/OWL/XSD, - schema.org, bioschemas, w3id.org/ro, …) are excluded at the query - level; domain-specific legacy filters that wrongly excluded legitimate - crate identifiers (iana, urn, foaf, …) have been removed.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - # Structural criteria: IRI or blank node, subject of some triple. - # SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) - # tag even referenced-but-undescribed entities so they become - # subjects and are correctly classified as Contextual Entities. - # - # The distinct subjects are materialised by a subquery FIRST so the - # exclusion FILTERs below run once per entity instead of once per - # triple (~2.5k subjects vs ~40k triples on a large crate). - { SELECT DISTINCT ?this WHERE { - ?this ?p ?o . - FILTER(isIRI(?this) || isBlank(?this)) - } } - - # Structural exclusions — Metadata Descriptor, Root Data Entity, - # Data Entities (File/Dataset or ro-crate:DataEntity), Ontology. - FILTER NOT EXISTS { ?this schema:about ?anyRoot } - FILTER NOT EXISTS { ?anyMF schema:about ?this } - FILTER NOT EXISTS { ?this a ro-crate:DataEntity } - FILTER NOT EXISTS { ?this a schema:MediaObject } - FILTER NOT EXISTS { ?this a schema:Dataset } - FILTER NOT EXISTS { ?this a owl:Ontology } - - # Vocabulary / profile namespace exclusions. Required because - # the RO-Crate 1.3 profile loads ontology.ttl into the same data - # graph as the metadata file; terms from those vocabularies - # appear as subjects with rdf:type + rdfs:label and would - # otherwise be misclassified. - FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "https://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/terms/")) - FILTER(!STRSTARTS(STR(?this), "http://www.opengis.net/")) - FILTER(!STRSTARTS(STR(?this), "https://w3id.org/")) - FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) - FILTER(!STRSTARTS(STR(?this), "https://codemeta.github.io/")) - FILTER(!STRSTARTS(STR(?this), "http://pcdm.org/")) - FILTER(!STRSTARTS(STR(?this), "http://xmlns.com/foaf/")) - FILTER(!STRSTARTS(STR(?this), "http://creativecommons.org/ns")) - FILTER(!STRSTARTS(STR(?this), "http://www.iana.org/")) - FILTER(!STRSTARTS(STR(?this), "https://www.iana.org/")) - FILTER(!STRSTARTS(STR(?this), "https://nationaalarchief.nl/archieven/archief/")) - FILTER(!STRSTARTS(STR(?this), "https://www.nationalarchives.gov.uk")) - - # Narrow urn: exclusion — only the internal ontology versionIRI. - # Unlike the legacy blanket `urn:` filter, this keeps legitimate - # `urn:uuid:*` contextual entities (e.g. DigitalDocument) in play. - FILTER(!STRSTARTS(STR(?this), "urn:absolute:")) - } - """ - ] ; - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:ContextualEntity ; - ] . - -ro-crate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; - sh:name "Identify License Entity" ; - sh:description """Mark a license entity any Data Entity referenced by the `schema:license` property that is defined in the RO-Crate metadata.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?subject schema:license ?this . - FILTER(isIRI(?this)) - # FILTER EXISTS { ?this ?p ?o . FILTER(?p NOT IN (owl:sameAs, rdf:type)) } - } - """ - ] ; - - # Expand data graph with triples to mark the matching entities as License instances - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:License ; - ] ; - - # Expand data graph with triples from the file data entity - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:ContextualEntity ; - ] . - -ro-crate:WebSiteRecommendedProperties a sh:NodeShape ; - sh:name "WebSite RECOMMENDED Properties" ; - sh:description """A `WebSite` MUST be identified by a valid IRI and MUST have a `name` property.""" ; - sh:targetClass schema:WebSite ; - sh:property [ - sh:path [ - sh:inversePath rdf:type - ] ; sh:datType sh:IRI ; sh:name "WebSite: value restriction of its identifier" ; sh:description "Check if the WebSite has a valid IRI" ; sh:message "A WebSite MUST have a valid IRI" ; - ] ; sh:property [ - sh:path schema:name ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:name "WebSite: REQUIRED `name` property" ; - sh:description "Check if the WebSite has a `name` property" ; - sh:message "A WebSite MUST have a `name` property" ; - ] . - -ro-crate:CreativeWorkAuthorDefinition a sh:NodeShape, validator:HiddenShape ; - sh:name "CreativeWork Author Definition" ; - sh:description """Define the `CreativeWorkAuthor` as the `Person` object of the `schema:author` predicate.""" ; - sh:targetObjectsOf schema:author ; - sh:rule [ - a sh:TripleRule ; - sh:subject sh:this ; - sh:predicate rdf:type ; - sh:object ro-crate:CreativeWorkAuthor ; - sh:condition [ - sh:property [ - sh:path rdf:type ; sh:hasValue schema:Person ; sh:minCount 1 - ] ;] ;] . - -ro-crate:ThumbnailReferencesFile a sh:NodeShape ; - sh:name "Thumbnail reference" ; - sh:description """If `thumbnail` is present, it MUST reference a File data entity.""" ; - sh:targetSubjectsOf schema:thumbnail ; - sh:property [ - sh:path schema:thumbnail ; - sh:class schema:MediaObject ; - sh:message "If present, `thumbnail` MUST reference a File data entity" ; - ] . - -# A contextual entity representing a `ComputerLanguage` and/or `SoftwareApplication` -# MUST have `name`, `url`, and `version`. -ro-crate:SoftwareApplicationOrComputerLanguageRequiredProperties a sh:NodeShape ; - sh:name "SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`" ; - sh:description """A contextual entity representing a `SoftwareApplication` - or a `ComputerLanguage` MUST declare `name`, `url`, and `version` - properties (RO-Crate 1.3, Provenance — Software Applications; - Workflows — programmingLanguage).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ?t . - FILTER(?t IN (schema:SoftwareApplication, schema:ComputerLanguage)) - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "SoftwareApplication/ComputerLanguage: REQUIRED `name`" ; - sh:description "Check that the entity declares a `name` property." ; - sh:path schema:name ; - sh:minCount 1 ; - sh:severity sh:Violation ; - sh:message "A SoftwareApplication or ComputerLanguage MUST have a `name` property" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "SoftwareApplication/ComputerLanguage: REQUIRED `url`" ; - sh:description "Check that the entity declares a `url` property." ; - sh:path schema:url ; - sh:minCount 1 ; - sh:severity sh:Violation ; - sh:message "A SoftwareApplication or ComputerLanguage MUST have a `url` property" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "SoftwareApplication/ComputerLanguage: REQUIRED `version`" ; - sh:description "Check that the entity declares a `version` property indicating a known release." ; - sh:path schema:version ; - sh:minCount 1 ; - sh:severity sh:Violation ; - sh:message "A SoftwareApplication or ComputerLanguage MUST have a `version` property (SHOULD indicate a known version that linked workflows/scripts were developed/tested with)" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/9_schema_org_iri_protocol.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/9_schema_org_iri_protocol.ttl deleted file mode 100644 index 082e38080..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/must/9_schema_org_iri_protocol.ttl +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -@prefix ro-crate: . -@prefix sh: . - -ro-crate:SchemaOrgTypeProtocol a sh:NodeShape ; - sh:name "Schema.org @type protocol" ; - sh:description "Schema.org @type values MUST use the http://schema.org/ namespace defined by the RO-Crate context." ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT DISTINCT ?this - WHERE { - ?this a ?schemaType . - } - """ - ] ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT $this ?schemaType ?term ?httpType - WHERE { - $this a ?schemaType . - FILTER(STRSTARTS(STR(?schemaType), "https://schema.org/")) - BIND(SUBSTR(STR(?schemaType), STRLEN("https://schema.org/") + 1) AS ?term) - BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpType) - } - """ ; - sh:message "Entity {?this} declares the @type '{?schemaType}', which is not a term of the RO-Crate @context: the context maps Schema.org terms to 'http://schema.org/', so the entity MUST be typed '{?term}' (i.e. '{?httpType}')" ; - sh:severity sh:Violation - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py deleted file mode 100644 index 2b3b377f8..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_identifier_format.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -RECOMMENDED checks on entity @id values: - - @id SHOULD NOT use ../ to climb out of the RO-Crate Root - - International characters SHOULD be native UTF-8, not percent-encoded - - Contextual entities SHOULD use absolute URI (permalink) or #-prefixed @id -""" - -import contextlib -import re - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -logger = logging.getLogger(__name__) - -# Matches any %XX sequence where XX decodes to a non-ASCII byte (0x80-0xFF). -# These are UTF-8 continuation / leading bytes for multi-byte code-points. -_PCT_NON_ASCII_RE = re.compile(r"%[89A-Fa-f][0-9A-Fa-f]") - -# RFC 3986 absolute URI scheme prefix: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) -# e.g. http:, https:, mailto:, urn:, doi:, ftp:, file: -_ABSOLUTE_URI_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+\-.]*:") - - -@requirement(name="Entity identifier: format recommendations") -class EntityIdentifierFormatChecker(PyFunctionCheck): - """ - Checks that entity @id values follow RO-Crate 1.3 RECOMMENDED conventions: - no parent-directory traversal, native UTF-8 for international characters, - and '#'-prefixed identifiers for named local entities. - """ - - @check(name="Entity identifiers SHOULD NOT use ../", severity=Severity.RECOMMENDED) - def check_no_parent_traversal(self, context: ValidationContext) -> CheckResultValue: - """ - @id paths SHOULD NOT use `../` to climb out of the RO-Crate Root - (RO-Crate 1.3, JSON-LD appendix). - """ - result = True - try: - entities = context.ro_crate.metadata.as_dict().get("@graph", []) - except ROCrateMetadataNotFoundError: - logger.debug("Skipping parent traversal check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - entity_id = entity.get("@id", "") - if "../" in entity_id: - context.result.add_issue( - f"Entity @id '{entity_id}' uses '../' to traverse above the " - f"RO-Crate Root; @id paths SHOULD NOT contain '../'", - self, - ) - result = False - if context.fail_fast: - return result - return result - - @check( - name="Entity identifiers SHOULD use native UTF-8, not percent-encoding", - severity=Severity.RECOMMENDED, - ) - def check_utf8_identifiers(self, context: ValidationContext) -> CheckResultValue: - """ - International characters in @id values SHOULD be written in native - UTF-8 rather than percent-encoded (RO-Crate 1.3, JSON-LD appendix). - """ - result = True - try: - entities = context.ro_crate.metadata.as_dict().get("@graph", []) - except ROCrateMetadataNotFoundError: - logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - entity_id = entity.get("@id", "") - if _PCT_NON_ASCII_RE.search(entity_id): - context.result.add_issue( - f"Entity @id '{entity_id}' contains percent-encoded non-ASCII " - f"characters; international characters SHOULD be written in " - f"native UTF-8 rather than percent-encoded", - self, - ) - result = False - if context.fail_fast: - return result - return result - - @check( - name="Contextual entity @id SHOULD be absolute URI or '#'-prefixed", - severity=Severity.RECOMMENDED, - ) - def check_named_entity_id_format(self, context: ValidationContext) -> CheckResultValue: - """ - Any Contextual Entity (Person, Organization, ContactPoint, PropertyValue, - Place, etc.) SHOULD use an @id that is an absolute URI (permalink), - a '#'-prefixed local identifier, or a blank node — not a bare relative - path (RO-Crate 1.3, 5.1 Any Contextual Entity). - """ - result = True - ro_crate_metadata = context.ro_crate.metadata - non_contextual_ids = set() - with contextlib.suppress(ValueError): - non_contextual_ids.add(ro_crate_metadata.get_file_descriptor_entity().id) - with contextlib.suppress(ValueError): - non_contextual_ids.add(ro_crate_metadata.get_root_data_entity().id) - with contextlib.suppress(ValueError): - non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) - - try: - entities = ro_crate_metadata.as_dict().get("@graph", []) - except ROCrateMetadataNotFoundError: - logger.debug("Skipping named entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - entity_id = entity.get("@id", "") - if entity_id in non_contextual_ids: - continue - raw_type = entity.get("@type") - if not raw_type: - # Reference-only stub (no @type) — out of scope for this check - continue - # Absolute URIs (any scheme), fragment-prefixed IDs, and blank nodes - # are all valid; only flag bare relative paths - if entity_id.startswith(("#", "_:")) or _ABSOLUTE_URI_RE.match(entity_id): - continue - context.result.add_issue( - f"Entity @id '{entity_id}' of type '{raw_type}' is a local identifier " - f"that does not start with '#'; named local entities SHOULD use a " - f"'#'-prefixed @id (e.g. '#alice') or an absolute URI (permalink)", - self, - ) - result = False - if context.fail_fast: - return result - return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl deleted file mode 100644 index 03a6395df..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/0_entity_metadata.ttl +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix sh: . -@prefix schema: . -@prefix xsd: . - -ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; - sh:name "RO-Crate Metadata Entity: RECOMMENDED properties" ; - sh:description """Check if the RO-Crate Metadata Entity includes at least one Schema.org type and a human-readable name""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ?type . - - { SELECT DISTINCT ?root ?metadatafile WHERE { - ?root a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - FILTER(?this != ?metadatafile) - FILTER (?this != ?root) - # Exclude entities with non-IRI identifiers or those from specific namespaces - FILTER (isIRI(?this)) - FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "https://w3id.org/ro/crate/")) - FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/terms/")) - FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) - FILTER(!STRSTARTS(STR(?this), "urn:")) - } - """ ; - ] ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:name "RO-Crate Metadata Entity: RECOMMENDED type" ; - sh:description """Check if the RO-Crate Metadata Entity includes at least one Schema.org type""" ; - sh:message "RO-Crate Metadata Entity SHOULD include at least one Schema.org type" ; - sh:select """ - SELECT ?this - WHERE { - FILTER NOT EXISTS { - $this a ?schemaType . - FILTER ( - STRSTARTS(STR(?schemaType), "http://schema.org/") || - STRSTARTS(STR(?schemaType), "https://schema.org/") - ) - } - } - """ ; - ] ; - sh:property [ - sh:name "RO-Crate Metadata Entity: name" ; - sh:description """Check if the RO-Crate Metadata Entity includes a human-readable name""" ; - sh:path schema:name ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:severity sh:Warning ; - sh:message "Entities SHOULD have a human-readable name" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py deleted file mode 100644 index 4261a1ed2..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/1_file_descriptor_name.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -# set up logging -logger = logging.getLogger(__name__) - - -@requirement(name="File Descriptor naming convention") -class FileDescriptorExistence(PyFunctionCheck): - """ - If stored in a file, SHOULD be named ${prefix}-ro-crate-metadata.json, where the variable ${prefix} - is a human readable version of the dataset's ID or name. - """ - - @check(name="Detached RO-Crate file descriptor RECOMMENDED naming convention") - def test_detached_descriptor_filename(self, context: ValidationContext) -> CheckResultValue: - """ - Check if the file descriptor of a Detached RO-Crate exists and is named according to the convention. - In a Detached RO-Crate, the file descriptor SHOULD be named `{prefix}-ro-crate-metadata.json`, - where `{prefix}` is a human readable version of the dataset's ID or name. - """ - if context.settings.metadata_only: - logger.debug("Skipping file descriptor existence check in metadata-only mode") - context.record_skip(self, "metadata-only mode", "configured") - return CheckResult.SKIPPED - if not context.ro_crate.has_descriptor(): - message = f'file descriptor "{context.rel_fd_path}" is not present' - context.result.add_issue(message, self) - return False - if context.ro_crate.is_detached(): - # Check if the filename follows the convention - fd_filename = context.ro_crate.get_descriptor_path() - if fd_filename and not ( - fd_filename.name.endswith("-ro-crate-metadata.json") - or fd_filename.name.endswith("-ro-crate-metadata.yaml") - ): - context.result.add_issue( - "In a detached RO-Crate, the metadata descriptor filename " - "SHOULD be named according to the convention `{prefix}-ro-crate-metadata.json` ", - self, - ) - return False - return True diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl deleted file mode 100644 index db398b1db..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_conformsto.ttl +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix schema_org: . -@prefix dct: . -@prefix prof: . -@prefix sh: . - -# Root Data Entity: conformsTo SHOULD be present if profiles exist in the crate -ro-crate:RootDataEntityConformsToIfProfiles a sh:NodeShape ; - sh:name "Root Data Entity: conformsTo SHOULD be present if profiles exist" ; - sh:description """If the RO-Crate conforms to additional profiles, - the Root Data Entity SHOULD have a conformsTo property referencing - Profile entities (RO-Crate 1.3).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ro-crate:RootDataEntity . - FILTER EXISTS { - ?profile a prof:Profile . - FILTER(?profile != ) - } - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: RECOMMENDED conformsTo if profiles exist" ; - sh:path dct:conformsTo ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "The Root Data Entity SHOULD have a `conformsTo` property if the RO-Crate conforms to profiles" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl deleted file mode 100644 index 006076510..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_funding.ttl +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . - -ro-crate:RootDataEntityRecommendedFunder a sh:NodeShape ; - sh:name "Root Data Entity: recommended funder" ; - sh:description """If the RO-Crate describes a research project, grant, or - monetary grant (via a `schema:Project`, `schema:Grant`, or - `schema:MonetaryGrant` entity in the @graph), the Root Data Entity - SHOULD reference the funding source via the `funder` property - (RO-Crate 1.3, Funding and Grants).""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:name "Root Data Entity: RECOMMENDED `funder` property (conditional)" ; - sh:description """Fire a warning only if the crate contains project or - grant entities AND the Root Data Entity has no `funder` property.""" ; - sh:select """ - SELECT $this - WHERE { - # Trigger: at least one project/grant entity is present. - ?project a ?projectType . - FILTER(?projectType IN (schema:Project, schema:Grant, schema:MonetaryGrant)) - # Violation condition: the Root has no `funder` property. - FILTER NOT EXISTS { $this schema:funder ?f } - } - """ ; - sh:severity sh:Warning ; - sh:message "The Root Data Entity SHOULD reference funders directly via the `funder` property when the crate describes a research project, grant, or monetary grant" ; - ] . - -# funder values SHOULD be Organization entities; -# Organization funder entities SHOULD themselves reference an external funder. -ro-crate:FunderEntityRecommendedType a sh:NodeShape ; - sh:name "Funder entity: recommended Organization type" ; - sh:description """Entities referenced via `funder` from the Root Data Entity SHOULD be of - type Organization (RO-Crate 1.3, Funding and Grants — K1/K2).""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:name "Funder entity: RECOMMENDED `Organization` type" ; - sh:description """Check if funder entities referenced from the Root Data Entity - are of type Organization.""" ; - sh:path schema_org:funder ; - sh:class schema_org:Organization ; - sh:severity sh:Warning ; - sh:message "Funder entities referenced from the Root Data Entity SHOULD be of type `Organization`" ; - ] . - -# An Organization that is referenced as funder from Root SHOULD itself -# carry a `funder` property pointing to an external funding body. -ro-crate:ProjectOrganizationRecommendedFunder a sh:NodeShape ; - sh:name "Project Organization: recommended funder reference" ; - sh:description """An Organization entity referenced via `funder` from the Root Data Entity - SHOULD itself reference the external funding body via its own `funder` property - (RO-Crate 1.3, Funding and Grants — K2).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?root a ro-crate:RootDataEntity . - ?root schema:funder ?this . - ?this a schema:Organization . - FILTER(!regex(str(?this), "^https?://", "i")) - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Project Organization: RECOMMENDED `funder` property" ; - sh:description """Check if an Organization referenced as funder from the Root Data Entity - itself references an external funding body via `funder`.""" ; - sh:path schema_org:funder ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "An Organization referenced as `funder` from the Root Data Entity SHOULD itself reference an external `funder`" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py deleted file mode 100644 index 53984453d..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging -from rocrate_validator.utils.signposting import check_downloadable -from rocrate_validator.utils.uri import URI - -logger = logging.getLogger(__name__) - - -@requirement(name="Root Data Entity: RECOMMENDED identifier") -class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): - """ - In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. - """ - - @check(name="Root Data Entity: RECOMMENDED identifier") - def check_identifier(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 - """ - In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. - """ - try: - if not context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is attached", "returned") - return CheckResult.SKIPPED - root_entity = context.ro_crate.metadata.get_root_data_entity() - if not root_entity.is_remote(): - context.record_skip(self, "root data entity is not remote", "returned") - return CheckResult.SKIPPED - - if root_entity.id == "./": - context.result.add_issue( - "In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL, not `./`", self - ) - return False - if not URI(root_entity.id).is_remote_resource(): - context.result.add_issue( - "In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL", self - ) - return False - - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except (AttributeError, TypeError, ValueError) as e: - context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) - return False - - -@requirement(name="Root Data Entity: use cite-as for resolvable identifiers") -class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): - """ - If the Root Data Entity has a resolvable identifier, - it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. - """ - - @check(name="Root Data Entity: use cite-as for resolvable identifiers") - def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 - """ - If the Root Data Entity has a resolvable identifier, - it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. - """ - try: - if not context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is attached", "returned") - return CheckResult.SKIPPED - root_entity = context.ro_crate.metadata.get_root_data_entity() - if not root_entity.is_remote(): - context.record_skip(self, "root data entity is not remote", "returned") - return CheckResult.SKIPPED - - # Check if the `cite-as` property is present and references the Root Data Entity - cite_as = root_entity.get_property("cite-as") - if root_entity.id_as_uri.is_remote_resource() and not cite_as: - context.result.add_issue( - "If the Root Data Entity has a resolvable identifier, " - "it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity.", - self, - ) - return False - - # If the `cite-as` property is present, check that it references the Root Data Entity - if cite_as.id != root_entity.id: - context.result.add_issue( - "If the Root Data Entity has a resolvable identifier, " - "it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity.", - self, - ) - return False - - return True - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except (AttributeError, TypeError, ValueError) as e: - context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) - return False - - -def _extract_identifier_urls(identifier_raw) -> list[str]: - """ - Extract HTTP URL(s) from a raw ``identifier`` property value. - - The identifier can be: - - A plain string URL - - A ``schema:PropertyValue`` entity with a ``url`` or ``value`` sub-property - - A list of any of the above - """ - urls = [] - items = identifier_raw if isinstance(identifier_raw, list) else [identifier_raw] - for item in items: - if isinstance(item, str): - if item.startswith("http"): - urls.append(item) - elif hasattr(item, "get_property"): - # PropertyValue: try url first, then value - for prop in ("url", "value"): - val = item.get_property(prop) - if val: - url = val if isinstance(val, str) else val.id if hasattr(val, "id") else None - if url and url.startswith("http"): - urls.append(url) - break - elif hasattr(item, "id") and item.id and item.id.startswith("http"): - urls.append(item.id) - return urls - - -@requirement(name="Root Data Entity: persistent identifier resolution") -class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): - """ - If the Root Data Entity has an ``identifier`` property with a resolvable - HTTP URL, resolving that URL SHOULD ultimately provide the RO-Crate Metadata - Document or an archive, accessible via Signposting or content negotiation - (RO-Crate 1.3, RECOMMENDED). - """ - - @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) - def check_identifier_resolvable(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 - if context.settings.skip_availability_check: - context.record_skip(self, "availability checks are disabled", "configured") - return CheckResult.SKIPPED - if context.settings.metadata_only: - context.record_skip(self, "metadata-only mode", "configured") - return CheckResult.SKIPPED - try: - root_entity = context.ro_crate.metadata.get_root_data_entity() - identifier_raw = root_entity.get_property("identifier") - if not identifier_raw: - return True - - urls = _extract_identifier_urls(identifier_raw) - if not urls: - return True - - result = True - for url in urls: - dl = check_downloadable(url) - if not dl.is_downloadable: - msg = ( - f"The Root Data Entity identifier '{url}' SHOULD resolve to the " - f"RO-Crate Metadata Document or an archive via Signposting or " - f"content negotiation" - ) - if dl.reason: - msg += f": {dl.reason}" - context.result.add_issue(msg, self) - result = False - return result - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except (AttributeError, OSError, TypeError, ValueError) as e: - context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) - return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl deleted file mode 100644 index b5d4e3393..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_identifier.ttl +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix schema_org: . -@prefix sh: . -@prefix xsd: . - -# Root Data Entity: identifier SHOULD be present if its @id is an absolute URI -ro-crate:RootDataEntityIdentifierIfPid a sh:NodeShape ; - sh:name "Root Data Entity: identifier SHOULD be present if PID exists" ; - sh:description """If the Root Data Entity has a persistent identifier - (e.g., its @id is an absolute URI), it SHOULD include an identifier property.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ro-crate:RootDataEntity . - FILTER(regex(str(?this), "^https?://", "i")) - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: RECOMMENDED identifier if persistent ID exists" ; - sh:path schema_org:identifier ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "The Root Data Entity SHOULD have an `identifier` property if it has a persistent identifier" ; - ] . - -# Root Data Entity: identifier SHOULD use PropertyValue approach (Science On Schema.org) -# This shape only fires when an identifier IS present but is not a PropertyValue entity. -ro-crate:RootDataEntityIdentifierPropertyValueApproach a sh:NodeShape ; - sh:name "Root Data Entity: identifier SHOULD use PropertyValue approach" ; - sh:description """If the Root Data Entity has an identifier, it SHOULD use - the PropertyValue approach as per Science On Schema.org guides.""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:name "Root Data Entity: RECOMMENDED PropertyValue approach for identifier" ; - sh:description """Check that if an identifier is present it is a PropertyValue entity.""" ; - sh:select """ - SELECT ?this - WHERE { - ?this schema:identifier ?id . - FILTER NOT EXISTS { ?id a schema:PropertyValue } - } - """ ; - sh:severity sh:Warning ; - sh:message "The Root Data Entity SHOULD use PropertyValue entities for identifiers (Science On Schema.org approach)" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl deleted file mode 100644 index 1db9c7507..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_metadata.ttl +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix schema_org: . -@prefix sh: . - -ro-crate:RootDataEntityDatePublishedDayPrecision a sh:NodeShape ; - sh:name "Root Data Entity: datePublished SHOULD specify at least day precision" ; - sh:description """The Root Data Entity SHOULD specify datePublished to at least - the precision of a day (YYYY-MM-DD) per RO-Crate 1.3.""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: RECOMMENDED datePublished day precision" ; - sh:path schema_org:datePublished ; - sh:pattern "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])" ; - sh:severity sh:Warning ; - sh:message "The Root Data Entity SHOULD specify datePublished to at least the precision of a day (YYYY-MM-DD)" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl deleted file mode 100644 index 1f1f4a281..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/2_root_data_entity_publisher.ttl +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . - -ro-crate:RootDataEntityRecommendedPublisher a sh:NodeShape ; - sh:name "Root Data Entity: recommended publisher" ; - sh:description """The Root Data Entity SHOULD have a `publisher` property - referencing an Organization or Person (as per schema.org).""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:name "Root Data Entity: RECOMMENDED `publisher` property" ; - sh:description """Check if the Root Data Entity has a `publisher` property. - The RO-Crate specification recommends that the Root Data Entity includes - a publisher (Organization or Person) to describe who is responsible for - making the RO-Crate available.""" ; - sh:path schema_org:publisher ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "The Root Data Entity SHOULD have a `publisher` property (Organization or Person)" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl deleted file mode 100644 index 8ae52d716..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/3_ro_crate_preview_exclusion.ttl +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix schema_org: . -@prefix sh: . - -ro-crate:PreviewFilesNotInHasPart a sh:NodeShape ; - sh:name "Preview file descriptor should not be included in `hasPart`" ; - sh:description """`ro-crate-preview.html` and `ro-crate-preview_files/` SHOULD NOT be included in `hasPart`.""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:property [ - sh:path schema_org:hasPart ; - sh:nodeKind sh:IRI ; - sh:description "Check that `ro-crate-preview.html` and `ro-crate-preview_files/` are not included in `hasPart`" ; - sh:message "RO-Crate Website files SHOULD NOT be included in `hasPart`" ; - sh:pattern "^(?!.*(ro-crate-preview\\.html|ro-crate-preview_files/)).*$" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py deleted file mode 100644 index 234166a57..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_license.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# ================================================================== -# NOTE -# ================================================================== -# -# The RO-Crate 1.3 specification (section on Licensing) states: -# -# "Data Entities with different license SHOULD have own license property" -# (requirement L4 / P2.2) -# -# This requirement is inherently non-actionable in its strict formulation -# because there is no way for a validator to determine whether a Data -# Entity's content *should* be under a different license than the Root -# Data Entity's. The validator can only observe three states: -# -# 1. A Data Entity declares a `license` identical to the Root's. -# This is redundant (the entity already inherits the Root license) -# but not incorrect. -# -# 2. A Data Entity declares a `license` different from the Root's. -# This is the case the spec explicitly encourages. -# -# 3. A Data Entity does not declare `license`. It inherits the Root -# license, which is the default and perfectly acceptable. -# -# While this cross-entity comparison *could* be expressed as a SHACL -# shape using a SPARQL constraint, doing so would produce a validation -# failure that affects the overall validation result. The intent here -# is purely advisory: the redundant declaration is not an error, it is -# a style improvement suggestion that should not cause the crate to -# fail validation. By implementing this as a Python PyFunctionCheck -# that only logs a warning (without adding a validation issue), the -# check provides actionable feedback to authors without penalising -# valid crates. - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -logger = logging.getLogger(__name__) - - -def _resolve_license_id(license_value) -> str: - """ - Resolve a license property value to its @id string. - - The `license` property may be: - - a string (e.g. "https://creativecommons.org/licenses/by/4.0/") - - a dict with @id (e.g. {"@id": "https://creativecommons.org/licenses/by/4.0/"}) - - an ROCrateEntity object (which has an .id attribute) - - a list containing any of the above - - None - - Returns the @id string, or the string value, or empty string if - unresolvable. - """ - if license_value is None: - return "" - if isinstance(license_value, list): - items = [_resolve_license_id(item) for item in license_value] - return ",".join(item for item in items if item) - if isinstance(license_value, str): - return license_value - if hasattr(license_value, "id"): - return license_value.id - if isinstance(license_value, dict): - return license_value.get("@id", "") - return str(license_value) - - -@requirement(name="Data Entity: SHOULD NOT redundantly declare the Root license") -class DataEntityLicenseDivergenceChecker(PyFunctionCheck): - """ - Data Entities that declare a ``license`` property identical to the - Root Data Entity's license are redundantly overriding the inherited - license. The declaration is not incorrect, but it is unnecessary: - by default, all Data Entities inherit the Root Data Entity's license. - - This check logs a warning for each such redundant declaration, - suggesting that the property can be removed. It does **not** add - a validation issue, so the crate still passes validation. - - Data Entities with a *different* license from the Root are explicitly - encouraged by the spec and produce no warning. Data Entities with - no ``license`` property also produce no warning — they correctly - inherit the Root license. - """ - - @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901, PLR0911 - root_entity = None - try: - root_entity = context.ro_crate.metadata.get_root_data_entity() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity license check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except ValueError: - context.record_skip(self, "root data entity is not available", "exception") - return CheckResult.SKIPPED - if root_entity is None: - return True - - root_license_raw = root_entity.get_property("license") - if root_license_raw is None: - return True - - root_license_id = _resolve_license_id(root_license_raw) - if not root_license_id: - return True - - try: - entities = context.ro_crate.metadata.get_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity license check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - if entity.id == root_entity.id: - continue - entity_license_raw = entity.get_property("license") - if entity_license_raw is None: - continue - - entity_license_id = _resolve_license_id(entity_license_raw) - if not entity_license_id: - continue - - if entity_license_id == root_license_id: - logger.warning( - "Data Entity '%s' declares a `license` property that is " - "identical to the Root Data Entity's license ('%s'); " - "this is redundant because Data Entities inherit the Root " - "license by default. Remove the `license` property or " - "change it to a different license if the content requires " - "one.", - entity.id, - root_license_id, - ) - return True diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py deleted file mode 100644 index ca5b5e1c7..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging -from rocrate_validator.utils.signposting import check_downloadable - -# set up logging -logger = logging.getLogger(__name__) - - -@requirement(name="Data Entity: citation references") -class DataEntityCitationChecker(PyFunctionCheck): - """ - Citation references must include an absolute URI. - """ - - @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> CheckResultValue: - result = True - try: - entities = context.ro_crate.metadata.get_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - citations = entity.get_property("citation") - if citations is None: - continue - citation_list = citations if isinstance(citations, list) else [citations] - for citation in citation_list: - if isinstance(citation, str): - citation_id = citation - elif hasattr(citation, "id"): - citation_id = citation.id - else: - context.result.add_issue( - f"Citation for Data Entity '{entity.id}' must reference a publication @id", self - ) - result = False - if context.fail_fast: - return False - continue - if not re.match(r"^[A-Za-z][A-Za-z0-9+\.-]*:", citation_id): - context.result.add_issue(f"Citation for Data Entity '{entity.id}' must be an absolute URI", self) - result = False - if context.fail_fast: - return False - return result - - -@requirement(name="Web-based Data Entity: REQUIRED availability") -class WebDataEntityRequiredChecker(PyFunctionCheck): - """ - Web-based Data Entities SHOULD be directly downloadable (RO-Crate 1.3). - Downloadability is checked via Signposting, Content-Type, and content negotiation. - Entities returning an HTML page (splash page / viewer) are also flagged. - """ - - @staticmethod - def _not_downloadable_message(entity_id: str, dl) -> str: - """Build the warning message for a web-based Data Entity that is not directly downloadable.""" - if dl.reason and "HTML" in dl.reason: - return ( - f"Web-based Data Entity '{entity_id}' references an HTML page " - f"(possible splash page or viewer application) and is not " - f"directly downloadable" - ) - msg = f"Web-based Data Entity '{entity_id}' is not directly downloadable" - if dl.reason: - msg += f": {dl.reason}" - return msg - - @check(name="Web-based Data Entity: RECOMMENDED resource availability", severity=Severity.RECOMMENDED) - def check_availability_warning(self, context: ValidationContext) -> CheckResultValue: - if ( - context.settings.skip_availability_check - or context.settings.creation_time - or context.settings.enforce_availability - or context.settings.metadata_only - ): - context.record_skip(self, "availability check is disabled or not applicable", "configured") - return CheckResult.SKIPPED - result = True - try: - entities = context.ro_crate.metadata.get_web_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - assert entity.id is not None, "Entity has no @id" - if entity.id.endswith("/"): - continue - try: - dl = check_downloadable(entity.id) - if not dl.is_downloadable: - context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) - result = False - except (OSError, RuntimeError, TypeError, ValueError) as e: - context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) - result = False - if not result and context.fail_fast: - return result - return result - - @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) - def check_content_size(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 - if context.settings.skip_availability_check: - context.record_skip(self, "availability checks are disabled", "configured") - return CheckResult.SKIPPED - result = True - try: - entities = context.ro_crate.metadata.get_web_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - assert entity.id is not None, "Entity has no @id" - if entity.is_available(): - content_size = entity.get_property("contentSize") - if content_size: - if isinstance(content_size, str): - content_value = content_size - elif hasattr(content_size, "id"): - content_value = content_size.id - else: - content_value = str(content_size) - try: - content_int = int(str(content_value)) - except (TypeError, ValueError, OverflowError): - content_int = None - external_size = context.ro_crate.get_external_file_size(entity.id) - if external_size is not None and content_int is not None and content_int != external_size: - context.result.add_issue( - f"The property contentSize={content_size} of the Web-based Data Entity " - f"{entity.id} does not match the actual size of " - f"the downloadable content, i.e., {external_size} (bytes)", - self, - violatingEntity=entity.id, - violatingProperty="contentSize", - violatingPropertyValue=str(content_value), - ) - result = False - if not result and context.fail_fast: - return result - return result - - @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) - def check_content_url(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 - if context.settings.skip_availability_check: - context.record_skip(self, "availability checks are disabled", "configured") - return CheckResult.SKIPPED - result = True - try: - entities = context.ro_crate.metadata.get_web_data_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - content_url = entity.get_property("contentUrl") - if not content_url: - continue - urls = content_url if isinstance(content_url, list) else [content_url] - for url in urls: - url_value = url if isinstance(url, str) else url.id if hasattr(url, "id") else None - if not url_value or not url_value.startswith("http"): - continue - try: - dl = check_downloadable(url_value) - if not dl.is_downloadable: - entity_ref = f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}'" - msg = f"{entity_ref} is not directly downloadable" - if dl.reason: - msg += f": {dl.reason}" - context.result.add_issue(msg, self) - result = False - except (OSError, RuntimeError, TypeError, ValueError) as e: - context.result.add_issue( - f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}' " - f"availability check failed: {e}", - self, - ) - result = False - if not result and context.fail_fast: - return result - return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl deleted file mode 100644 index aecc04fe1..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_data_entity_metadata.ttl +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix dct: . -@prefix prof: . -@prefix sh: . -@prefix xsd: . - -ro-crate:FileRecommendedProperties a sh:NodeShape ; - sh:name "File Data Entity: RECOMMENDED `encodingFormat` property" ; - sh:description """A `File` Data Entity SHOULD have detailed descriptions of encodings through the `encodingFormat` property.""" ; - sh:targetClass ro-crate:File ; - sh:property [ - sh:minCount 1 ; - sh:maxCount 2 ; - sh:path schema_org:encodingFormat ; - sh:severity sh:Warning ; - sh:name "File Data Entity: RECOMMENDED `encodingFormat` property" ; - sh:description """Check if the File Data Entity has a detailed description of encodings through the `encodingFormat` property. - The `encodingFormat` property SHOULD be a PRONOM identifier (e.g., application/pdf) or, - to add more detail, SHOULD be linked using a `PRONOM` identifier to a `Contextual Entity` of type `WebPage`.""" ; - sh:message "Missing or invalid `encodingFormat` linked to the `File Data Entity`" ; - sh:or ( - [ - sh:datatype xsd:string ; - sh:pattern "^(\\w*)\\/(\\w[\\w\\.-]*)(?:\\+(\\w[\\w\\.-]*))?(?:;(\\w+=[^;]+))*$" ; - ] - [ - sh:nodeKind sh:IRI ; - ] - )] . - -ro-crate:FileContentSizeRecommendedProperties a sh:NodeShape ; - sh:name "File Data Entity: RECOMMENDED contentSize" ; - sh:description """A `File` Data Entity SHOULD have `contentSize` set to the size in bytes.""" ; - sh:targetClass ro-crate:File ; - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "File Data Entity: RECOMMENDED `contentSize` property" ; - sh:path schema_org:contentSize ; - sh:datatype xsd:string ; - sh:severity sh:Warning ; - sh:message "File Data Entities SHOULD have a `contentSize` property" ; - ] . - -ro-crate:DataEntityRecommendedProperties a sh:NodeShape ; - sh:name "Data Entity: RECOMMENDED properties" ; - sh:description """Data Entities SHOULD have `name`, `description`, `license`, `contentLocation`, `spatialCoverage`, and `citation` properties.""" ; - sh:targetClass ro-crate:DataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:name "Data Entity: RECOMMENDED `name` property" ; - sh:path schema_org:name ; - sh:minCount 1 ; - sh:nodeKind sh:Literal ; - sh:severity sh:Warning ; - sh:message "Data Entities SHOULD have a `name` property" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Data Entity: RECOMMENDED `description` property" ; - sh:path schema_org:description ; - sh:minCount 1 ; - sh:nodeKind sh:Literal ; - sh:severity sh:Warning ; - sh:message "Data Entities SHOULD have a `description` property" ; - ] ; - sh:property [ - sh:name "Data Entity: RECOMMENDED `license` reference" ; - sh:description """If present, `license` SHOULD reference a CreativeWork entity.""" ; - sh:path schema_org:license ; - sh:class schema_org:CreativeWork ; - sh:severity sh:Warning ; - sh:message "Data Entity `license` SHOULD reference a CreativeWork entity" ; - ] ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this (schema:contentLocation|schema:spatialCoverage) ?location . - FILTER NOT EXISTS { - ?location a schema:Place . - } - } - """ ; - sh:name "Data Entity: RECOMMENDED `contentLocation`/`spatialCoverage` reference" ; - sh:description """If present, `contentLocation` or `spatialCoverage` SHOULD reference a Place.""" ; - sh:message "If present, `contentLocation` or `spatialCoverage` SHOULD reference a Place" ; - sh:severity sh:Warning ; - ] ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this schema:citation ?citation . - FILTER NOT EXISTS { - ?citation a ?type . - FILTER (?type IN (schema:ScholarlyArticle, schema:CreativeWork)) - } - } - """ ; - sh:name "Data Entity: RECOMMENDED `citation` reference" ; - sh:description """If present, `citation` SHOULD reference a ScholarlyArticle or CreativeWork.""" ; - sh:message "Data Entity `citation` SHOULD reference a ScholarlyArticle or CreativeWork" ; - sh:severity sh:Warning ; - ] ; - sh:property [ - sh:name "Data Entity: RECOMMENDED `author` reference" ; - sh:description """If present, `author` SHOULD reference Person entities.""" ; - sh:path schema_org:author ; - sh:class schema_org:Person ; - sh:severity sh:Warning ; - sh:message "Data Entity `author` SHOULD reference a Person entity" ; - ] . - -# ro-crate:DirectoryDataEntityRecommendedValueRestriction a sh:NodeShape ; -# sh:name "Directory Data Entity: RECOMMENDED value restriction" ; -# sh:description """A Directory Data Entity SHOULD end with `/`""" ; -# sh:targetClass ro-crate:Directory ; -# sh:property [ -# a sh:PropertyShape ; -# sh:name "Directory Data Entity: RECOMMENDED value restriction" ; -# sh:description """Check if the Directory Data Entity ends with `/`""" ; -# sh:path [ -# sh:inversePath rdf:type -# ] ; sh:message """Every Data Entity Directory URI SHOULD end with `/`""" ; sh:pattern "/$" ; -# ] . - -# ro-crate:DatasetIdShouldEndWithSlash a sh:NodeShape ; -# sh:name "Dataset: RECOMMENDED trailing slash" ; -# sh:description "Dataset IRI SHOULD end with `/`." ; -# sh:targetClass schema_org:Dataset ; -# sh:nodeKind sh:IRI ; -# sh:pattern "/$" ; -# sh:message "Dataset IRI SHOULD end with '/'" . - -ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; - sh:name "Web-based Directory Data Entity: recommended distribution" ; - sh:description """If a non-root Dataset Data Entity has a web URI (absolute https?://), - it SHOULD include a `distribution` property pointing to a downloadable archive - (RO-Crate 1.3, Data Entities — Directories on the web).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?this a schema:Dataset . - FILTER(?this != ?root) - FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) - FILTER regex(str(?this), "^https?://", "i") - } - """ - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Web-based Directory Data Entity: RECOMMENDED `distribution` property" ; - sh:description """Check if the web-based Directory Data Entity has a `distribution` - property pointing to a downloadable archive.""" ; - sh:path schema_org:distribution ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "Web-based Directory Data Entities SHOULD include a `distribution` property pointing to a downloadable archive" ; - ] . - -ro-crate:FileConformsToProfile a sh:NodeShape ; - sh:name "File: RECOMMENDED `conformsTo` profile" ; - sh:description """If present, `conformsTo` SHOULD reference a Profile or CreativeWork entity.""" ; - sh:targetClass ro-crate:File ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this dct:conformsTo ?target . - FILTER NOT EXISTS { - ?target a schema:CreativeWork . - } - FILTER NOT EXISTS { - ?target a . - } - } - """ ; - sh:name "File: RECOMMENDED `conformsTo` profile" ; - sh:message "File `conformsTo` SHOULD reference a Profile or CreativeWork entity" ; - sh:severity sh:Warning ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl deleted file mode 100644 index dad77234f..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_dataset_data_entity.ttl +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro-crate: . -@prefix schema_org: . -@prefix sh: . - -# Selects local (relative-URI) non-root Dataset entities (directory Data Entities). -ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - ?this a schema:Dataset . - FILTER(?this != ?root) - FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) - FILTER(!regex(str(?this), "^https?://", "i")) - } - """ . - -# SHOULD: relative @id SHOULD end with '/' -ro-crate:DirectoryDataEntityTrailingSlash a sh:NodeShape ; - sh:name "Dataset Data Entity: SHOULD end with trailing slash" ; - sh:description """A Dataset Data Entity whose @id is a relative URI SHOULD end with '/' - (RO-Crate 1.3, Dataset Data Entities).""" ; - sh:target ro-crate:LocalDatasetTarget ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - FILTER(!regex(str(?this), "/$")) - } - """ ; - sh:name "Dataset Data Entity: RECOMMENDED trailing slash" ; - sh:description "Check that a local Dataset Data Entity @id ends with '/'." ; - sh:severity sh:Warning ; - sh:message "The @id of a local Dataset Data Entity SHOULD end with '/'" ; - ] . - -# SHOULD: hasPart SHOULD list contained Data Entities -ro-crate:DirectoryDataEntityHasPart a sh:NodeShape ; - sh:name "Dataset Data Entity: SHOULD have hasPart" ; - sh:description """A local Dataset Data Entity SHOULD use `hasPart` to list - the Data Entities it contains (RO-Crate 1.3, Dataset Data Entities).""" ; - sh:target ro-crate:LocalDatasetTarget ; - sh:property [ - a sh:PropertyShape ; - sh:name "Dataset Data Entity: RECOMMENDED `hasPart` property" ; - sh:description "Check that a local Dataset Data Entity lists its contents via `hasPart`." ; - sh:path schema_org:hasPart ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "Local Dataset Data Entities SHOULD list their contents via `hasPart`" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py deleted file mode 100644 index f6c67cd70..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_missing_file_local_path.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# NOTE: This check is implemented as a Python PyFunctionCheck rather than a -# SHACL shape because the core condition — "the file is not present in the -# RO-Crate payload" — requires filesystem access (i.e. checking whether a -# referenced local file actually exists on disk or inside a ZIP archive). -# SHACL cannot express conditions that depend on external state such as -# file availability. A SHACL shape could only verify that `localPath` is -# present as a property, which would produce false positives on every local -# file that *is* present and therefore has no need for `localPath`. By -# using a Python check we can combine graph inspection with filesystem -# availability checks and emit a warning only when a local file is missing -# *and* no `localPath` property is provided. -# -# Additionally, the spec states that a File entity MAY use a local identifier -# starting with "#" for files that are deliberately not present in the -# payload. In that case `localPath` SHOULD be used to indicate where the -# file can be found. This requires distinguishing "#" identifiers from -# regular relative paths, which SHACL cannot do. - -import contextlib - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging - -logger = logging.getLogger(__name__) - - -@requirement(name="Data Entity: missing file SHOULD use localPath") -class MissingFileLocalPathChecker(PyFunctionCheck): - """ - In an attached RO-Crate, when a File Data Entity references a local file - that is not present in the RO-Crate payload, the entity SHOULD declare a - ``localPath`` property to indicate where the file can be found locally. - - Additionally, a File Data Entity whose ``@id`` begins with ``#`` denotes a - deliberately absent file; in that case ``localPath`` SHOULD also be - declared to indicate where the file can be found when it exists. - (RO-Crate 1.3 specification, section on File Data Entities.) - """ - - def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: - """ - Validate a single File Data Entity. Returns False (and records an issue) - when the entity refers to an absent local file without declaring a - `localPath` property; returns True otherwise. - """ - if entity.has_local_identifier(): - local_path = entity.get_property("localPath") - if not local_path: - context.result.add_issue( - f"File Data Entity '{entity.id}' uses a local " - f"identifier (#) for a deliberately absent file, " - f"but does not declare a `localPath` property; " - f"consider adding `localPath` to indicate where " - f"the file can be found locally", - self, - ) - return False - local_path_value = ( - local_path - if isinstance(local_path, str) - else local_path.id - if hasattr(local_path, "id") - else str(local_path) - ) - logger.warning( - "File Data Entity '%s' declares localPath='%s' for a " - "deliberately absent file; the availability of this " - "path cannot be verified by the validator", - entity.id, - local_path_value, - ) - return True - if not entity.has_relative_path() or entity.is_available(): - return True - if not entity.get_property("localPath"): - context.result.add_issue( - f"File Data Entity '{entity.id}' is not present in the " - f"RO-Crate payload and does not declare a `localPath` " - f"property; consider adding `localPath` to indicate where " - f"the file can be found locally", - self, - ) - return False - return True - - @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) - def check_missing_file_local_path(self, context: ValidationContext) -> CheckResultValue: - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping missing local file check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if is_detached or context.settings.metadata_only: - context.record_skip(self, "RO-Crate is detached or metadata-only mode is active", "configured") - return CheckResult.SKIPPED - root_entity_id = None - with contextlib.suppress(ValueError): - root_entity_id = context.ro_crate.metadata.get_root_data_entity().id - result = True - try: - entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) - except ROCrateMetadataNotFoundError: - logger.debug("Skipping missing local file check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - if root_entity_id and entity.id == root_entity_id: - continue - if not entity.is_file(): - continue - if not self._check_entity_local_path(context, entity): - result = False - if context.fail_fast: - return False - return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py deleted file mode 100644 index 32db7f50b..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Network-aware refinement for Referenced RO-Crate `sdDatePublished` check. - -The structural SHACL shape `ReferencedROCrateSdDatePublishedRecommended` -already warns when a referenced RO-Crate data entity with an absolute URI -@id omits both `identifier` and `sdDatePublished`. RO-Crate 1.3 § 4.5 -relaxes that obligation when the URI declares Signposting -`Link: rel="cite-as"` (which supplies a persistent citation surrogate). -This Python check performs the network-dependent refinement. -""" - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging -from rocrate_validator.utils.signposting import has_signposting_cite_as - -logger = logging.getLogger(__name__) - - -@requirement(name="Referenced RO-Crate: Signposting cite-as refinement for sdDatePublished") -class ReferencedROCrateSignpostingCiteAsChecker(PyFunctionCheck): - """ - Network-aware refinement of the `sdDatePublished` requirement for - referenced RO-Crate data entities (RO-Crate 1.3, § 4.5). - """ - - @staticmethod - def _needs_sddatepublished_check(entity, root_id) -> bool: - """ - Return True when `entity` is a referenced RO-Crate (absolute-URI @id - conforming to RO-Crate) that declares neither `identifier` nor - `sdDatePublished`, and therefore requires the Signposting refinement. - """ - if entity.id == root_id: - return False - if not entity.id.startswith(("http://", "https://")): - return False - conforms_to = entity.get_property("conformsTo", []) - if not isinstance(conforms_to, list): - conforms_to = [conforms_to] - conforms_to_ids = [c.id if hasattr(c, "id") else str(c) for c in conforms_to] - if not any(c and c.startswith("https://w3id.org/ro/crate") for c in conforms_to_ids): - return False - return not (entity.get_property("identifier") or entity.get_property("sdDatePublished")) - - @check( - name="Referenced RO-Crate: `sdDatePublished` SHOULD be present when Signposting cite-as is not declared", - severity=Severity.RECOMMENDED, - ) - def check_sddatepublished_signposting(self, context: ValidationContext) -> CheckResultValue: - """ - For each referenced RO-Crate data entity whose @id is an absolute URI - with no declared `identifier` and no `sdDatePublished`, verify whether - the URI declares Signposting `Link: rel="cite-as"`. If cite-as is - absent the entity SHOULD include `sdDatePublished`. - """ - if ( - context.settings.skip_availability_check - or not (context.settings.creation_time or context.settings.enforce_availability) - or context.settings.metadata_only - ): - context.record_skip(self, "availability check is disabled or not applicable", "configured") - return CheckResult.SKIPPED - - result = True - try: - root = context.ro_crate.metadata.get_root_data_entity() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - except ValueError: - context.record_skip(self, "root data entity is not available", "exception") - return CheckResult.SKIPPED - - try: - entities = context.ro_crate.metadata.get_dataset_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - if not self._needs_sddatepublished_check(entity, root.id): - continue - entity_id = entity.id - - cite_as_present = has_signposting_cite_as(entity_id) - if cite_as_present is None: - logger.debug( - "Signposting probe for referenced RO-Crate '%s' failed; skipping cite-as refinement.", - entity_id, - ) - continue - if cite_as_present: - logger.debug( - "Referenced RO-Crate '%s' declares Signposting rel='cite-as'; `sdDatePublished` not required.", - entity_id, - ) - continue - - context.result.add_issue( - f"Referenced RO-Crate '{entity_id}' has no Signposting " - f'`Link: rel="cite-as"` declared; `sdDatePublished` SHOULD be ' - f"present to indicate when the absolute URI was accessed", - self, - ) - result = False - if context.fail_fast: - return result - - return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl deleted file mode 100644 index e2db267f1..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_referenced_rocrate.ttl +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix dct: . -@prefix prof: . -@prefix sh: . -@prefix xsd: . - -# --------------------------------------------------------------- -# Shared SPARQL target: Referenced RO-Crate Data Entity -# -# Selects Dataset entities that declare conformsTo pointing to an -# RO-Crate specification URI (starting with https://w3id.org/ro/crate) -# and are not the Root Data Entity. -# --------------------------------------------------------------- -ro-crate:ReferencedROCrateDataEntityTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a schema:Dataset ; - dct:conformsTo ?profile . - FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - FILTER(?this != ?root) - } - """ . - -# --------------------------------------------------------------- -# Shared SPARQL target: Referenced RO-Crate Metadata Descriptor -# -# Selects entities referenced by subjectOf of a Referenced RO-Crate -# Data Entity. -# --------------------------------------------------------------- -ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?crate a schema:Dataset ; - dct:conformsTo ?profile ; - schema:subjectOf ?this . - FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - { SELECT DISTINCT ?root WHERE { - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } } - FILTER(?crate != ?root) - } - """ . - -# These shapes validate data entities that represent referenced RO-Crates. -# A "referenced RO-Crate" is a Dataset entity whose conformsTo property -# includes an RO-Crate specification URI (starting with -# https://w3id.org/ro/crate). The RO-Crate 1.3 specification requires -# that such entities follow specific conventions regarding conformsTo, -# subjectOf, and the structure of their associated metadata descriptor. - -# --------------------------------------------------------------- -# Referenced RO-Crate data entity SHOULD include the -# version-less RO-Crate base profile URI in conformsTo -# --------------------------------------------------------------- -ro-crate:ReferencedROCrateConformsToVersionlessProfile a sh:NodeShape ; - sh:name "Referenced RO-Crate: SHOULD include version-less profile in conformsTo" ; - sh:description """A data entity that represents a referenced RO-Crate - SHOULD include the version-less RO-Crate base profile URI - (https://w3id.org/ro/crate) in its conformsTo property, to indicate - that it is an RO-Crate. Specific profile versions are also permitted - in addition to the version-less URI.""" ; - sh:target ro-crate:ReferencedROCrateDataEntityTarget ; - sh:property [ - a sh:PropertyShape ; - sh:name "Referenced RO-Crate: SHOULD include version-less conformsTo" ; - sh:description """A referenced RO-Crate data entity SHOULD include the - version-less RO-Crate base profile URI - (https://w3id.org/ro/crate) in its conformsTo property.""" ; - sh:path dct:conformsTo ; - sh:hasValue ; - sh:severity sh:Warning ; - sh:message "Referenced RO-Crate data entity SHOULD include the version-less RO-Crate base profile URI (https://w3id.org/ro/crate) in its conformsTo property" ; - ] . - -# --------------------------------------------------------------- -# Referenced RO-Crate data entity SHOULD have subjectOf -# pointing to a metadata descriptor contextual entity -# --------------------------------------------------------------- -ro-crate:ReferencedROCrateSubjectOfRecommended a sh:NodeShape ; - sh:name "Referenced RO-Crate: SHOULD have subjectOf" ; - sh:description """A data entity that represents a referenced RO-Crate - SHOULD have a subjectOf property linking to a contextual entity - representing the metadata descriptor of the referenced RO-Crate.""" ; - sh:target ro-crate:ReferencedROCrateDataEntityTarget ; - sh:property [ - a sh:PropertyShape ; - sh:name "Referenced RO-Crate: SHOULD have subjectOf" ; - sh:description """A referenced RO-Crate data entity SHOULD have a - subjectOf property linking to a contextual entity representing - the metadata descriptor of the referenced RO-Crate.""" ; - sh:path schema_org:subjectOf ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "Referenced RO-Crate data entity SHOULD have a subjectOf property linking to the referenced crate metadata descriptor" ; - ] . - -# --------------------------------------------------------------- -# Referenced RO-Crate data entity whose @id is an absolute URI -# without a declared persistent identifier SHOULD include -# `sdDatePublished` to indicate when the absolute URI was -# accessed (unless Signposting rel=cite-as is declared — that -# conditional refinement is verified separately by the Python -# check `ReferencedROCrateSignpostingCiteAsChecker`). -# (RO-Crate 1.3, § 4.5 — Determining entity identifier for a -# referenced RO-Crate, step #3.) -# --------------------------------------------------------------- -ro-crate:ReferencedROCrateSdDatePublishedRecommended a sh:NodeShape ; - sh:name "Referenced RO-Crate: SHOULD have sdDatePublished when @id is an absolute URI without persistent identifier" ; - sh:description """A data entity that represents a referenced RO-Crate - whose @id is an absolute HTTP(S) URI — and that does not declare a - persistent identifier via `identifier` — SHOULD include the - `sdDatePublished` timestamp to indicate when the URI was accessed - (RO-Crate 1.3, § 4.5). This structural check does not inspect - Signposting `Link: rel=cite-as` headers; that refinement is performed - by a separate network-aware Python check.""" ; - sh:target ro-crate:ReferencedROCrateDataEntityTarget ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT $this - WHERE { - FILTER(STRSTARTS(STR($this), "http://") || STRSTARTS(STR($this), "https://")) - FILTER NOT EXISTS { $this schema:identifier ?id } - FILTER NOT EXISTS { $this schema:sdDatePublished ?date } - } - """ ; - sh:severity sh:Warning ; - sh:message "A Referenced RO-Crate data entity with an absolute URI @id and no declared persistent identifier SHOULD include `sdDatePublished` to indicate when the URI was accessed (unless Signposting `Link: rel=cite-as` is declared on that URI)" ; - ] . - -# --------------------------------------------------------------- -# Metadata descriptor of a referenced RO-Crate -# -# All three constraints (encodingFormat, no conformsTo, no about) -# share the same SPARQL target. -# --------------------------------------------------------------- -ro-crate:ReferencedROCrateMetadataDescriptorProperties a sh:NodeShape ; - sh:name "Referenced RO-Crate metadata descriptor: recommended properties" ; - sh:description """The contextual entity representing the metadata descriptor - of a referenced RO-Crate SHOULD have encodingFormat set to - 'application/ld+json' and SHOULD NOT have conformsTo or about - properties. Profile declarations belong on the data entity, and - the about property is reserved for the crate's own metadata - descriptor.""" ; - sh:target ro-crate:ReferencedROCrateMetadataDescriptorTarget ; - sh:property [ - a sh:PropertyShape ; - sh:name "Referenced RO-Crate metadata descriptor: SHOULD have encodingFormat application/ld+json" ; - sh:description """The metadata descriptor of a referenced RO-Crate - SHOULD declare encodingFormat as 'application/ld+json'.""" ; - sh:path schema_org:encodingFormat ; - sh:hasValue "application/ld+json" ; - sh:severity sh:Warning ; - sh:message "Referenced RO-Crate metadata descriptor SHOULD have encodingFormat 'application/ld+json'" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Referenced RO-Crate metadata descriptor: SHOULD NOT have conformsTo" ; - sh:description """The metadata descriptor of a referenced RO-Crate - SHOULD NOT have a conformsTo property. Profile declarations belong - on the data entity.""" ; - sh:path dct:conformsTo ; - sh:maxCount 0 ; - sh:severity sh:Warning ; - sh:message "Referenced RO-Crate metadata descriptor SHOULD NOT have a conformsTo property" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "Referenced RO-Crate metadata descriptor: SHOULD NOT have about" ; - sh:description """The metadata descriptor of a referenced RO-Crate - SHOULD NOT have an about property. The about property belongs - on the crate's own metadata descriptor.""" ; - sh:path schema_org:about ; - sh:maxCount 0 ; - sh:severity sh:Warning ; - sh:message "Referenced RO-Crate metadata descriptor SHOULD NOT have an about property" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py deleted file mode 100644 index 23955a844..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/4_web_dataset_distribution.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from rocrate_validator.errors import ROCrateMetadataNotFoundError -from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement -from rocrate_validator.utils import log as logging -from rocrate_validator.utils.signposting import check_downloadable - -logger = logging.getLogger(__name__) - - -def _resolve_distribution_url(dist) -> str | None: - """ - Extract the download URL from a distribution value. - - The distribution property can be: - - A plain string URL - - A DataDownload entity reference (ROCrateEntity) with a ``contentUrl`` - or an ``@id`` that is itself a download URL - """ - if isinstance(dist, str): - return dist if dist.startswith("http") else None - if hasattr(dist, "get_property"): - # Prefer contentUrl, fall back to @id - content_url_raw = dist.get_property("contentUrl") - if content_url_raw: - url = ( - content_url_raw - if isinstance(content_url_raw, str) - else content_url_raw.id - if hasattr(content_url_raw, "id") - else None - ) - if url and url.startswith("http"): - return url - if hasattr(dist, "id") and dist.id and dist.id.startswith("http"): - return dist.id - return None - - -@requirement(name="Dataset: distribution downloadability") -class DatasetDistributionChecker(PyFunctionCheck): - """ - If a Dataset declares a ``distribution`` property pointing to a DataDownload - entity, the referenced archive SHOULD be directly downloadable (RO-Crate 1.3, - RECOMMENDED). Downloadability is verified via Signposting, Content-Type, and - content negotiation. - """ - - def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bool: - """Verify a single distribution value; return False (and record an issue) if not downloadable.""" - url = _resolve_distribution_url(dist) - if not url: - return True - try: - dl = check_downloadable(url) - if not dl.is_downloadable: - msg = f"The distribution '{url}' of Dataset '{entity_id}' SHOULD be downloadable" - if dl.reason: - msg += f": {dl.reason}" - context.result.add_issue(msg, self) - return False - except (OSError, RuntimeError, TypeError, ValueError) as e: - context.result.add_issue( - f"Error checking downloadability of distribution '{url}' for Dataset '{entity_id}': {e}", self - ) - return False - return True - - @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) - def check_distribution_downloadable(self, context: ValidationContext) -> CheckResultValue: - if context.settings.skip_availability_check or context.settings.metadata_only: - context.record_skip(self, "availability checks are disabled or metadata-only mode is active", "configured") - return CheckResult.SKIPPED - result = True - try: - entities = context.ro_crate.metadata.get_dataset_entities() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - for entity in entities: - distribution_raw = entity.get_property("distribution") - if not distribution_raw: - continue - distributions = distribution_raw if isinstance(distribution_raw, list) else [distribution_raw] - for dist in distributions: - if not self._check_distribution(context, entity.id, dist): - result = False - if not result and context.fail_fast: - return result - return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl deleted file mode 100644 index 8eb0eacdf..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/5_web_data_entity_metadata.ttl +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix xsd: . - -ro-crate:WebBasedDataEntityRecommendedProperties a sh:NodeShape ; - sh:name "Web-based Data Entity: RECOMMENDED properties" ; - sh:description """A Web-based Data Entity SHOULD have `contentSize` and `sdDatePublished` properties.""" ; - sh:targetClass ro-crate:WebDataEntity ; - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "Web-based Data Entity: RECOMMENDED `contentSize` property" ; - sh:description """Check if the Web-based Data Entity has a `contentSize` property""" ; - sh:path schema_org:contentSize ; - sh:datatype xsd:string ; - sh:severity sh:Warning ; - sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; - ] ; - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:name "Web-based Data Entity: RECOMMENDED `sdDatePublished` property" ; - sh:description """Check if the Web-based Data Entity has a `sdDatePublished` property to indicate when the absolute URL was accessed""" ; - sh:path schema_org:sdDatePublished ; - sh:datatype xsd:string ; - sh:pattern "^([\\+-]?\\d{4})((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))|W([0-4]\\d|5[0-2])(-?[1-7])|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)?[0-5]\\d)?|24:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)$" ; - sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property to indicate when the absolute URL was accessed""" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl deleted file mode 100644 index ea05c5f9e..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/6_contextual_entity_metadata.ttl +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema: . -@prefix sh: . -@prefix xsd: . -@prefix dct: . - -ro-crate:ContextualEntityReferences a sh:NodeShape ; - sh:name "Contextual Entity RECOMMENDED references" ; - sh:description "Contextual entities SHOULD be referenced by other entities and described in the same graph." ; - sh:targetClass ro-crate:ContextualEntity ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:name "Contextual Entity references" ; - sh:description "Check that Contextual Entities are referenced by other entities." ; - sh:message "Contextual entities SHOULD be referenced by other entities." ; - sh:select """ - SELECT ?this - WHERE { - FILTER NOT EXISTS { - ?other ?pr ?this . - FILTER (?other != ?this) - } - } - """ ; - ] . - -ro-crate:ContextualEntityProperties a sh:NodeShape ; - sh:name "Contextual Entity Properties" ; - sh:description """Contextual Entities MUST have a `name` property and SHOULD have a `description` property.""" ; - sh:targetClass ro-crate:ContextualEntity ; - sh:property [ - sh:path schema:name ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:name "Contextual Entity: REQUIRED `name` property" ; - sh:description "Check if the Contextual Entity has a `name` property" ; - sh:message "A Contextual Entity MUST have a `name` property" ; - ] ; - sh:property [ - sh:path schema:description ; - sh:dataType xsd:string ; - sh:name "Contextual Entity: OPTIONAL `description` property" ; - sh:description "Check if the Contextual Entity has a `description` property" ; - sh:message "A Contextual Entity SHOULD have a `description` property" ; - ] . - -ro-crate:ReferencedContextualEntitiesShouldBeDescribed a sh:NodeShape ; - sh:name "Contextual Entity RECOMMENDED description" ; - sh:description """Contextual entities referenced by other entities SHOULD be described in the same graph.""" ; - sh:targetClass ro-crate:ContextualEntity ; - sh:property [ - sh:name "Contextual Entity: REQUIRED RDF type" ; - sh:description "Check if the Contextual Entity is described in the same graph with at least an RDF type specified." ; - sh:message "Contextual entities that are referenced by other entities SHOULD be described in the same @graph, with at least an RDF type specified." ; - # One rdf:type triple is automatically added to the data graph for each Contextual Entity - # by the `ContextualEntityDefinition` shape, so this constraint checks - # that at least that triple is present in the graph for the Contextual Entity. - sh:path rdf:type ; - sh:minCount 2 ; - ] . - -# ro-crate:EntityNameRecommendedProperties a sh:NodeShape ; -# sh:name "Entity: RECOMMENDED name" ; -# sh:description """Entities SHOULD have a human-readable `name`, in particular if its `@id` does not go to a human-readable Web page. -# This follows the RO-Crate 1.3 Common Principles for RO-Crate entities.""" ; -# sh:targetClass ro-crate:ContextualEntity ; -# sh:property [ -# sh:path schema:name ; -# sh:minCount 1 ; -# sh:datatype xsd:string ; -# sh:severity sh:Warning ; -# sh:name "Contextual Entity: RECOMMENDED `name` property" ; -# sh:description "Check if the Contextual Entity has a `name` property" ; -# sh:message "A Contextual Entity SHOULD have a `name` property" ; -# ] . - -ro-crate:CreativeWorkAuthorMinimumRecommendedProperties a sh:NodeShape ; - sh:name "CreativeWork Author: minimum RECOMMENDED properties" ; - sh:description """The minimum recommended properties for a `CreativeWork Author` are `name` and `affiliation`.""" ; - sh:targetClass ro-crate:CreativeWorkAuthor ; - sh:property [ - sh:path schema:name ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:name "CreativeWork Author: RECOMMENDED name property" ; - sh:description "Check if the author has a name." ; - sh:message "The author SHOULD have a name." ; - ] ; - sh:property [ - sh:path schema:affiliation ; - sh:minCount 1 ; - sh:or ( - [ - sh:dataType xsd:string ; - ] - [ - sh:class schema:Organization ; - ] - ) ; - sh:severity sh:Warning ; - sh:name "CreativeWork Author: RECOMMENDED affiliation property" ; - sh:description "Check if the author has an organizational affiliation." ; - sh:message "The author SHOULD have an organizational affiliation." ; - ] ; - sh:property [ - sh:path schema:affiliation ; - sh:minCount 1 ; - sh:class schema:Organization ; - sh:severity sh:Warning ; - sh:name "CreativeWork Author: RECOMMENDED Contextual Entity linked for the organizational `affiliation` property" ; - sh:description "Check if the author has a Contextual Entity for the organizational `affiliation` property." ; - sh:message "The author SHOULD have a Contextual Entity which specifies the organizational `affiliation`." ; - ] . - -ro-crate:OrganizationRecommendedProperties a sh:NodeShape ; - sh:name "Organization: RECOMMENDED properties" ; - sh:description """The recommended properties for an `Organization` are `name` and `url`.""" ; - sh:targetClass schema:Organization ; - sh:property [ - sh:path schema:name ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:name "Organization: RECOMMENDED name property" ; - sh:description "Check if the `organization` has a name." ; - sh:message "The organization SHOULD have a name." ; - ] ; - sh:property [ - sh:path schema:url ; - sh:minCount 1 ; - sh:dataType xsd:anyURI ; - sh:name "Organization: RECOMMENDED url property" ; - sh:description "Check if the `organization` has a URL." ; - sh:message "The organization SHOULD have a URL." ; - ] . - -ro-crate:PersonAffiliationRecommendedProperties a sh:NodeShape ; - sh:name "Person: RECOMMENDED affiliation" ; - sh:description """Persons SHOULD reference an Organization for affiliation.""" ; - sh:targetClass schema:Person ; - sh:property [ - sh:path schema:affiliation ; - sh:class schema:Organization ; - sh:severity sh:Warning ; - sh:message "Persons SHOULD reference an Organization for affiliation" ; - ] . - -ro-crate:LicenseEntityRecommendedProperties a sh:NodeShape ; - sh:name "License entity: RECOMMENDED properties" ; - sh:description """License entities SHOULD have `name` and `description`.""" ; - sh:targetObjectsOf schema:license ; - sh:property [ - sh:path schema:name ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:severity sh:Warning ; - sh:message "License entities SHOULD have a name" ; - ] ; - sh:property [ - sh:path schema:description ; - sh:minCount 1 ; - sh:dataType xsd:string ; - sh:severity sh:Warning ; - sh:message "License entities SHOULD have a description" ; - ] . - -ro-crate:PlaceGeoRecommendedProperties a sh:NodeShape ; - sh:name "Place: RECOMMENDED geo" ; - sh:description """Places SHOULD reference Geometry via `geo` when geometry is provided.""" ; - sh:targetClass schema:Place ; - sh:property [ - sh:path schema:geo ; - sh:class ; - sh:severity sh:Warning ; - sh:message "Places SHOULD reference a Geometry entity via `geo`" ; - ] . - -ro-crate:GeometryWktRecommendedProperties a sh:NodeShape ; - sh:name "Geometry: RECOMMENDED asWKT" ; - sh:description """Geometry entities SHOULD provide `asWKT`.""" ; - sh:targetClass ; - sh:property [ - sh:path ; - sh:minCount 1 ; - sh:severity sh:Warning ; - sh:message "Geometry entities SHOULD provide `asWKT`" ; - ] . - -ro-crate:LicenseEntityAbsoluteUrl a sh:NodeShape ; - sh:name "License entity: SHOULD have absolute URL @id" ; - sh:description """A License entity SHOULD be identified by an absolute HTTP(S) URL, - for example an SPDX license URI (https://spdx.org/licenses/). - (RO-Crate 1.3, Contextual Entities — License)""" ; - sh:targetClass ro-crate:License ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - FILTER(!regex(str(?this), "^https?://", "i")) - } - """ ; - sh:severity sh:Warning ; - sh:message "A License entity SHOULD have an absolute HTTP(S) URL as its @id (e.g., an SPDX license URI such as https://spdx.org/licenses/MIT.html)" ; - ] . - -ro-crate:LicenseEntityCreativeWorkType a sh:NodeShape ; - sh:name "License entity: SHOULD be typed as CreativeWork" ; - sh:description """A License entity SHOULD include `schema:CreativeWork` in its `@type`. - (RO-Crate 1.3, Contextual Entities — License)""" ; - sh:targetClass ro-crate:License ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - FILTER NOT EXISTS { - ?this a schema:CreativeWork . - } - } - """ ; - sh:severity sh:Warning ; - sh:message "A License entity SHOULD have `CreativeWork` in its `@type`" ; - ] . - -ro-crate:EncodingFormatRecommendedTypes a sh:NodeShape ; - sh:name "Encoding format: RECOMMENDED types" ; - sh:description """Encoding format contextual entities SHOULD include WebPage and/or Standard types.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?s schema:encodingFormat ?this . - FILTER(isIRI(?this)) - } - """ - ] ; - sh:property [ - sh:path rdf:type ; - sh:or ( - [ - sh:hasValue schema:WebPage - ] - [ - sh:hasValue dct:Standard - ] - ) ; - sh:severity sh:Warning ; - sh:message "Encoding format entities SHOULD include WebPage and/or Standard types" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl deleted file mode 100644 index b62683f8f..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/6_organization_metadata.ttl +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -@prefix ro: <./> . -@prefix ro-crate: . -@prefix rdf: . -@prefix schema: . -@prefix sh: . -@prefix xsd: . - -ro-crate:OrganizationRorIdentifier a sh:NodeShape ; - sh:name "Organization: SHOULD have ROR identifier" ; - sh:description """An Organization entity SHOULD use a ROR (Research Organization Registry) identifier - as its @id, if possible. - (RO-Crate 1.3, Contextual Entities — Organizations as values; Metadata — Recommended Identifiers)""" ; - sh:targetClass schema:Organization ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - FILTER(!STRSTARTS(STR(?this), "https://ror.org/")) - } - """ ; - sh:severity sh:Warning ; - sh:message "An Organization entity SHOULD have a ROR identifier as its @id (e.g., https://ror.org/03f0f6041)" ; - ] . - -ro-crate:OrganizationContactPoint a sh:NodeShape ; - sh:name "Organization: SHOULD have contactPoint referencing ContactPoint" ; - sh:description """An Organization SHOULD have a contactPoint property referencing a ContactPoint contextual entity. - (RO-Crate 1.3, Contextual Entities — Contact Information)""" ; - sh:targetClass schema:Organization ; - sh:property [ - sh:path schema:contactPoint ; - sh:minCount 1 ; - sh:class schema:ContactPoint ; - sh:severity sh:Warning ; - sh:name "Organization: RECOMMENDED contactPoint references ContactPoint entity" ; - sh:description "Check if the Organization has a contactPoint referencing a ContactPoint entity." ; - sh:message "An Organization's contactPoint SHOULD reference a ContactPoint contextual entity" ; - ] . - -ro-crate:PersonContactPoint a sh:NodeShape ; - sh:name "Person: SHOULD have contactPoint referencing ContactPoint" ; - sh:description """A Person's contactPoint property SHOULD reference a ContactPoint contextual entity. - (RO-Crate 1.3, Contextual Entities — Contact Information)""" ; - sh:targetClass schema:Person ; - sh:property [ - sh:path schema:contactPoint ; - sh:class schema:ContactPoint ; - sh:severity sh:Warning ; - sh:name "Person: RECOMMENDED contactPoint references ContactPoint entity" ; - sh:description "Check if the Person's contactPoint references a ContactPoint entity." ; - sh:message "A Person's contactPoint SHOULD reference a ContactPoint contextual entity" ; - ] . - -ro-crate:AuthorPublisherContactPoint a sh:NodeShape ; - sh:name "Root Data Entity: at least one author/publisher SHOULD have contactPoint" ; - sh:description """At least one of the Person or Organization entities referenced as author or publisher - from the Root Data Entity SHOULD have a contactPoint property. - (RO-Crate 1.3, Contextual Entities — Contact Information)""" ; - sh:targetClass ro-crate:RootDataEntity ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { - ?this schema:author ?author . - } UNION { - ?this schema:publisher ?publisher . - BIND(?publisher AS ?author) - } - FILTER NOT EXISTS { - { - ?this schema:author ?a . - } UNION { - ?this schema:publisher ?a . - } - ?a schema:contactPoint ?cp . - } - } - """ ; - sh:severity sh:Warning ; - sh:message "At least one author or publisher Person/Organization SHOULD have a contactPoint property" ; - ] . - -ro-crate:PersonOrcidIdentifier a sh:NodeShape ; - sh:name "Person: SHOULD have ORCID identifier" ; - sh:description """A Person entity SHOULD use an ORCID identifier as its @id. - (RO-Crate 1.3, Contextual Entities — People)""" ; - sh:targetClass schema:Person ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - FILTER(!STRSTARTS(STR(?this), "https://orcid.org/")) - } - """ ; - sh:severity sh:Warning ; - sh:message "A Person entity SHOULD have an ORCID identifier as its @id (e.g., https://orcid.org/0000-0001-2345-6789)" ; - ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl deleted file mode 100644 index fce00879e..000000000 --- a/rocrate_validator/profiles/ro-crate/1.3/should/9_schema_org_id_compatibility.ttl +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2024-2026 CRS4 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -@prefix ro-crate: . -@prefix sh: . - -ro-crate:SchemaOrgIdCompatibility a sh:NodeShape ; - sh:name "Schema.org @id compatibility" ; - sh:description "Nested Schema.org @id references SHOULD use the http://schema.org/ namespace for compatibility with the RO-Crate context." ; - # OWL-RL adds reflexive owl:sameAs triples for IRIs; these are not nested - # @id references and must not produce compatibility recommendations. - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT DISTINCT ?this - WHERE { - ?this ?predicate ?schemaId . - FILTER(?predicate NOT IN ( - , - - )) - FILTER(isIRI(?schemaId)) - FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) - } - """ - ] ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT $this ?predicate ?schemaId ?term ?httpId - WHERE { - $this ?predicate ?schemaId . - FILTER(?predicate NOT IN ( - , - - )) - FILTER(isIRI(?schemaId)) - FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) - BIND(SUBSTR(STR(?schemaId), STRLEN("https://schema.org/") + 1) AS ?term) - BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpId) - } - """ ; - sh:message "Entity {?this} references the Schema.org IRI '{?schemaId}' through a nested @id. For compatibility with the RO-Crate @context, the reference SHOULD use '{?term}' (i.e. '{?httpId}')" ; - sh:severity sh:Warning - ] . From 9dd09229f1e4e1e385dbc95e5cf7d3322f393cc2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 18:52:56 +0200 Subject: [PATCH 131/271] =?UTF-8?q?refactor(profile):=20=F0=9F=A7=A9=20dec?= =?UTF-8?q?lare=20RO-Crate=201.3=20rule=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/profiles/ro-crate/1.3/ontology.ttl | 8 ++++++++ rocrate_validator/profiles/ro-crate/1.3/profile.ttl | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl index 1e12c9fcc..9de529152 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl @@ -20,12 +20,20 @@ @prefix rdfs: . @prefix schema: . @prefix ro-crate: . +@prefix ro-crate-1.2: . @prefix bioschemas: . # @base <./.> . rdf:type owl:Ontology ; owl:versionIRI . +# The descriptor discovery rule is inherited from the 1.2 overlay source and +# assigns its internal marker class. Both marker IRIs denote the same +# validator concept, so OWL inference makes the inherited marker available to +# shapes written against the native 1.3 vocabulary. +ro-crate:ROCrateMetadataFileDescriptor + owl:equivalentClass ro-crate-1.2:ROCrateMetadataFileDescriptor . + # # ################################################################# # # # Classes # # ################################################################# diff --git a/rocrate_validator/profiles/ro-crate/1.3/profile.ttl b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl index 85b32a263..30579e6bb 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/profile.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl @@ -16,6 +16,7 @@ @prefix prof: . @prefix role: . @prefix rdfs: . +@prefix validator: . @@ -33,6 +34,11 @@ # regular metadata, URI of publisher dct:publisher ; + # Reuse the RO-Crate 1.2 validation rules and override only the rules whose + # behavior changes because of the RO-Crate 1.3 JSON-LD context. + prof:isProfileOf ; + validator:isRuleOverlayOf ; + # this profile has a JSON-LD context resource prof:hasResource [ a prof:ResourceDescriptor ; From 2316a430f9c4393597af794754c20bdb0c27b086 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 24 Sep 2026 17:53:38 +0200 Subject: [PATCH 132/271] =?UTF-8?q?refactor(cli):=20=F0=9F=8E=A8=20update?= =?UTF-8?q?=20color=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/profiles.py | 18 +++++++++--------- rocrate_validator/cli/commands/validate.py | 4 ++-- .../utils/io_helpers/output/text/formatters.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index d71bb9bd9..29c577ec6 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -194,7 +194,7 @@ def check_profile(ctx, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, no_ header_style="bold cyan", border_style="bright_black", ) - table.add_column("Profile", style="hot_pink bold") + table.add_column("Profile", style="magenta bold") table.add_column("Check", style="cyan") table.add_column("Status", justify="center") table.add_column("Message") @@ -281,7 +281,7 @@ def describe_profile( return # Set the subheader title - subheader_title = f"[bold][cyan]Profile:[/cyan] [hot_pink italic]{profile.identifier}[/hot_pink italic][/bold]" + subheader_title = f"[bold][cyan]Profile:[/cyan] [italic magenta]{profile.identifier}[/italic magenta][/bold]" # Set the subheader content subheader_content = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" @@ -450,7 +450,7 @@ def __verbose_describe_profile__(profile): table.add_column("Effective ID (†)", style="cyan bold", justify="right") table.add_column("Name", style="hot_pink bold", justify="left") table.add_column("Relation", justify="left") - table.add_column("Source ID (†)", style="hot_pink italic", justify="right") + table.add_column("Source ID (†)", style="magenta italic", justify="right") table.add_column("Description", style="white italic") table.add_column("Severity (*)", style="bold", justify="center") @@ -512,10 +512,10 @@ def __format_check_relation__(effective_check: EffectiveRequirementCheck) -> str if effective_check.relation == RequirementCheckRelation.INHERITED: return ( f"[bold cyan]Inherited[/bold cyan] from " - f"[italic hot_pink]{effective_check.source_profile.identifier}[/italic hot_pink]" + f"[italic magenta]{effective_check.source_profile.identifier}[/italic magenta]" ) replaced = ", ".join(check.identifier for check in effective_check.replaces) - return f"[bold orange1]Replaces[/bold orange1] [italic hot_pink]{replaced}[/italic hot_pink]" + return f"[bold orange1]Replaces[/bold orange1] [italic magenta]{replaced}[/italic magenta]" def __describe_check__( @@ -530,7 +530,7 @@ def __describe_check__( header = ( f"[bold cyan]Profile:[/bold cyan] " - f"[italic hot_pink]{profile.identifier}[/italic hot_pink]\n" + f"[italic magenta]{profile.identifier}[/italic magenta]\n" f"[bold cyan]Effective ID:[/bold cyan] " f"[italic green]{effective_check.identifier}[/italic green]\n" f"[bold cyan]Name:[/bold cyan] [italic hot_pink]{check.name}[/italic hot_pink]\n" @@ -542,7 +542,7 @@ def __describe_check__( if requirement.path: header += f"\n[bold cyan]Source file:[/bold cyan] [italic green]{shorten_path(requirement.path)}[/italic green]" - title = f"[bold][cyan]Check:[/cyan] [hot_pink italic]{effective_check.identifier}[/hot_pink italic][/bold]" + title = f"[bold][cyan]Check:[/cyan] [magenta italic]{effective_check.identifier}[/magenta italic][/bold]" console.print( Padding( Panel(header, title=title, padding=(1, 1, 1, 1), title_align="left", border_style="cyan"), @@ -562,9 +562,9 @@ def __describe_check__( provenance = ( f"[bold cyan]Relation:[/bold cyan] {__format_check_relation__(effective_check)}\n" f"[bold cyan]Effective ID:[/bold cyan] [italic cyan]{effective_check.identifier}[/italic cyan]\n" - f"[bold cyan]Source ID:[/bold cyan] [italic hot_pink]{effective_check.source_identifier}[/italic hot_pink]\n" + f"[bold cyan]Source ID:[/bold cyan] [italic magenta]{effective_check.source_identifier}[/italic magenta]\n" f"[bold cyan]Source profile:[/bold cyan] " - f"[italic hot_pink]{effective_check.source_profile.identifier}[/italic hot_pink]" + f"[italic magenta]{effective_check.source_profile.identifier}[/italic magenta]" ) console.print( Padding( diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 0b9b95246..fc3178381 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -386,7 +386,7 @@ def validate( results = {} for profile in profile_identifiers: # Duplicate settings for each profile and set the profile identifier - logger.info("\nValidating RO-Crate against profile: [bold cyan]%s[/bold cyan]", profile) + logger.info("\nValidating RO-Crate against profile: [bold magenta]%s[/bold magenta]", profile) profile_settings = validation_settings.copy() profile_settings["profile_identifier"] = profile logger.debug("Profile selected for validation: %s", profile) @@ -683,7 +683,7 @@ def _emit_json_report( console.print( f"\n{' ' * 2}✅ [bold]Validation [green]PASSED![/green]. " f"\n{' ' * 5}RO-Crate is valid according to the profile(s): " - f"[cyan]{', '.join(profile_identifiers)}[/cyan][/bold]" + f"[magenta]{', '.join(profile_identifiers)}[/magenta][/bold]" ) else: console.print(f"\n{' ' * 2}❌ [bold]Validation [red]FAILED![/red][/bold]") diff --git a/rocrate_validator/utils/io_helpers/output/text/formatters.py b/rocrate_validator/utils/io_helpers/output/text/formatters.py index ce753b75b..748409370 100644 --- a/rocrate_validator/utils/io_helpers/output/text/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/text/formatters.py @@ -61,7 +61,7 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR issue_color = get_severity_color(check.level.severity) yield Padding( f"[bold][{issue_color}][ {check.identifier.center(16)} ][/{issue_color}] " - f"[magenta]{check.name}[/magenta][/bold]:", + f"[hot_pink]{check.name}[/hot_pink][/bold]:", (1, 8, 0, 8), ) yield Padding(Markdown(check.description), (0, 0, 0, len(check.identifier) + 13)) From 09a9c3a9c6f5cdca9251a702011bdaab0be186c1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 16:11:09 +0200 Subject: [PATCH 133/271] =?UTF-8?q?test(profile):=20=E2=9C=85=20cover=20ov?= =?UTF-8?q?erlay=20dependency=20closure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/test_load_requirements.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index 7f7c46d36..70f72a267 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -160,28 +160,34 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): assert requirement_check.severity == Severity.RECOMMENDED, "The severity of the requirement check is incorrect" -def test_check_dependencies_are_ordered_and_closed(profiles_path: str): +def test_check_dependencies_are_ordered_and_closed(profiles_path: str) -> None: + """Resolve the 1.3 context check dependencies through its 1.2 overlay source.""" profiles = Profile.load_profiles(profiles_path=profiles_path, severity=Severity.REQUIRED) profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.3") + source_profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.2") + + source_requirements = {requirement.name: requirement for requirement in source_profile.requirements} + existence = source_requirements["File Descriptor existence"] + encoding = source_requirements["File Descriptor UTF-8 encoding"] + json_format = source_requirements["File Descriptor JSON format"] + context = next( + requirement for requirement in profile.requirements if requirement.name == "File Descriptor JSON-LD format" + ) - requirements = {requirement.name: requirement for requirement in profile.requirements} - existence = requirements["File Descriptor existence"] - encoding = requirements["File Descriptor UTF-8 encoding"] - json_format = requirements["File Descriptor JSON format"] - - ordered_names = [requirement.name for requirement in profile.requirements] + ordered_names = [requirement.name for requirement in source_profile.requirements] assert ordered_names.index(existence.name) < ordered_names.index(encoding.name) assert ordered_names.index(encoding.name) < ordered_names.index(json_format.name) - closure = RequirementLoader.dependency_closure([json_format]) + closure = RequirementLoader.dependency_closure([context]) assert {requirement.name for requirement in closure} == { existence.name, encoding.name, json_format.name, + context.name, } with pytest.raises(CheckDependencyError, match="was not selected"): - RequirementLoader.dependency_closure([json_format], include_dependencies=False) + RequirementLoader.dependency_closure([context], include_dependencies=False) def test_hidden_requirements(profiles_loading_hidden_requirements: str): From 1aeb95a1578219d8dff814eaf342519a30593931 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 16:11:19 +0200 Subject: [PATCH 134/271] =?UTF-8?q?test(profile):=20=E2=9C=85=20cover=20in?= =?UTF-8?q?herited=20marker=20namespace=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/test_profile_namespaces.py | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/tests/unit/requirements/test_profile_namespaces.py b/tests/unit/requirements/test_profile_namespaces.py index ead3d3af6..94d832396 100644 --- a/tests/unit/requirements/test_profile_namespaces.py +++ b/tests/unit/requirements/test_profile_namespaces.py @@ -55,6 +55,14 @@ def _collect_profile_namespaces(graph: Graph) -> set[str]: return {namespace for namespace in declared_namespaces if any(term.startswith(namespace) for term in terms)} +def _profile_vocabulary_namespace(graph: Graph) -> str: + """Return the namespace explicitly bound as the artifact's own vocabulary.""" + try: + return str(next(namespace for prefix, namespace in graph.namespaces() if prefix == "ro-crate")) + except StopIteration as exc: + raise AssertionError("The graph must bind its profile vocabulary to the `ro-crate` prefix") from exc + + def _parse_profile_artifacts(profile_path: Path, *, exclude: set[Path] | None = None) -> Graph: graph = Graph() excluded_paths = exclude or set() @@ -65,30 +73,29 @@ def _parse_profile_artifacts(profile_path: Path, *, exclude: set[Path] | None = @pytest.mark.parametrize("profile_path", _profile_paths_with("ontology.ttl"), ids=_profile_id) -def test_profile_ontology_uses_the_namespace_of_its_shapes(profile_path: Path): +def test_profile_ontology_uses_the_namespace_of_its_shapes(profile_path: Path) -> None: """ - Every profile vocabulary namespace declared by an ontology must also occur - in that profile's SHACL artifacts. + The ontology's own profile vocabulary must also occur in its SHACL artifacts. This is the general invariant behind issue 193: if the ontology and shapes use different namespaces, a derived profile can target an ontology class - that the base shapes never emit or target. + that the base shapes never emit or target. External profile namespaces used + only by bridge axioms do not become part of the local vocabulary. """ ontology_path = profile_path / "ontology.ttl" ontology = _parse_graph(ontology_path) artifacts = _parse_profile_artifacts(profile_path, exclude={ontology_path}) - ontology_namespaces = _collect_profile_namespaces(ontology) + ontology_namespace = _profile_vocabulary_namespace(ontology) artifact_namespaces = _collect_profile_namespaces(artifacts) - assert ontology_namespaces, f"{ontology_path} must use at least one profile namespace" - assert ontology_namespaces <= artifact_namespaces, ( - f"{ontology_path} uses namespaces not used by the profile's shapes: {ontology_namespaces - artifact_namespaces}" + assert ontology_namespace in artifact_namespaces, ( + f"{ontology_path} uses the profile vocabulary {ontology_namespace}, but the profile's shapes do not" ) @pytest.mark.parametrize("profile_path", _profile_paths_with("ontology.ttl", "prefixes.ttl"), ids=_profile_id) -def test_sparql_prefixes_use_the_profile_ontology_namespace(profile_path: Path): +def test_sparql_prefixes_use_the_profile_ontology_namespace(profile_path: Path) -> None: """ The SPARQL prefix registry must use the ontology's vocabulary namespace. @@ -98,15 +105,28 @@ def test_sparql_prefixes_use_the_profile_ontology_namespace(profile_path: Path): ontology = _parse_graph(profile_path / "ontology.ttl") prefixes = _parse_graph(profile_path / "prefixes.ttl") - ontology_namespaces = _collect_profile_namespaces(ontology) + ontology_namespace = _profile_vocabulary_namespace(ontology) prefix_namespaces = _collect_profile_namespaces(prefixes) - assert prefix_namespaces == ontology_namespaces, ( - f"{profile_path} prefixes must use the ontology namespaces: " - f"expected {ontology_namespaces}, got {prefix_namespaces}" + assert prefix_namespaces == {ontology_namespace}, ( + f"{profile_path} prefixes must use the ontology's own namespace: " + f"expected {ontology_namespace}, got {prefix_namespaces}" ) +def test_ro_crate_1_3_maps_the_inherited_descriptor_marker() -> None: + """The native 1.3 target class must match the marker emitted by inherited 1.2 discovery.""" + ontology = _parse_graph(PROFILES_PATH / "ro-crate/1.3/ontology.ttl") + ro_crate_1_2 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/") + ro_crate_1_3 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/") + + assert ( + ro_crate_1_3.ROCrateMetadataFileDescriptor, + OWL.equivalentClass, + ro_crate_1_2.ROCrateMetadataFileDescriptor, + ) in ontology + + @pytest.mark.parametrize( "profile_path", _profile_paths_with("ontology.ttl", "must/2_root_data_entity_metadata.ttl"), From 4fc8c06951c96ab8078bb044df177d7d86930e57 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 26 Sep 2026 16:11:32 +0200 Subject: [PATCH 135/271] =?UTF-8?q?test(profile):=20=E2=9C=85=20isolate=20?= =?UTF-8?q?default=20profile=20context=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 5 ++-- tests/unit/test_default_profile.py | 37 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index b3db37ee2..08eab49ce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -361,11 +361,12 @@ def test_profiles_list(cli_runner: CliRunner): assert "ro-crate-1.1" in result.output # Check for a known profile -def test_profiles_check(cli_runner: CliRunner): +def test_profiles_check(cli_runner: CliRunner) -> None: + """Resolve the bare RO-Crate token to the latest profile in check reports.""" result = cli_runner.invoke(cli, ["profiles", "check", "ro-crate", "--no-paging"]) assert result.exit_code == 0 - assert "Profile checks: ro-crate-1.2" in result.output + assert "Profile checks: ro-crate-1.3" in result.output assert "unique-requirement-check-identity" in result.output assert "rule-overlay-consistency" in result.output assert "PASS" in result.output diff --git a/tests/unit/test_default_profile.py b/tests/unit/test_default_profile.py index 86e91a956..a6907835d 100644 --- a/tests/unit/test_default_profile.py +++ b/tests/unit/test_default_profile.py @@ -18,10 +18,16 @@ """ import logging +from http import HTTPStatus +from types import SimpleNamespace + +import pytest +from rdflib import Graph from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import URI, Profile, Severity, ValidationSettings +from rocrate_validator.models import URI, Profile, Severity, ValidationContext, ValidationSettings from rocrate_validator.services import get_profile, get_profiles, validate +from rocrate_validator.utils.http import HttpRequester from tests.ro_crates import ValidROC from tests.ro_crates_v1_3 import ValidROCrate13 from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER @@ -31,6 +37,18 @@ EXPECTED_DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.3" +@pytest.fixture(autouse=True) +def local_jsonld_context(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep default-profile unit tests independent from remote contexts.""" + response = SimpleNamespace( + status_code=HTTPStatus.OK, + headers={"Content-Type": "application/ld+json"}, + json=lambda: {"@context": {}}, + ) + monkeypatch.setattr(HttpRequester, "get", lambda self, url, **kwargs: response, raising=False) + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + + def test_default_profile_identifier_is_ro_crate_1_3(): """The base RO-Crate profile used by default is 1.3.""" assert DEFAULT_PROFILE_IDENTIFIER == EXPECTED_DEFAULT_PROFILE_IDENTIFIER @@ -82,17 +100,18 @@ def test_version_sort_key_orders_versions_numerically(): assert Profile.version_sort_key(None) < Profile.version_sort_key("0.1") -def test_default_profile_validates_a_1_3_crate(): +def test_default_profile_validates_a_1_3_crate() -> None: """A 1.3 crate validates against the default profile without pinning it.""" settings = ValidationSettings( rocrate_uri=URI(str(ValidROCrate13().attached)), requirement_severity=Severity.REQUIRED, + skip_checks=["ro-crate-1.3_4.1"], ) result = validate(settings) assert result.passed(), "A valid RO-Crate 1.3 should pass under the default profile" -def test_default_profile_rejects_a_1_1_context(): +def test_default_profile_rejects_a_1_1_context() -> None: """ A 1.1-era crate no longer passes under the default profile. @@ -102,14 +121,20 @@ def test_default_profile_rejects_a_1_1_context(): settings = ValidationSettings( rocrate_uri=URI(str(ValidROC().wrroc_paper)), requirement_severity=Severity.REQUIRED, + skip_checks=["ro-crate-1.3_4.1"], ) result = validate(settings) assert not result.passed() - failed_check_ids = {issue.check.identifier for issue in result.get_issues()} + failed_check_ids = {issue.to_dict()["check"]["identifier"] for issue in result.get_issues()} assert "ro-crate-1.3_4.2" in failed_check_ids, ( f"Expected the 1.3 context check to fail, got: {sorted(failed_check_ids)}" ) - # ...and it still passes when explicitly pinned to 1.1 + # ...and the same context is accepted when explicitly pinned to 1.1. This + # assertion deliberately ignores unrelated SHACL results because this unit + # test replaces the data graph to avoid resolving remote JSON-LD contexts. settings.profile_identifier = RO_CRATE_1_1_PROFILE_IDENTIFIER - assert validate(settings).passed(), "Pinning `ro-crate-1.1` should restore the previous behaviour" + settings.skip_checks = ["ro-crate-1.1_4.1"] + result = validate(settings) + failed_check_ids = {issue.to_dict()["check"]["identifier"] for issue in result.get_issues()} + assert "ro-crate-1.1_4.2" not in failed_check_ids From 67c61180c07fe223d5f48512f92b57a79cbe364c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 28 Sep 2026 09:05:30 +0200 Subject: [PATCH 136/271] =?UTF-8?q?docs(changelog):=20=F0=9F=93=9D=20docum?= =?UTF-8?q?ent=20profile=20overlays=20and=20check=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f04168fa..3c8579e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - To keep the previous behaviour, explicitly select `ro-crate-1.1` with `profile_identifier` or `-p ro-crate-1.1`. +### ✨ Added + +- feat(profiles): support rule overlays through `validator:ruleOverlayOf`, allowing a target profile + to reuse checks from a direct parent while retaining source provenance and reporting target-relative + effective identities. Overrides defined by the target profile match parent checks by + `(name, severity)`. +- feat(profile-checks): add an extensible profile consistency framework that detects duplicate check + identities, invalid overlay relationships, and ambiguous checks inherited from multiple sources. +- feat(cli): add `profiles check ` and automatic profile validation, with + `--no-profile-checks` and the corresponding API setting available as explicit opt-outs. +- feat(api): expose effective provenance for checks defined by the target profile, inherited checks, + and replacement checks in validation events, issues, statistics, and skipped-check results. +- feat(cli): show source and effective check identities and their provenance relationships in verbose + `profiles describe` output. + ### 🔧 Changed - refactor(profiles): resolve bare profile tokens to the highest available version using numeric version ordering. - refactor(validation): store the resolved profile identifier in validation settings so downstream consumers use the profile that was actually applied. +- refactor(validation): preserve general-to-specific profile traversal for overlays, execute Python + replacements in their source check's slot, and evaluate the collected SHACL shapes once at the + target profile. +- refactor(cli): use consistent styling for profile names, identifiers, check names, and provenance + labels across profile commands. + +### 🐛 Fixed + +- fix(validation): resolve the overlay target from `profile_identifier` and preserve inherited check + dependencies and effective skip settings independently of profile loading order. +- fix(shacl): load each overlay source graph once before validating the merged shapes graph at the + target profile. +- fix(profiles): reject ambiguous effective identities instead of implicitly selecting one inherited + check, and disambiguate legacy duplicate check names within existing profiles. + +### 📚 Documentation + +- docs(profiles): document ordinary inheritance, check overrides, rule overlays, deactivation, and + source versus effective identities with concrete SHACL and Python examples. +- docs(api): document effective check provenance for composed profiles. + +### ⚡ Performance + +- perf(profiles): cache profile consistency results and effective check provenance within a validation + session to avoid repeated resolution work. ## [0.11.4] - 2026-09-16 From 82df8dcae7b280774f4d83bdba71336a0d19b5dc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 28 Sep 2026 10:44:27 +0200 Subject: [PATCH 137/271] =?UTF-8?q?refactor(profiles):=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20rename=20overlay=20property=20to=20isRuleOverlayOf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- docs/10_api.rst | 2 +- docs/11_writing_a_profile.rst | 6 +++--- rocrate_validator/models/profile.py | 10 +++++----- rocrate_validator/models/profile_check.py | 2 +- rocrate_validator/models/statistics.py | 2 +- rocrate_validator/models/validation.py | 2 +- .../data/profiles/check_overriding/b/profile.ttl | 2 +- .../data/profiles/effective_checks/b/profile.ttl | 2 +- tests/unit/requirements/test_profiles.py | 16 ++++++++-------- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8579e72..5adc09209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### ✨ Added -- feat(profiles): support rule overlays through `validator:ruleOverlayOf`, allowing a target profile +- feat(profiles): support rule overlays through `validator:isRuleOverlayOf`, allowing a target profile to reuse checks from a direct parent while retaining source provenance and reporting target-relative effective identities. Overrides defined by the target profile match parent checks by `(name, severity)`. diff --git a/docs/10_api.rst b/docs/10_api.rst index 467d64fc5..9fc33b470 100644 --- a/docs/10_api.rst +++ b/docs/10_api.rst @@ -123,7 +123,7 @@ Composed profile check identities When a validation target uses ``prof:isProfileOf`` without an overlay, an inherited check keeps the identifier and profile of the source that defines it. -When the target also declares ``validator:ruleOverlayOf``, the API exposes two +When the target also declares ``validator:isRuleOverlayOf``, the API exposes two identities for checks reused from the overlaid source: * the **source identity** identifies the physical implementation and remains diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 23d847511..e24558361 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -228,7 +228,7 @@ The examples below use ``example-profile-1.2`` as source profile ``A`` and implementation from ``A``. * - 3 - Rule overlay - - ``B prof:isProfileOf A`` plus ``B validator:ruleOverlayOf A`` + - ``B prof:isProfileOf A`` plus ``B validator:isRuleOverlayOf A`` - Presents rules reused from ``A`` with effective profile and identifiers based on ``B``, while retaining their source provenance. * - 4 @@ -321,11 +321,11 @@ overlay source: a prof:Profile ; prof:hasToken "example-profile-1.3" ; prof:isProfileOf ; - validator:ruleOverlayOf . + validator:isRuleOverlayOf . .. note:: - ``validator:ruleOverlayOf`` is an RO-Crate Validator extension, not a + ``validator:isRuleOverlayOf`` is an RO-Crate Validator extension, not a property defined by the W3C Profiles Vocabulary. diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 87b61fd86..10a234f26 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -304,7 +304,7 @@ def is_transitive_profile_of(self) -> list[str]: return cast("list[str]", self.__get_specification_property__("isTransitiveProfileOf", PROF_NS, pop_first=False)) @property - def rule_overlay_of(self) -> list[str]: + def is_rule_overlay_of(self) -> list[str]: """ Direct parent profiles whose validation rules are overlaid by this profile. @@ -314,7 +314,7 @@ def rule_overlay_of(self) -> list[str]: Profile consistency checks require every value to resolve to a loaded direct parent and reject ambiguous check identities across sources. """ - return cast("list[str]", self.__get_specification_property__("ruleOverlayOf", VALIDATOR_NS, pop_first=False)) + return cast("list[str]", self.__get_specification_property__("isRuleOverlayOf", VALIDATOR_NS, pop_first=False)) @property def parents(self) -> list[Profile]: @@ -463,7 +463,7 @@ def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequi Return the provenance and reporting identity of ``check`` in this profile. A check defined by this profile replaces direct-parent checks with the - same name and severity. Checks composed through ``ruleOverlayOf`` are + same name and severity. Checks composed through ``isRuleOverlayOf`` are reported in this profile's namespace; ordinary inherited checks retain their source identity. """ @@ -474,7 +474,7 @@ def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequi overlay_replacements = tuple( parent_check for parent_check in replaced_checks - if parent_check.requirement.profile.uri in self.rule_overlay_of + if parent_check.requirement.profile.uri in self.is_rule_overlay_of ) if len(overlay_replacements) == 1: identity_check = overlay_replacements[0] @@ -489,7 +489,7 @@ def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequi replaces=replaced_checks, ) - if source_profile.uri in self.rule_overlay_of: + if source_profile.uri in self.is_rule_overlay_of: relative_identifier = check.relative_identifier.split(" ", maxsplit=1)[-1] return EffectiveRequirementCheck( check=check, diff --git a/rocrate_validator/models/profile_check.py b/rocrate_validator/models/profile_check.py index 11295592a..43f034e23 100644 --- a/rocrate_validator/models/profile_check.py +++ b/rocrate_validator/models/profile_check.py @@ -93,7 +93,7 @@ class RuleOverlayConsistency(ProfileCheck): description = "Rule overlay sources must be loaded direct parents with distinct check identities" def run(self, profile: Profile) -> ProfileCheckResult: - sources = profile.rule_overlay_of + sources = profile.is_rule_overlay_of if not sources: return ProfileCheckResult( check_id=self.identifier, diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index a0035a5bc..a8de8ce59 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -324,7 +324,7 @@ def __initialise__(cls, validation_settings: ValidationSettings): inherited for inherited in profile.inherited_profiles if not validation_settings.disable_inherited_profiles_issue_reporting - or inherited.uri in profile.rule_overlay_of + or inherited.uri in profile.is_rule_overlay_of ) logger.debug("Inherited profiles: %r", profile.inherited_profiles) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 55b76d630..4a50b1bca 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -716,7 +716,7 @@ def target_profile(self) -> Profile: def is_rule_overlay_source(self, profile: Profile) -> bool: """Return whether ``profile`` is composed into the validation target.""" target = self.target_profile - return profile == target or profile.uri in target.rule_overlay_of + return profile == target or profile.uri in target.is_rule_overlay_of def effective_check_identifier(self, check: RequirementCheck) -> str: """Return a context-local identifier without mutating the source check.""" diff --git a/tests/data/profiles/check_overriding/b/profile.ttl b/tests/data/profiles/check_overriding/b/profile.ttl index 7b1c66e1d..917a011d3 100644 --- a/tests/data/profiles/check_overriding/b/profile.ttl +++ b/tests/data/profiles/check_overriding/b/profile.ttl @@ -34,7 +34,7 @@ # # This profile is an extension of the RO-Crate Metadata Specification 1.1 profile prof:isProfileOf ; - validator:ruleOverlayOf ; + validator:isRuleOverlayOf ; # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile prof:isTransitiveProfileOf ; diff --git a/tests/data/profiles/effective_checks/b/profile.ttl b/tests/data/profiles/effective_checks/b/profile.ttl index 615f2170e..347897da4 100644 --- a/tests/data/profiles/effective_checks/b/profile.ttl +++ b/tests/data/profiles/effective_checks/b/profile.ttl @@ -6,5 +6,5 @@ a prof:Profile ; rdfs:label "Effective checks overlay profile" ; prof:isProfileOf ; - validator:ruleOverlayOf ; + validator:isRuleOverlayOf ; prof:hasToken "effective-b" . diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index ee87ca058..fb0fbda74 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -419,16 +419,16 @@ def test_rule_overlay_sources_are_consistent(check_overriding_profiles_path: str def test_rule_overlay_source_must_be_a_direct_parent(check_overriding_profiles_path: str, monkeypatch): profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) overlay = next(item for item in profiles if item.identifier == "d") - original_rule_overlay_of = cast("Any", Profile.rule_overlay_of).fget - assert original_rule_overlay_of is not None + original_is_rule_overlay_of = cast("Any", Profile.is_rule_overlay_of).fget + assert original_is_rule_overlay_of is not None monkeypatch.setattr( Profile, - "rule_overlay_of", + "is_rule_overlay_of", property( lambda profile: ( ["https://w3id.org/a"] if profile.identifier == overlay.identifier - else original_rule_overlay_of(profile) + else original_is_rule_overlay_of(profile) ) ), ) @@ -446,16 +446,16 @@ def test_multiple_rule_overlay_sources_require_distinct_check_identities( ): profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) overlay = next(item for item in profiles if item.identifier == "y") - original_rule_overlay_of = cast("Any", Profile.rule_overlay_of).fget - assert original_rule_overlay_of is not None + original_is_rule_overlay_of = cast("Any", Profile.is_rule_overlay_of).fget + assert original_is_rule_overlay_of is not None monkeypatch.setattr( Profile, - "rule_overlay_of", + "is_rule_overlay_of", property( lambda profile: ( ["https://w3id.org/e", "https://w3id.org/f"] if profile.identifier == overlay.identifier - else original_rule_overlay_of(profile) + else original_is_rule_overlay_of(profile) ) ), ) From 156d53bfcf969b104b59e55fe6bca4f6bd9bdb00 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 29 Sep 2026 10:41:40 +0200 Subject: [PATCH 138/271] test(profiles): narrow HTTPS Schema.org @id compatibility assertion --- .../ro-crate-1.3/test_metadata_document_v1_3.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py index 58f2b1ea2..10cf57fe8 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py @@ -440,14 +440,16 @@ def test_https_entity_id_is_not_recommended(): "name": "Standalone Schema.org resource", } ) - do_entity_test( - __metadata_document_crates__.valid_schema_org_iri_protocol, - models.Severity.RECOMMENDED, - True, - profile_identifier="ro-crate-1.3", - metadata_dict=metadata_dict, - metadata_only=True, + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.valid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.3", + metadata_only=True, + metadata_dict=metadata_dict, + ) ) + assert not any(issue.check.requirement.name == "Schema.org @id compatibility" for issue in result.get_issues()) def test_https_nested_schema_org_id_is_recommended_for_compatibility(): From 0416446bade7776120f112ee58d64fdffd1b79e5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 29 Sep 2026 13:59:27 +0200 Subject: [PATCH 139/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20corre?= =?UTF-8?q?ct=20skipped=20check=20counts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Count each check once using its final result. Do not mark already processed checks as skipped when validation aborts. --- rocrate_validator/models/requirement.py | 6 +++++ rocrate_validator/models/statistics.py | 32 ++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 96af9af68..6b52b3fc0 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -293,6 +293,12 @@ def __record_skipped_check__( RequirementCheckValidationEvent, ) + # Overlay replacements and batched SHACL checks may already have an + # outcome before traversal reaches their own requirement. Aborting + # must not overwrite that outcome or emit a misleading skipped event. + if category is SkipCategory.NOT_REACHED and context.result.get_check_result(check) is not None: + return + context.result._record_check_result(check, CheckResult.SKIPPED, message, category) inherited_reporting_disabled = ( check.requirement.profile.identifier != context.profile_identifier diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index a8de8ce59..e093cc018 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -416,6 +416,29 @@ def __handle_requirement_validation_start__(self, _event: Event, _ctx: Validatio def __handle_requirement_check_validation_start__(self, _event: Event, _ctx: ValidationContext | None) -> None: logger.debug("Requirement check validation start") + def __record_check_status__(self, check: RequirementCheck, result: CheckResult) -> None: + """ + Keep one final status for each requirement check. + + A check can emit more than one result while validating a crate (for + example, a dependency may be skipped before a later execution). The + statistics describe the final validation state, not the history of + intermediate events. + """ + for status in ("passed_checks", "failed_checks", "skipped_checks", "validated_checks"): + self._stats[status] = [ + candidate for candidate in self._stats[status] if candidate.identifier != check.identifier + ] + + if result is CheckResult.PASSED: + self._stats["passed_checks"].append(check) + self._stats["validated_checks"].append(check) + elif result is CheckResult.FAILED: + self._stats["failed_checks"].append(check) + self._stats["validated_checks"].append(check) + else: + self._stats["skipped_checks"].append(check) + def __handle_requirement_check_validation_end__(self, event: Event, ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementCheckValidationEvent) assert ctx is not None @@ -428,14 +451,7 @@ def __handle_requirement_check_validation_end__(self, event: Event, ctx: Validat if event.validation_result is not None: if event.requirement_check.severity >= requirement_severity: result = normalize_check_result(event.validation_result) - if result is CheckResult.PASSED: - self._stats["passed_checks"].append(event.requirement_check) - elif result is CheckResult.FAILED: - self._stats["failed_checks"].append(event.requirement_check) - else: - self._stats["skipped_checks"].append(event.requirement_check) - if result is not CheckResult.SKIPPED: - self._stats["validated_checks"].append(event.requirement_check) + self.__record_check_status__(event.requirement_check, result) self.notify_listeners() else: logger.debug( From 08d9e0a40b40f169f9b0b8b43ec3446f8261d0ce Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 29 Sep 2026 14:11:11 +0200 Subject: [PATCH 140/271] =?UTF-8?q?test(validation):=20=E2=9C=85=20cover?= =?UTF-8?q?=20skipped=20counts=20and=20overlay=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify skipped counts match final results without duplicates. Ensure failed overlay checks remain failed after a fail-fast abort. --- tests/test_models.py | 54 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index cd580e07b..556745c9d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path + import pytest from rocrate_validator import models, services from rocrate_validator.models import URI, LevelCollection, RequirementLevel, Severity, ValidationSettings -from tests.ro_crates import InvalidRootDataEntity, WROCInvalidReadme +from rocrate_validator.requirements.python import PyFunctionCheck +from tests.ro_crates import InvalidRootDataEntity, ValidROC, WROCInvalidReadme def test_severity_ordering(): @@ -95,6 +98,55 @@ def test_sortability_checks(validation_settings: ValidationSettings): assert one.requirement >= two.requirement +def test_validation_statistics_count_final_check_statuses(validation_settings: ValidationSettings): + """Repeated intermediate skip events must not inflate final check counts.""" + validation_settings.rocrate_uri = URI(str(ValidROC().wrroc_paper)) + validation_settings.profile_identifier = "ro-crate-1.2" + validation_settings.requirement_severity = Severity.REQUIRED + validation_settings.abort_on_first = True + + result = services.validate(validation_settings) + statistics = result.statistics + + assert statistics.total_skipped_checks == result.skipped_checks_count + assert len({check.identifier for check in statistics.skipped_checks}) == statistics.total_skipped_checks + assert set(statistics.skipped_checks) == result.skipped_checks + assert set(statistics.skipped_checks).isdisjoint(statistics.validated_checks) + assert len(statistics.validated_checks) + statistics.total_skipped_checks == statistics.total_checks + + +def test_validation_statistics_preserve_failed_overlay_check_on_abort( + validation_settings: ValidationSettings, monkeypatch: pytest.MonkeyPatch +): + """Fail-fast must preserve a replacement that failed in its source-profile slot.""" + validation_settings.rocrate_uri = URI(str(ValidROC().wrroc_paper)) + validation_settings.profiles_path = Path(__file__).parent / "data" / "profiles" / "effective_checks" + validation_settings.profile_identifier = "effective-b" + validation_settings.requirement_severity = Severity.REQUIRED + validation_settings.abort_on_first = True + execute_check = PyFunctionCheck.execute_check + + def fail_overlay_check(check, context): + if check.requirement.profile.identifier == "effective-b" and check.name == "Shared check": + context.result.add_issue("Overlay replacement failed", check) + return False + return execute_check(check, context) + + monkeypatch.setattr(PyFunctionCheck, "execute_check", fail_overlay_check) + + result = services.validate(validation_settings) + statistics = result.statistics + + assert not result.passed() + assert len(result.failed_checks) == 1 + failed_check = next(iter(result.failed_checks)) + assert statistics.failed_checks == [failed_check] + assert result.get_check_result(failed_check) is models.CheckResult.FAILED + assert failed_check in statistics.validated_checks + assert failed_check in result.executed_checks + assert statistics.total_skipped_checks == result.skipped_checks_count == 0 + + def test_sortability_issues(validation_settings: ValidationSettings): validation_settings.rocrate_uri = URI(str(WROCInvalidReadme().wroc_readme_wrong_encoding_format)) result: models.ValidationResult = services.validate(validation_settings) From 39f3f6409973975ab7710cc1b83c6d8c6727b0c2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 29 Sep 2026 16:38:05 +0200 Subject: [PATCH 141/271] =?UTF-8?q?fix(models):=20=F0=9F=93=8A=20keep=20ch?= =?UTF-8?q?eck=20statistics=20aligned=20with=20validation=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Count only checks selected for the current validation and keep skipped details in the same scope. Preserve recorded outcomes when execution revisits a check, and support deferred results from batched validation. --- rocrate_validator/models/check_result.py | 4 +++ rocrate_validator/models/requirement.py | 22 ++++++++++--- rocrate_validator/models/result.py | 11 ++++--- rocrate_validator/models/statistics.py | 39 ++++++++++++------------ 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/rocrate_validator/models/check_result.py b/rocrate_validator/models/check_result.py index da1674147..cdcf43397 100644 --- a/rocrate_validator/models/check_result.py +++ b/rocrate_validator/models/check_result.py @@ -28,6 +28,10 @@ class CheckResult(str, Enum): CheckResultValue = bool | CheckResult | None +class DeferredRequirementCheck(Exception): + """Leave a check pending until its validation backend records an outcome.""" + + def normalize_check_result(value: CheckResultValue) -> CheckResult: """Convert legacy boolean and ``None`` outcomes to a check result.""" if isinstance(value, CheckResult): diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 6b52b3fc0..37401c627 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -28,7 +28,12 @@ from rocrate_validator.errors import CheckDependencyError, ROCrateMetadataNotFoundError, ValidationExecutionError from rocrate_validator.events import EventType from rocrate_validator.models._logging import logger -from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result +from rocrate_validator.models.check_result import ( + CheckResult, + CheckResultValue, + DeferredRequirementCheck, + normalize_check_result, +) from rocrate_validator.models.severity import ( LevelCollection, RequirementLevel, @@ -231,6 +236,9 @@ def _do_validate_(self, context: ValidationContext) -> bool: all_passed, should_break = self.__execute_check__(check, context, all_passed) if should_break: break + except DeferredRequirementCheck: + logger.debug("Check '%s' is pending batched validation", check.name) + continue except SkipRequirementCheck as e: logger.debug("Skipping check '%s' because: %s", check.name, e) self.__record_skipped_check__(check, context, e.message or "Check requested a skip", e.category) @@ -431,8 +439,10 @@ def __execute_check__( # An overlay replacement may already have run in the source profile's # slot. Keep the target pass a no-op instead of executing the same # Python check twice. - if context.result.get_check_result(check) is not None: - return all_passed, False + recorded_result = context.result.get_check_result(check) + if recorded_result is not None: + failed = recorded_result is CheckResult.FAILED + return all_passed and not failed, failed and context.fail_fast execution_check = self.__resolve_execution_check__(check, context) if execution_check is None: @@ -487,8 +497,10 @@ def __execute_check__( normalized_result, ) new_all_passed = all_passed and normalized_result is not CheckResult.FAILED - should_break = normalized_result is CheckResult.FAILED and context.fail_fast - return new_all_passed, should_break + # A batched backend can also report failures for checks other than the + # one driving this execution. + should_break = context.fail_fast and (normalized_result is CheckResult.FAILED or context.result.has_issues()) + return new_all_passed and not should_break, should_break def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 58f2bd2ec..03a4effc3 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -321,19 +321,22 @@ def get_executed_check_result(self, check: RequirementCheck) -> bool | None: @property def skipped_checks(self) -> set[RequirementCheck]: """ - The checks that have been skipped + The reportable checks that have been skipped. + + Internal outcomes remain available through ``get_check_result`` even + for hidden, overridden or otherwise out-of-scope checks. """ - return self._skipped_checks + return {check for check in self._skipped_checks if check in self.statistics.checks} @property def skipped_checks_count(self) -> int: """Get the number of skipped checks.""" - return len(self._skipped_checks) + return len(self.skipped_checks) @property def skipped_check_details(self) -> list[SkippedCheckDetail]: """Get the ordered explanations for skipped checks.""" - return list(self._skipped_check_details.values()) + return [detail for detail in self._skipped_check_details.values() if detail.check in self.statistics.checks] def record_skip( self, diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index e093cc018..281b567cd 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -270,7 +270,6 @@ def __collect_requirement_checks__( requirement, *, severity_validation, - validation_settings, target_profile_identifier, checks, checks_by_severity, @@ -290,8 +289,7 @@ def __collect_requirement_checks__( requirement_checks = [ _ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) - if (not validation_settings.skip_checks or _.identifier not in validation_settings.skip_checks) - and (not _.overridden or _.requirement.profile.identifier == target_profile_identifier) + if not _.overridden or _.requirement.profile.identifier == target_profile_identifier ] num_checks = len(requirement_checks) requirement_checks_count += num_checks @@ -323,8 +321,11 @@ def __initialise__(cls, validation_settings: ValidationSettings): profiles.extend( inherited for inherited in profile.inherited_profiles - if not validation_settings.disable_inherited_profiles_issue_reporting - or inherited.uri in profile.is_rule_overlay_of + if validation_settings.enable_profile_inheritance + and ( + not validation_settings.disable_inherited_profiles_issue_reporting + or inherited.uri in profile.is_rule_overlay_of + ) ) logger.debug("Inherited profiles: %r", profile.inherited_profiles) @@ -343,8 +344,10 @@ def __initialise__(cls, validation_settings: ValidationSettings): # Process the requirements and checks processed_requirements = [] - for profile in profiles: - for requirement in profile.requirements: + for validation_profile in profiles: + for requirement in validation_profile.get_requirements( + cast("Severity", severity_validation), exact_match=validation_settings.requirement_severity_only + ): if requirement in processed_requirements: continue processed_requirements.append(requirement) @@ -354,7 +357,6 @@ def __initialise__(cls, validation_settings: ValidationSettings): requirement_checks_count = cls.__collect_requirement_checks__( requirement, severity_validation=severity_validation, - validation_settings=validation_settings, target_profile_identifier=target_profile_identifier, checks=checks, checks_by_severity=checks_by_severity, @@ -442,16 +444,10 @@ def __record_check_status__(self, check: RequirementCheck, result: CheckResult) def __handle_requirement_check_validation_end__(self, event: Event, ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementCheckValidationEvent) assert ctx is not None - target_profile = ctx.target_validation_profile - requirement_severity = self._settings.requirement_severity - if not event.requirement_check.requirement.hidden and ( - not event.requirement_check.overridden - or target_profile.identifier == event.requirement_check.requirement.profile.identifier - ): + if event.requirement_check in self.checks: if event.validation_result is not None: - if event.requirement_check.severity >= requirement_severity: - result = normalize_check_result(event.validation_result) - self.__record_check_status__(event.requirement_check, result) + result = normalize_check_result(event.validation_result) + self.__record_check_status__(event.requirement_check, result) self.notify_listeners() else: logger.debug( @@ -466,7 +462,11 @@ def __handle_requirement_check_validation_end__(self, event: Event, ctx: Validat def __handle_requirement_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementValidationEvent) - if not event.requirement.hidden: + if event.requirement in self.requirements: + for status in ("passed_requirements", "failed_requirements", "validated_requirements"): + self._stats[status] = [ + requirement for requirement in self._stats[status] if requirement != event.requirement + ] if event.validation_result: self._stats["passed_requirements"].append(event.requirement) else: @@ -476,7 +476,8 @@ def __handle_requirement_validation_end__(self, event: Event, _ctx: ValidationCo def __handle_profile_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: assert isinstance(event, ProfileValidationEvent) - self._stats["validated_profiles"].append(event.profile) + if event.profile in self.profiles and event.profile not in self.validated_profiles: + self._stats["validated_profiles"].append(event.profile) logger.debug("Profile validation ended: %s", event.profile.identifier) def __handle_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: From d4ab2cb84e45cbe5a57b642aa878ba0e943724d1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 29 Sep 2026 16:43:11 +0200 Subject: [PATCH 142/271] =?UTF-8?q?fix(validation):=20=F0=9F=94=84=20align?= =?UTF-8?q?=20batched=20SHACL=20outcomes=20and=20live=20progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep inherited SHACL checks pending until the combined validation records their outcome. Include batched target failures in statistics and derive live progress from unique check results. --- .../requirements/shacl/checks.py | 91 ++++++++----------- .../requirements/shacl/requirements.py | 23 ++++- .../requirements/shacl/validator.py | 13 ++- .../io_helpers/output/text/layout/progress.py | 29 ++++-- .../io_helpers/output/text/layout/report.py | 2 + 5 files changed, 87 insertions(+), 71 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index a888ed593..5cb028789 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -28,7 +28,7 @@ RequirementCheckValidationEvent, RequirementLevel, Severity, - SkipRequirementCheck, + SkipCategory, SourceSnippet, ValidationContext, ) @@ -38,7 +38,6 @@ SHACLValidationAlreadyProcessed, SHACLValidationContext, SHACLValidationContextManager, - SHACLValidationSkip, SHACLValidator, SHACLViolation, ) @@ -243,7 +242,7 @@ def execute_check(self, context: ValidationContext): self.identifier, ) result = self.__do_execute_check__(ctx) - ctx.current_validation_result = self.identifier not in result + ctx.current_validation_result = self not in result logger.debug( "SHACL Validation of requirement check %s (profile: %s) finished with result %s", self.requirement.profile.identifier, @@ -260,15 +259,6 @@ def execute_check(self, context: ValidationContext): # The check belongs to a profile which has already been processed # so we can skip the validation and return the specific result for the check return self.identifier not in [i.check.identifier for i in context.result.get_issues()] - except SHACLValidationSkip as e: - logger.debug( - "SHACL Validation of profile %s requirement %s skipped", - self.requirement.profile.identifier, - self.identifier, - ) - # The validation is postponed to the more specific profiles - # so the check is not considered as failed. - raise SkipRequirementCheck(self, str(e)) from e except ROCrateMetadataNotFoundError as e: logger.debug( "Unable to perform metadata validation due to missing metadata file: %s", @@ -340,10 +330,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): failed_requirements_checks, failed_requirements_checks_violations = self.__collect_failed_checks__( shacl_context, shacl_result, shapes_registry, shapes_graph ) - failed_requirement_checks_notified = self.__process_failed_checks__( - shacl_context, failed_requirements_checks, failed_requirements_checks_violations - ) - self.__notify_skipped_checks__(shacl_context, failed_requirement_checks_notified) + self.__process_failed_checks__(shacl_context, failed_requirements_checks, failed_requirements_checks_violations) + self.__notify_deferred_checks__(shacl_context, failed_requirements_checks) logger.debug("Remaining skipped checks: %r", len(shacl_context.result.skipped_checks)) for skipped_check in shacl_context.result.skipped_checks: @@ -429,24 +417,17 @@ def __process_failed_checks__( failed_requirements_checks_violations[requirementCheck.identifier], ) - # If the fail fast mode is disabled, notify all the validation issues - # related to profiles other than the current one. - # They are issues which have not been notified yet because skipped during - # the validation of their corresponding profile because SHACL checks are executed - # all together and not profile by profile + # Notify every batched outcome, including target-local checks: + # traversal will reuse their recorded results without executing them. if requirementCheck.identifier not in failed_requirement_checks_notified: shacl_context.result._add_executed_check(requirementCheck, False) - if requirementCheck.requirement.profile != shacl_context.current_validation_profile: - failed_requirement_checks_notified.append(requirementCheck.identifier) - shacl_context.validator.notify( - RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False - ) - ) - logger.debug( - "Added failed check to the context: %s", - requirementCheck.identifier, + failed_requirement_checks_notified.append(requirementCheck.identifier) + shacl_context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False ) + ) + logger.debug("Added failed check to the context: %s", requirementCheck.identifier) # if the fail fast mode is enabled, stop the validation after the first failed check if shacl_context.fail_fast: @@ -490,32 +471,34 @@ def __register_check_violations__(self, shacl_context, requirementCheck, violati if shacl_context.fail_fast: break - def __notify_skipped_checks__(self, shacl_context, failed_requirement_checks_notified): - for skipped_check in list(shacl_context.result.skipped_checks): - logger.debug("Processing skipped check: %s", skipped_check.identifier) - if not isinstance(skipped_check, SHACLCheck): - logger.debug("Skipped check is not a SHACLCheck: %s", skipped_check.identifier) + def __notify_deferred_checks__(self, shacl_context, failed_checks): + """Complete pending checks without changing genuine skip outcomes.""" + for check in sorted(shacl_context.deferred_checks): + if shacl_context.result.get_check_result(check) is not None: continue - if skipped_check.identifier not in failed_requirement_checks_notified: - failed_requirement_checks_notified.append(skipped_check.identifier) - shacl_context.result._add_executed_check(skipped_check, True) - if ( - skipped_check.requirement.profile != shacl_context.target_profile - and not shacl_context.is_rule_overlay_source(skipped_check.requirement.profile) - and shacl_context.settings.disable_inherited_profiles_issue_reporting - ): - continue - shacl_context.validator.notify( - RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, - skipped_check, - validation_result=True, - ) + if shacl_context.fail_fast and failed_checks: + Requirement.__record_skipped_check__( + check, + shacl_context.base_context, + "SHACL validation stopped before this check's outcome was determined", + SkipCategory.NOT_REACHED, ) - logger.debug( - "Added skipped check to the context: %s", - skipped_check.identifier, + continue + shacl_context.result._add_executed_check(check, True) + if ( + check.requirement.profile != shacl_context.target_profile + and not shacl_context.is_rule_overlay_source(check.requirement.profile) + and shacl_context.settings.disable_inherited_profiles_issue_reporting + ): + continue + shacl_context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, + check, + validation_result=True, ) + ) + shacl_context.deferred_checks.clear() @classmethod def get_instance(cls, shape: Shape) -> Optional["SHACLCheck"]: diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 68f80601c..d46b48d27 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -25,6 +25,7 @@ RequirementCheck, RequirementLevel, RequirementLoader, + SkipCategory, ValidationContext, ) from rocrate_validator.requirements.shacl.checks import SHACLCheck @@ -102,22 +103,29 @@ def finalize(cls, context: ValidationContext) -> None: logger.debug("Starting %s requirement finalization for context %s", cls.__name__, context) + from rocrate_validator.requirements.shacl.validator import SHACLValidationContext # noqa: PLC0415 + + shacl_context = SHACLValidationContext.get_instance(context) # If the validation was aborted (e.g. the metadata is not valid JSON), the data # graph cannot be parsed: skip the forced SHACL run to avoid false positives. - if context.aborted: + if context.aborted or (context.fail_fast and context.result.has_issues()): + cls.__record_skipped_checks__( + shacl_context.deferred_checks, + context, + "Validation stopped before the deferred SHACL checks were executed", + SkipCategory.NOT_REACHED, + ) + shacl_context.deferred_checks.clear() logger.debug("Skipping forced SHACL run: validation aborted (%s)", context.abort_reason) return # extract profiles and target profile from context profiles = context.profiles - from rocrate_validator.requirements.shacl.validator import SHACLValidationContext # noqa: PLC0415 - target = next((p for p in profiles if p.identifier == context.settings.profile_identifier), None) if target is None: return - shacl_context = SHACLValidationContext.get_instance(context) # If pyshacl already ran for the target during the main loop there is # nothing to do. if shacl_context.get_validation_result(target) is not None: @@ -167,6 +175,13 @@ def finalize(cls, context: ValidationContext) -> None: ) from e finally: shacl_context.__unset_current_validation_profile__() + cls.__record_skipped_checks__( + shacl_context.deferred_checks, + context, + "Deferred SHACL validation could not be completed", + SkipCategory.EXCEPTION, + ) + shacl_context.deferred_checks.clear() # do finalization logic here (empty for now) logger.debug("Completed %s requirement finalization for context %s", cls.__name__, context) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 78e84b6f5..114867095 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -41,6 +41,7 @@ ValidationContext, ValidationResult, ) +from rocrate_validator.models.check_result import DeferredRequirementCheck from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry from rocrate_validator.requirements.shacl.transformers.preparation import ( @@ -55,8 +56,8 @@ logger = logging.getLogger(__name__) -class SHACLValidationSkip(Exception): - pass +class SHACLValidationSkip(DeferredRequirementCheck): + """Defer a source-profile check until the merged SHACL validation runs.""" class SHACLValidationAlreadyProcessed(Exception): @@ -80,11 +81,12 @@ def __enter__(self) -> SHACLValidationContext: ) logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.identifier) if self._profile.identifier != self._context.settings.profile_identifier: - logger.debug("Skipping validation of profile %s", self._profile.identifier) + logger.debug("Deferring validation of profile %s", self._profile.identifier) # This is a temporary control-flow skip. The target profile runs # SHACL once over the merged shapes graph and records the check # outcome afterwards. - raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.identifier}") + self._shacl_context.deferred_checks.add(self._check) + raise SHACLValidationSkip(f"Deferring validation of profile {self._profile.identifier}") logger.debug("ValidationContext of profile %s initialized", self._profile.identifier) return self._shacl_context @@ -124,6 +126,9 @@ def __init__(self, context: ValidationContext): # loaded only once before the combined validation is executed. self._loaded_profiles: set[str] = set() + # Checks awaiting the combined run have no result yet; they are not skips. + self.deferred_checks: set[RequirementCheck] = set() + # reference to the current validation profile self._current_validation_profile: Profile | None = None diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/progress.py b/rocrate_validator/utils/io_helpers/output/text/layout/progress.py index 5796960bb..2719fb83f 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/progress.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/progress.py @@ -19,6 +19,7 @@ RequirementCheckValidationEvent, RequirementValidationEvent, ValidationContext, + ValidationEvent, ValidationSettings, ValidationStatistics, ) @@ -59,9 +60,16 @@ def __init__(self, settings: dict | ValidationSettings, stats: ValidationStatist self.REQUIREMENT_CHECK_VALIDATION, total=stats.total_checks ) # Initialize progress according to current statistics - self.__progress.update(task_id=self.profile_validation, advance=len(stats.validated_profiles)) - self.__progress.update(task_id=self.requirement_validation, advance=len(stats.validated_requirements)) - self.__progress.update(task_id=self.requirement_check_validation, advance=len(stats.validated_checks)) + self._update_progress(stats) + + def _update_progress(self, stats: ValidationStatistics) -> None: + """Use the same unique, in-scope outcomes for live and final progress.""" + self.__progress.update(task_id=self.profile_validation, completed=len(stats.validated_profiles)) + self.__progress.update(task_id=self.requirement_validation, completed=len(stats.validated_requirements)) + self.__progress.update( + task_id=self.requirement_check_validation, + completed=len(stats.validated_checks) + stats.total_skipped_checks, + ) def start(self): self.__progress.start() @@ -77,13 +85,16 @@ def _on_requirement_check_validation_end( self, event: RequirementCheckValidationEvent, ctx: ValidationContext | None ) -> None: assert ctx is not None, "Validation context must be provided" - # Only advance the progress for checks at or above the requested severity threshold, - # so the bar matches the set of checks actually reported. - if event.requirement_check.severity >= ctx.settings.requirement_severity: - self.__progress.update(task_id=self.requirement_check_validation, advance=1) + self._update_progress(ctx.result.statistics) def _on_requirement_validation_end(self, event: RequirementValidationEvent, ctx: ValidationContext | None) -> None: - self.__progress.update(task_id=self.requirement_validation, advance=1) + assert ctx is not None, "Validation context must be provided" + self._update_progress(ctx.result.statistics) def _on_profile_validation_end(self, event: ProfileValidationEvent, ctx: ValidationContext | None) -> None: - self.__progress.update(task_id=self.profile_validation, advance=1) + assert ctx is not None, "Validation context must be provided" + self._update_progress(ctx.result.statistics) + + def _on_validation_end(self, event: ValidationEvent, ctx: ValidationContext | None) -> None: + assert ctx is not None, "Validation context must be provided" + self._update_progress(ctx.result.statistics) diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/report.py b/rocrate_validator/utils/io_helpers/output/text/layout/report.py index 1cdb9c1a2..9ed51e479 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/report.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/report.py @@ -328,6 +328,8 @@ def _on_requirement_validation_end(self, event: RequirementValidationEvent, ctx: self._layout.update_stats(ctx.result.statistics) def _on_validation_end(self, event: ValidationEvent, ctx: ValidationContext | None) -> None: + assert ctx is not None, "Validation context must be provided" + self._layout.update_stats(ctx.result.statistics) self._layout.show_overall_result(event.validation_result) From a8cd0fe28c3f1c6580b60f5d1cdb9d4174c7cced Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 29 Sep 2026 16:49:57 +0200 Subject: [PATCH 143/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20deferred=20SHACL=20status=20and=20progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise deferred checks across overlays, abort and fail-fast handling, configured skips, batched failures, and live/final progress. Update profile and skip-detail tests for validation- scope filtering. --- tests/unit/requirements/test_profiles.py | 8 +- tests/unit/test_skipped_check_details.py | 9 +- tests/unit/test_validation_statistics.py | 228 +++++++++++++++++++++++ 3 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_validation_statistics.py diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index fb0fbda74..5f7f760b8 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -29,6 +29,7 @@ Profile, RequirementCheckRelation, Severity, + SkippedCheckDetail, ValidationContext, ValidationSettings, Validator, @@ -587,8 +588,13 @@ def test_rule_overlay_effective_identity_is_context_local(check_overriding_profi assert serialized_check["source_identifier"] == source_identifier assert serialized_check["source_profile"] == "a" + # Keep internal outcomes for dependency resolution, but do not count a + # replaced source check among the reportable skips of the overlay. context.result._record_check_result(inherited_check, CheckResult.SKIPPED, "test skip") - serialized_skip = context.result.skipped_check_details[0].to_dict() + assert context.result.get_check_result(inherited_check) is CheckResult.SKIPPED + assert context.result.skipped_checks_count == 0 + assert context.result.skipped_check_details == [] + serialized_skip = SkippedCheckDetail(inherited_check, "test skip", context=context).to_dict() assert serialized_skip["identifier"].startswith("b_") assert serialized_skip["profile"] == "b" assert serialized_skip["source_identifier"] == source_identifier diff --git a/tests/unit/test_skipped_check_details.py b/tests/unit/test_skipped_check_details.py index ae68b8584..0852d52a3 100644 --- a/tests/unit/test_skipped_check_details.py +++ b/tests/unit/test_skipped_check_details.py @@ -17,13 +17,14 @@ from rocrate_validator.models import CheckResult, SkipCategory, ValidationResult -def _result(): +def _result(check): result = object.__new__(ValidationResult) result._check_results = {} result._executed_checks = set() result._executed_checks_results = {} result._skipped_checks = set() result._skipped_check_details = {} + result._statistics = SimpleNamespace(checks={check}) return result @@ -41,8 +42,8 @@ def _check(): def test_skipped_check_detail_preserves_reason_and_category(): - result = _result() check = _check() + result = _result(check) result._record_check_result(check, CheckResult.SKIPPED, "base failed", SkipCategory.DEPENDENCY) @@ -59,8 +60,8 @@ def test_skipped_check_detail_preserves_reason_and_category(): def test_skipped_check_detail_is_removed_when_check_passes(): - result = _result() check = _check() + result = _result(check) result.record_skip(check, "base skipped", SkipCategory.DEPENDENCY) result._record_check_result(check, CheckResult.PASSED) @@ -69,8 +70,8 @@ def test_skipped_check_detail_is_removed_when_check_passes(): def test_record_skip_normalizes_string_category_to_enum(): - result = _result() check = _check() + result = _result(check) result.record_skip(check, "metadata descriptor is not available", "exception") diff --git a/tests/unit/test_validation_statistics.py b/tests/unit/test_validation_statistics.py new file mode 100644 index 000000000..2fedb16e7 --- /dev/null +++ b/tests/unit/test_validation_statistics.py @@ -0,0 +1,228 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + +from rocrate_validator import services +from rocrate_validator.events import EventType, Subscriber +from rocrate_validator.models import CheckResult, SkipCategory, ValidationContext, ValidationSettings, Validator +from rocrate_validator.requirements.shacl.checks import SHACLCheck +from rocrate_validator.requirements.shacl.validator import SHACLValidator +from rocrate_validator.utils.io_helpers.output.text.layout.progress import ProgressMonitor + + +@pytest.fixture +def statistics_settings(tmp_path: Path): + """A small overlay with inherited, deactivated, hidden and target-local checks.""" + profiles = tmp_path / "profiles" + for token in ("base", "overlay", "wrapper"): + profile = profiles / token + profile.mkdir(parents=True) + inheritance = "prof:isProfileOf ; validator:isRuleOverlayOf ;" if token != "base" else "" + (profile / "profile.ttl").write_text( + f""" + @prefix prof: . + @prefix rdfs: . + @prefix validator: . + a prof:Profile; prof:hasToken "{token}"; + rdfs:label "{token}"; {inheritance} . + """, + encoding="utf-8", + ) + prefixes = """ + @prefix sh: . + @prefix schema: . + @prefix validator: . + """ + (profiles / "base" / "shapes.ttl").write_text( + prefixes + + """ + a sh:NodeShape; sh:name "Name"; sh:targetNode ; + sh:property [sh:path schema:name; sh:name "Required name"; sh:minCount 1] . + a sh:NodeShape; sh:name "Disabled"; sh:targetNode ; + sh:deactivated true; sh:class schema:Dataset . + a sh:NodeShape, validator:HiddenShape; sh:name "Hidden"; + sh:targetNode ; sh:class schema:Thing . + """, + encoding="utf-8", + ) + (profiles / "overlay" / "shapes.ttl").write_text( + prefixes + + """ + a sh:NodeShape; sh:name "Description"; sh:targetNode ; + sh:property [sh:path schema:description; sh:name "Required description"; sh:minCount 1] . + """, + encoding="utf-8", + ) + return ValidationSettings( + rocrate_uri=tmp_path, + profiles_path=profiles, + profile_identifier="overlay", + metadata_only=True, + metadata_dict={ + "@context": {"@vocab": "http://schema.org/"}, + "@graph": [{"@id": "urn:item", "@type": "Thing", "name": "Example", "description": "Example"}], + }, + ) + + +class _StatisticsTrace(Subscriber): + def __init__(self): + super().__init__("statistics trace") + self.skipped_counts = [] + self.profile_snapshots = {} + + def update(self, event, ctx=None): + statistics = ctx.result.statistics + self.skipped_counts.append(statistics.total_skipped_checks) + if event.event_type is EventType.PROFILE_VALIDATION_END: + self.profile_snapshots[event.profile.identifier] = ( + set(statistics.validated_checks), + set(statistics.skipped_checks), + ) + + +class _ProgressTrace(ProgressMonitor): + def __init__(self, settings, stats=None): + super().__init__(settings, stats) + self.snapshots = [] + + def update(self, event, ctx=None): + super().update(event, ctx) + self.snapshots.append([(task.completed, task.total) for task in self.progress.tasks]) + + +@pytest.mark.parametrize("profile", ["overlay", "wrapper"]) +def test_deferred_checks_are_not_reported_as_skipped(statistics_settings, profile): + statistics_settings.profile_identifier = profile + trace = _StatisticsTrace() + result = services.validate(statistics_settings, subscribers=[trace]) + + assert result.passed() + assert trace.skipped_counts == sorted(trace.skipped_counts) + assert result.statistics.total_skipped_checks == result.skipped_checks_count == 1 + assert result.skipped_check_details[0].category is SkipCategory.DEACTIVATED + validated, skipped = trace.profile_snapshots["base"] + assert not validated # inherited SHACL checks are still pending + assert {check.name for check in skipped} == {"Disabled"} + assert set(result.statistics.validated_checks) | set(result.statistics.skipped_checks) == result.statistics.checks + + +@pytest.mark.parametrize("profile", ["overlay", "wrapper"]) +def test_live_and_final_progress_count_unique_processed_checks(statistics_settings, profile): + statistics_settings.profile_identifier = profile + monitor = _ProgressTrace(statistics_settings) + result = services.validate(statistics_settings, subscribers=[monitor]) + final_monitor = ProgressMonitor(statistics_settings, result.statistics) + + assert all(completed <= total for snapshot in monitor.snapshots for completed, total in snapshot) + assert [(task.completed, task.total) for task in monitor.progress.tasks] == [ + (task.completed, task.total) for task in final_monitor.progress.tasks + ] + assert all(task.completed == task.total for task in monitor.progress.tasks) + + +@pytest.mark.parametrize("effective_identifier", [False, True]) +def test_configured_skip_is_included_in_totals_and_preserved(statistics_settings, effective_identifier): + context = ValidationContext(Validator(statistics_settings), statistics_settings) + source = next(profile for profile in context.profiles if profile.identifier == "base") + check = source.get_requirement_check("Required name") + statistics_settings.skip_checks = [ + context.effective_check_identifier(check) if effective_identifier else check.identifier + ] + result = services.validate(statistics_settings) + statistics = result.statistics + + assert {detail.check.name for detail in result.skipped_check_details} == {"Required name", "Disabled"} + assert statistics.total_skipped_checks == result.skipped_checks_count == 2 + assert set(statistics.validated_checks) | set(statistics.skipped_checks) == statistics.checks + + +def test_batched_target_failures_are_counted(statistics_settings): + del statistics_settings.metadata_dict["@graph"][0]["description"] + result = services.validate(statistics_settings) + + assert not result.passed() + assert {check.name for check in result.statistics.failed_checks} == {"Required description"} + assert set(result.statistics.failed_checks) == result.failed_checks + assert all(result.get_check_result(check) is CheckResult.FAILED for check in result.statistics.failed_checks) + statistics = result.statistics + assert len(statistics.validated_checks) + statistics.total_skipped_checks == statistics.total_checks + + +@pytest.mark.parametrize("abort", [False, True]) +def test_deferred_checks_become_skipped_when_validation_stops(statistics_settings, monkeypatch, abort): + statistics_settings.abort_on_first = not abort + execute_check = SHACLCheck.execute_check + + def stop_at_target(check, context): + if check.requirement.profile.identifier == "overlay": + context.result.add_issue("Stop before the combined SHACL run", check) + if abort: + context.abort_validation("Input is unavailable") + return False + return execute_check(check, context) + + def unexpected_batch(*args, **kwargs): + pytest.fail("SHACL must not run after validation has stopped") + + monkeypatch.setattr(SHACLCheck, "execute_check", stop_at_target) + monkeypatch.setattr(SHACLValidator, "validate", unexpected_batch) + trace = _StatisticsTrace() + result = services.validate(statistics_settings, subscribers=[trace]) + statistics = result.statistics + + assert not result.passed() + assert trace.skipped_counts == sorted(trace.skipped_counts) + assert { + detail.check.name for detail in result.skipped_check_details if detail.category is SkipCategory.NOT_REACHED + } == {"Check Name", "Required name", "Required description"} + assert not statistics.passed_checks + assert statistics.total_skipped_checks == result.skipped_checks_count == 4 + assert set(statistics.validated_checks) | set(statistics.skipped_checks) == statistics.checks + assert result.to_dict()["skipped_checks"] == len(result.to_dict()["skipped_check_details"]) == 4 + + +@pytest.mark.parametrize("missing_property", ["name", "description"]) +def test_fail_fast_batch_keeps_failed_and_skipped_counts_disjoint(statistics_settings, missing_property): + statistics_settings.abort_on_first = True + del statistics_settings.metadata_dict["@graph"][0][missing_property] + result = services.validate(statistics_settings) + statistics = result.statistics + + assert not result.passed() + assert len(statistics.failed_checks) == 1 + assert set(statistics.failed_checks) == result.failed_checks + assert set(statistics.validated_checks).isdisjoint(statistics.skipped_checks) + assert statistics.total_skipped_checks == result.skipped_checks_count + assert set(statistics.validated_checks) | set(statistics.skipped_checks) == statistics.checks + + +def test_suppressed_inherited_checks_do_not_inflate_progress(statistics_settings): + profile_path = statistics_settings.profiles_path / "overlay" / "profile.ttl" + profile_path.write_text( + profile_path.read_text(encoding="utf-8").replace("validator:isRuleOverlayOf ;", ""), + encoding="utf-8", + ) + statistics_settings.disable_inherited_profiles_issue_reporting = True + monitor = _ProgressTrace(statistics_settings) + result = services.validate(statistics_settings, subscribers=[monitor]) + + assert result.passed() + assert result.statistics.total_checks == 2 + assert result.statistics.total_skipped_checks == result.skipped_checks_count == 0 + assert all(task.completed == task.total for task in monitor.progress.tasks) + assert all(completed <= total for snapshot in monitor.snapshots for completed, total in snapshot) From c0682cbafa3d6180fb9e042ff6aa5727aefbb290 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 30 Sep 2026 10:25:07 +0200 Subject: [PATCH 144/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.12.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 115 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a3226d1..3eb0346fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,62 +7,123 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.12.0] - 2026-09-30 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.11.4...0.12.0 + ### ⚠️ Breaking Changes - **The default validation profile is now `ro-crate-1.3`** (previously `ro-crate-1.1`). Since 1.3 is self-contained, validation now applies 95 requirements instead of 30; crates declaring the - 1.1 context fail the `ro-crate-1.3_2.2` check under the default profile. -- This primarily affects API users when `profile_identifier` is omitted. CLI auto-detection is - unchanged unless no profile is detected or `--no-auto-profile` is used. -- To keep the previous behaviour, explicitly select `ro-crate-1.1` with `profile_identifier` or - `-p ro-crate-1.1`. + 1.1 context fail the `ro-crate-1.3_2.2` check under the default profile. This primarily affects API + users when `profile_identifier` is omitted. CLI auto-detection is unchanged unless no profile is + detected or `--no-auto-profile` is used. To keep the previous behaviour, explicitly select + `ro-crate-1.1` with `profile_identifier` or `-p ro-crate-1.1` + ([b6199c60](https://github.com/kikkomep/rocrate-validator/commit/b6199c608df4b1d20e987a4946d06005b04fe795)). ### ✨ Added - feat(profiles): support rule overlays through `validator:isRuleOverlayOf`, allowing a target profile to reuse checks from a direct parent while retaining source provenance and reporting target-relative effective identities. Overrides defined by the target profile match parent checks by - `(name, severity)`. + `(name, severity)` ([e92e826b](https://github.com/kikkomep/rocrate-validator/commit/e92e826bd2b2e49b36775f5150c81f50089439fb)). - feat(profile-checks): add an extensible profile consistency framework that detects duplicate check - identities, invalid overlay relationships, and ambiguous checks inherited from multiple sources. + identities, invalid overlay relationships, and ambiguous checks inherited from multiple sources + ([59a66303](https://github.com/kikkomep/rocrate-validator/commit/59a6630320b4303b0ef6c22020371dafb1b01891), + [99748956](https://github.com/kikkomep/rocrate-validator/commit/997489564c1065df9fc71f36844271bfffe5d4a2)). - feat(cli): add `profiles check ` and automatic profile validation, with - `--no-profile-checks` and the corresponding API setting available as explicit opt-outs. + `--no-profile-checks` and the corresponding API setting available as explicit opt-outs + ([a4eb3fd2](https://github.com/kikkomep/rocrate-validator/commit/a4eb3fd2868b91a511bb993b73910ad0b36225de), + [f430b2b9](https://github.com/kikkomep/rocrate-validator/commit/f430b2b95604be431956e4cdc5f08229a853722a)). - feat(api): expose effective provenance for checks defined by the target profile, inherited checks, - and replacement checks in validation events, issues, statistics, and skipped-check results. + and replacement checks in validation events, issues, statistics, and skipped-check results + ([a7fabdb4](https://github.com/kikkomep/rocrate-validator/commit/a7fabdb47b2d974894039fb80b4730b65a9d4292)). - feat(cli): show source and effective check identities and their provenance relationships in verbose - `profiles describe` output. + `profiles describe` output ([b4108c16](https://github.com/kikkomep/rocrate-validator/commit/b4108c160a0eb51a34e0d91e7b16d91520e477fc)). +- feat(profiles): provide comprehensive RO-Crate 1.3 validation coverage for metadata descriptors, + entities, workflows, and detached or referenced crates + ([5a98e4e0](https://github.com/kikkomep/rocrate-validator/commit/5a98e4e02ce60bd5e61e3be943ceea8bba166e25), + [e2bc9846](https://github.com/kikkomep/rocrate-validator/commit/e2bc984662408160535c25a803300c905c58216e), + [41cd9fc3](https://github.com/kikkomep/rocrate-validator/commit/41cd9fc3be279ad8c15effd6d891f4770cf2dafb), + [bc2f76e4](https://github.com/kikkomep/rocrate-validator/commit/bc2f76e46d1e047a5c36a09e417d59146143b17a)). +- feat(validation): support tri-state check outcomes and declared dependencies between checks, and + propagate dependency outcomes through validation + ([80495af4](https://github.com/kikkomep/rocrate-validator/commit/80495af484f6a60c2f7fa6fae389c52b8c9dfa3a), + [77c4741b](https://github.com/kikkomep/rocrate-validator/commit/77c4741bf8e1cdb28627906ed7bd8c2611708e7f), + [7a5a1749](https://github.com/kikkomep/rocrate-validator/commit/7a5a17497426e5cd86eb219ca5ca2c5df2fe53ec)). +- feat(cli): report structured skipped-check reasons in validation results and aggregate them in JSON + output ([c3044802](https://github.com/kikkomep/rocrate-validator/commit/c30448029d979b6af44d0ccf67831e8076e2eaf4), + [035e5485](https://github.com/kikkomep/rocrate-validator/commit/035e5485c7f439c927bd1709ad6f5b83b04b87ca), + [892460ec](https://github.com/kikkomep/rocrate-validator/commit/892460ece97b738a6c9d1c4ee093983cc5d69df2), + [70c47ab5](https://github.com/kikkomep/rocrate-validator/commit/70c47ab5a0b4c93ed326e57ad0d06a24121d4c8c)). +- feat(graph): add an extensible graph-transformer pipeline for preparing validation graphs and + selecting candidate entities from SHACL shapes + ([80531e0a](https://github.com/kikkomep/rocrate-validator/commit/80531e0add141bd6a439eef73a298138faceeebd), + [74aca87e](https://github.com/kikkomep/rocrate-validator/commit/74aca87ed604be2dd1f4c0c3df225f645c35ed9b), + [3a8d0cf0](https://github.com/kikkomep/rocrate-validator/commit/3a8d0cf054e21ed2cef2863dca62496988aa12b5), + [822c26be](https://github.com/kikkomep/rocrate-validator/commit/822c26be8073baf015ed5e09e8759b9aaea128e6)). +- feat(profiles): validate Schema.org IRI protocols with SHACL + ([7f6e8032](https://github.com/kikkomep/rocrate-validator/commit/7f6e8032a8cc2354dfb3a49f35eba81c091c0828)). +- feat(validation): propagate check execution errors and render them safely in the CLI + ([f2e7f36a](https://github.com/kikkomep/rocrate-validator/commit/f2e7f36a9a4fa2b7d77341e3e075bfbd8199aada), + [48bf2968](https://github.com/kikkomep/rocrate-validator/commit/48bf296844d3868e52c161d300c7d7c11a4ea01f)). ### 🔧 Changed -- refactor(profiles): resolve bare profile tokens to the highest available version using numeric - version ordering. -- refactor(validation): store the resolved profile identifier in validation settings so downstream - consumers use the profile that was actually applied. -- refactor(validation): preserve general-to-specific profile traversal for overlays, execute Python - replacements in their source check's slot, and evaluate the collected SHACL shapes once at the - target profile. +- refactor(validation): resolve bare profile tokens to the highest available version using numeric + ordering and store the resolved identifier so downstream consumers use the applied profile + ([7b782975](https://github.com/kikkomep/rocrate-validator/commit/7b7829750bac3a6b28293310d6f5de4b71c2ae0b)). - refactor(cli): use consistent styling for profile names, identifiers, check names, and provenance - labels across profile commands. + labels across profile commands ([40e7fe4e](https://github.com/kikkomep/rocrate-validator/commit/40e7fe4e07cdef914f99e41434158cf271c98ad3), + [2316a430](https://github.com/kikkomep/rocrate-validator/commit/2316a430f9c4393597af794754c20bdb0c27b086)). +- refactor(shacl): separate graph-transformer implementations from the SHACL validation core + ([21590fe8](https://github.com/kikkomep/rocrate-validator/commit/21590fe88be1511195c8d3b9051a4244a5f33bed), + [a46eb34f](https://github.com/kikkomep/rocrate-validator/commit/a46eb34ff08c562fbc5d3a4109cf6809eea1338e)). +- refactor(profile): declare RO-Crate 1.3 as a rule overlay over RO-Crate 1.2 and rename the overlay + property to `isRuleOverlayOf` + ([9dd09229](https://github.com/kikkomep/rocrate-validator/commit/9dd09229f1e4e1e385dbc95e5cf7d3322f393cc2), + [82df8dca](https://github.com/kikkomep/rocrate-validator/commit/82df8dcae7b280774f4d83bdba71336a0d19b5dc)). ### 🐛 Fixed -- fix(validation): resolve the overlay target from `profile_identifier` and preserve inherited check - dependencies and effective skip settings independently of profile loading order. -- fix(shacl): load each overlay source graph once before validating the merged shapes graph at the - target profile. +- fix(validation): preserve general-to-specific overlay traversal and source check execution order; + resolve the overlay target from `profile_identifier` and preserve inherited dependencies and skip + settings independently of profile loading order + ([a73f90da](https://github.com/kikkomep/rocrate-validator/commit/a73f90da12b10eca5681497d20cc9432f145245f), + [dbea5738](https://github.com/kikkomep/rocrate-validator/commit/dbea5738c6adfd56eca8ee1ed2595d0a78c75bba), + [996b52aa](https://github.com/kikkomep/rocrate-validator/commit/996b52aa561acb6c00f0c24c911f57bd409919b3), + [709e7564](https://github.com/kikkomep/rocrate-validator/commit/709e75641dfc2ed8115154d7ff15f0578d5d252a)). +- fix(shacl): load each overlay source graph once and evaluate the merged shapes graph at the target + profile ([22455d3b](https://github.com/kikkomep/rocrate-validator/commit/22455d3be43fc6425ddb76161d8d67d7ed53b660)). - fix(profiles): reject ambiguous effective identities instead of implicitly selecting one inherited - check, and disambiguate legacy duplicate check names within existing profiles. + check, and disambiguate legacy duplicate check names within existing profiles + ([f00ea8c1](https://github.com/kikkomep/rocrate-validator/commit/f00ea8c11f73cd62f180814e52b11f72fac1ea7a), + [16709a69](https://github.com/kikkomep/rocrate-validator/commit/16709a69d6ac06ad22dc8e7a7e392b8afd875821)). +- fix(graph): treat `schema:additionalType` as a reference predicate + ([ce4856c9](https://github.com/kikkomep/rocrate-validator/commit/ce4856c93bf869370bc07530a120c87a2a91c6a5)). +- fix(validation): correct skipped-check counts, align check statistics with validation outcomes, and + keep batched SHACL results synchronized with live progress + ([0416446b](https://github.com/kikkomep/rocrate-validator/commit/0416446bade7776120f112ee58d64fdffd1b79e5), + [39f3f640](https://github.com/kikkomep/rocrate-validator/commit/39f3f6409973975ab7710cc1b83c6d8c6727b0c2), + [d4ab2cb8](https://github.com/kikkomep/rocrate-validator/commit/d4ab2cb84e45cbe5a57b642aa878ba0e943724d1)). ### 📚 Documentation - docs(profiles): document ordinary inheritance, check overrides, rule overlays, deactivation, and - source versus effective identities with concrete SHACL and Python examples. -- docs(api): document effective check provenance for composed profiles. + source versus effective identities with concrete SHACL and Python examples + ([3dd0b793](https://github.com/kikkomep/rocrate-validator/commit/3dd0b793296a699312d621c14c23f75e7f5a768f)). +- docs(api): document effective check provenance for composed profiles + ([12485e89](https://github.com/kikkomep/rocrate-validator/commit/12485e8984873e35672c0be805ba7a8d8252215d)). +- docs(graph): document the graph-transformer extension points and SHACL implementation catalog + ([782f74c4](https://github.com/kikkomep/rocrate-validator/commit/782f74c4d9e90919412ffc41fa50d6b86b5505bc), + [fc455598](https://github.com/kikkomep/rocrate-validator/commit/fc455598486d4288ce39f81abb82ea6da0813ed7)). ### ⚡ Performance -- perf(profiles): cache profile consistency results and effective check provenance within a validation - session to avoid repeated resolution work. +- perf(validation): cache profile consistency results and effective check provenance within a + validation session to avoid repeated resolution work + ([c0b65cad](https://github.com/kikkomep/rocrate-validator/commit/c0b65cad72434db97e3cf56405b57a65e1462e80), + [8df06f12](https://github.com/kikkomep/rocrate-validator/commit/8df06f12df83a2c3e62fce1b96fc879c5587ecfe)). ## [0.11.4] - 2026-09-16 From 4e0bf575109214d6f49e115fd2088d2c2c9c1a99 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 30 Sep 2026 10:25:49 +0200 Subject: [PATCH 145/271] =?UTF-8?q?chore(deps):=20=F0=9F=94=A7=20update=20?= =?UTF-8?q?dependencies=20in=20poetry.lock=20(v0.12.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 1148 ++++++++++++++++++++++++++------------------------- 1 file changed, 587 insertions(+), 561 deletions(-) diff --git a/poetry.lock b/poetry.lock index e8302f3ac..59ad20b46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -269,14 +269,14 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "cattrs" -version = "26.2.0" +version = "26.2.1" description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cattrs-26.2.0-py3-none-any.whl", hash = "sha256:c680f17cd1df3a1c038ad1db8d90a9c595568cc54c4e8098e7ceb8d7b9bd826b"}, - {file = "cattrs-26.2.0.tar.gz", hash = "sha256:3cf49f69df8326bcf17a3cb3d3d3ec4a856858fe3a7473746c9044c317d3ba55"}, + {file = "cattrs-26.2.1-py3-none-any.whl", hash = "sha256:a12aaa3453dc8f633a815293179f08b7421ed18d2575c459c3c736f840beac24"}, + {file = "cattrs-26.2.1.tar.gz", hash = "sha256:679132bfdc225c5ee40c024fc42519954767c387f950dc6751946c586bccdc6d"}, ] [package.dependencies] @@ -435,184 +435,184 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.5.1" +version = "3.5.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, - {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, - {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, - {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, - {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, - {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, - {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, - {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, - {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, - {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, - {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, - {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, - {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-win32.whl", hash = "sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-win32.whl", hash = "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-win32.whl", hash = "sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-win_arm64.whl", hash = "sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc"}, + {file = "charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685"}, + {file = "charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef"}, ] [[package]] @@ -675,133 +675,133 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.16.1" +version = "7.16.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853"}, - {file = "coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097"}, - {file = "coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a"}, - {file = "coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881"}, - {file = "coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43"}, - {file = "coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b"}, - {file = "coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec"}, - {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9"}, - {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38"}, - {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a"}, - {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594"}, - {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773"}, - {file = "coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc"}, - {file = "coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90"}, - {file = "coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da"}, - {file = "coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07"}, - {file = "coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5"}, - {file = "coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475"}, - {file = "coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567"}, - {file = "coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e"}, - {file = "coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d"}, - {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b"}, - {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf"}, - {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01"}, - {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08"}, - {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572"}, - {file = "coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2"}, - {file = "coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7"}, - {file = "coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22"}, - {file = "coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af"}, - {file = "coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c"}, - {file = "coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809"}, - {file = "coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf"}, - {file = "coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f"}, - {file = "coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360"}, - {file = "coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08"}, - {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941"}, - {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489"}, - {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776"}, - {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6"}, - {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0"}, - {file = "coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d"}, - {file = "coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e"}, - {file = "coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8"}, - {file = "coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9"}, - {file = "coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245"}, - {file = "coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5"}, - {file = "coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5"}, - {file = "coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f"}, - {file = "coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41"}, - {file = "coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878"}, - {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2"}, - {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080"}, - {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037"}, - {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b"}, - {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0"}, - {file = "coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a"}, - {file = "coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa"}, - {file = "coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d"}, - {file = "coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15"}, - {file = "coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57"}, - {file = "coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3"}, - {file = "coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc"}, - {file = "coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751"}, - {file = "coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab"}, - {file = "coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9"}, - {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412"}, - {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a"}, - {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540"}, - {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9"}, - {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361"}, - {file = "coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b"}, - {file = "coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b"}, - {file = "coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f"}, - {file = "coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152"}, - {file = "coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb"}, - {file = "coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741"}, - {file = "coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1"}, - {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72"}, - {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8"}, - {file = "coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c"}, - {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a"}, - {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da"}, - {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2"}, - {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e"}, - {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae"}, - {file = "coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2"}, - {file = "coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d"}, - {file = "coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6"}, - {file = "coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743"}, - {file = "coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e"}, - {file = "coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae"}, - {file = "coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370"}, - {file = "coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae"}, - {file = "coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de"}, - {file = "coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1"}, - {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c"}, - {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6"}, - {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f"}, - {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df"}, - {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040"}, - {file = "coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827"}, - {file = "coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d"}, - {file = "coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855"}, - {file = "coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6"}, - {file = "coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d"}, - {file = "coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6"}, - {file = "coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1"}, - {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1"}, - {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b"}, - {file = "coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de"}, - {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef"}, - {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876"}, - {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8"}, - {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220"}, - {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0"}, - {file = "coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5"}, - {file = "coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db"}, - {file = "coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0"}, - {file = "coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4"}, - {file = "coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210"}, + {file = "coverage-7.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23219888477edd736b6fcaec1272d47d93b926e999641ffea7e53a1738e70b2b"}, + {file = "coverage-7.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40c0f00899fe6181ae7f434ceb200e51f5ee4b8ed10e3b5f0b605f0cae15da87"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4624f80732f6b427ac58f1f59c577a0994a12e8174b5af6a027b4b58795d4c3"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:191803c4996b499fcd78c2ad5e5f767dcc53cb4dc6de6d6a741b443a1821ef02"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fd670ac43b709c575aefc25bf52d8a598a3bc5017bddfd0a179152ab06a2deb"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:705e5af11d34647efdc170c7840b6857c81cf74be96419a553f237e68e62cb72"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8afd9bf35cc6a1f22eb3634808fa8e0b91902459c5721ef2e4461dfe771d7f08"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f43bac1856ba269b905302778d4df433d6006489a192174ad77ac528e395032"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f8475460aa33ee28ac896ab1156d0bb3b6c639f7f8383c2677d3359eb35f8205"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d6276d78f6fca7d0ac066d5da4165c5acd07829e8305c2cb900b738fb3a75a72"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:736fde09ea39646d11f8e3b76bd3425c075aa4dd45f24891970bb77c14ff20f5"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c85d54e7e8a2ca932fe8399301af9b8d5907ea2a455ffaff6e7d1208db83b943"}, + {file = "coverage-7.16.2-cp310-cp310-win32.whl", hash = "sha256:5139009b5efd2194fc168ee9362f0e191ba612ef5d29242f9269c22f9b8f80c7"}, + {file = "coverage-7.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:c3305c38a2fa21a4254f2ace7dd9ef5fc569c9a558b66e7017650b3d637fb95e"}, + {file = "coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d"}, + {file = "coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878"}, + {file = "coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516"}, + {file = "coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d"}, + {file = "coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a"}, + {file = "coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48"}, + {file = "coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206"}, + {file = "coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a"}, + {file = "coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162"}, + {file = "coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02"}, + {file = "coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9"}, + {file = "coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4"}, + {file = "coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865"}, + {file = "coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6"}, + {file = "coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79"}, + {file = "coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a"}, + {file = "coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6"}, + {file = "coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011"}, + {file = "coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b"}, + {file = "coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb"}, + {file = "coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf"}, + {file = "coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce"}, + {file = "coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46"}, + {file = "coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c"}, + {file = "coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91"}, + {file = "coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8"}, + {file = "coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63"}, + {file = "coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e"}, + {file = "coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae"}, + {file = "coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495"}, + {file = "coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75"}, + {file = "coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c"}, + {file = "coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35"}, + {file = "coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5"}, + {file = "coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8"}, + {file = "coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f"}, + {file = "coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa"}, ] [package.dependencies] @@ -1037,14 +1037,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.32.7" +version = "4.0.7" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52"}, - {file = "filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7"}, + {file = "filelock-4.0.7-py3-none-any.whl", hash = "sha256:a93c4d93269b339a6af4848342c7e940d0f9928ad95eff64764699e5f1bf8a6a"}, + {file = "filelock-4.0.7.tar.gz", hash = "sha256:da5915714a70b55d167fdc7e251ad91302b0a36816fb574dfafae8f4f2c9bb21"}, ] [[package]] @@ -1083,14 +1083,14 @@ files = [ [[package]] name = "identify" -version = "2.6.19" +version = "2.6.20" description = "File identification library for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"}, - {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"}, + {file = "identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa"}, + {file = "identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285"}, ] [package.extras] @@ -1098,14 +1098,14 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.19" +version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main", "docs"] files = [ - {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, - {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [package.extras] @@ -1465,150 +1465,150 @@ files = [ [[package]] name = "librt" -version = "0.15.0" +version = "0.16.0" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489"}, - {file = "librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52"}, - {file = "librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8"}, - {file = "librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1"}, - {file = "librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d"}, - {file = "librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702"}, - {file = "librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9"}, - {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b"}, - {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db"}, - {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451"}, - {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389"}, - {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c"}, - {file = "librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e"}, - {file = "librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053"}, - {file = "librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22"}, - {file = "librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63"}, - {file = "librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef"}, - {file = "librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f"}, - {file = "librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8"}, - {file = "librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a"}, - {file = "librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18"}, - {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c"}, - {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a"}, - {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091"}, - {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40"}, - {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0"}, - {file = "librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb"}, - {file = "librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db"}, - {file = "librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56"}, - {file = "librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e"}, - {file = "librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d"}, - {file = "librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1"}, - {file = "librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa"}, - {file = "librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd"}, - {file = "librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa"}, - {file = "librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d"}, - {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656"}, - {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81"}, - {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d"}, - {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0"}, - {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2"}, - {file = "librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1"}, - {file = "librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022"}, - {file = "librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570"}, - {file = "librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26"}, - {file = "librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801"}, - {file = "librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc"}, - {file = "librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95"}, - {file = "librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b"}, - {file = "librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2"}, - {file = "librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3"}, - {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785"}, - {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6"}, - {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101"}, - {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218"}, - {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b"}, - {file = "librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab"}, - {file = "librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890"}, - {file = "librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8"}, - {file = "librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad"}, - {file = "librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993"}, - {file = "librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8"}, - {file = "librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21"}, - {file = "librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953"}, - {file = "librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa"}, - {file = "librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879"}, - {file = "librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae"}, - {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd"}, - {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285"}, - {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239"}, - {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60"}, - {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65"}, - {file = "librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622"}, - {file = "librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15"}, - {file = "librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28"}, - {file = "librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95"}, - {file = "librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714"}, - {file = "librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3"}, - {file = "librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d"}, - {file = "librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38"}, - {file = "librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19"}, - {file = "librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab"}, - {file = "librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2"}, - {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108"}, - {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08"}, - {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47"}, - {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81"}, - {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc"}, - {file = "librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf"}, - {file = "librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915"}, - {file = "librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605"}, - {file = "librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca"}, - {file = "librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965"}, - {file = "librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad"}, - {file = "librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9"}, - {file = "librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328"}, - {file = "librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0"}, - {file = "librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9"}, - {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659"}, - {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0"}, - {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d"}, - {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c"}, - {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d"}, - {file = "librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374"}, - {file = "librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9"}, - {file = "librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8"}, - {file = "librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b"}, - {file = "librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54"}, - {file = "librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6"}, - {file = "librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988"}, - {file = "librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13"}, - {file = "librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416"}, - {file = "librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a"}, - {file = "librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c"}, - {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302"}, - {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d"}, - {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9"}, - {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab"}, - {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b"}, - {file = "librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162"}, - {file = "librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1"}, - {file = "librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc"}, - {file = "librt-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d"}, - {file = "librt-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959"}, - {file = "librt-0.15.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59"}, - {file = "librt-0.15.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1"}, - {file = "librt-0.15.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4"}, - {file = "librt-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55"}, - {file = "librt-0.15.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465"}, - {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d"}, - {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881"}, - {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738"}, - {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7"}, - {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb"}, - {file = "librt-0.15.0-cp39-cp39-win32.whl", hash = "sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455"}, - {file = "librt-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578"}, - {file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"}, + {file = "librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6"}, + {file = "librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262"}, + {file = "librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143"}, + {file = "librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba"}, + {file = "librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155"}, + {file = "librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18"}, + {file = "librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602"}, + {file = "librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96"}, + {file = "librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7"}, + {file = "librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d"}, + {file = "librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678"}, + {file = "librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742"}, + {file = "librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07"}, + {file = "librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b"}, + {file = "librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a"}, + {file = "librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940"}, + {file = "librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc"}, + {file = "librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a"}, + {file = "librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6"}, + {file = "librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e"}, + {file = "librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d"}, + {file = "librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4"}, + {file = "librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92"}, + {file = "librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91"}, + {file = "librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1"}, + {file = "librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667"}, + {file = "librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152"}, + {file = "librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839"}, + {file = "librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7"}, + {file = "librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41"}, + {file = "librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1"}, + {file = "librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7"}, + {file = "librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d"}, + {file = "librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b"}, + {file = "librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff"}, + {file = "librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d"}, + {file = "librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42"}, + {file = "librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a"}, + {file = "librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3"}, + {file = "librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971"}, + {file = "librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba"}, + {file = "librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9"}, + {file = "librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc"}, + {file = "librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866"}, + {file = "librt-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e1967e36ac4cae0c7e9615ad32e1a513cdacff79f9e8afb28bedc91caf48b4f3"}, + {file = "librt-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:20fe0bf9053885e21c62b3e091fb5e73e1c54d1daf2e70eb388d70763bcd4220"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a63610fa76524edfa605b5b259a603915c7a6e10e54f003e5503030506e81de"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:845a511b60ca43b9880dcc84a9784c891d6a2098c829130b320846c69c9c0c68"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d608f0bf3b8cbddd0067fe02cb8cab7d13e8eb9386b23a1843d4363044fd9e22"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df183721229ae51eef90108c115b43e98cb169b6155d34480338e5fc6616df00"}, + {file = "librt-0.16.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b6183e2e2e0ee00aac2ec07c7f7d151c97e85666b304f574b71cff0f9fccc4e"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:273d00be33792a15189331df10f1f1331621b043881e66c6c4377f7f776e1291"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aa9357a1b4d4fc787bb718a59cb1112c28c8a976d6bfa268b71cc0a4ab8f3a94"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e9ce0bc440e7fd09b5f51f372f0f6640658f854b8fb05260f819cc669c93c42d"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:6c8893eae2fd13c5488d94056f3e6e5cf3142bfb1c4acaf136cb33d760c5964b"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7393c9a48dcce4817dbd4b0d8ff6237efe9b0a0609f5b0adaef315f8541726b5"}, + {file = "librt-0.16.0-cp39-cp39-win32.whl", hash = "sha256:71b93b42784e25b975079573c642a8fedb049a7bb31d70a51721b1666b3b2ced"}, + {file = "librt-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:5750a105b42a416f930edc59054927a406effb2550cd5bab92ad7a5842ed5d05"}, + {file = "librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc"}, ] [[package]] @@ -1814,100 +1814,103 @@ typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "msgpack" -version = "1.2.2" +version = "1.2.3" description = "MessagePack serializer" optional = false python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "msgpack-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970"}, - {file = "msgpack-1.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90"}, - {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839"}, - {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3"}, - {file = "msgpack-1.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77"}, - {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9"}, - {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c"}, - {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648"}, - {file = "msgpack-1.2.2-cp310-cp310-win32.whl", hash = "sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563"}, - {file = "msgpack-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec"}, - {file = "msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e"}, - {file = "msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683"}, - {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7"}, - {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba"}, - {file = "msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949"}, - {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761"}, - {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb"}, - {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f"}, - {file = "msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b"}, - {file = "msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c"}, - {file = "msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35"}, - {file = "msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc"}, - {file = "msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3"}, - {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba"}, - {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923"}, - {file = "msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f"}, - {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367"}, - {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc"}, - {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3"}, - {file = "msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4"}, - {file = "msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3"}, - {file = "msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640"}, - {file = "msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533"}, - {file = "msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34"}, - {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc"}, - {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9"}, - {file = "msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b"}, - {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b"}, - {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5"}, - {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab"}, - {file = "msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd"}, - {file = "msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f"}, - {file = "msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896"}, - {file = "msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04"}, - {file = "msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77"}, - {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe"}, - {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f"}, - {file = "msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea"}, - {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b"}, - {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5"}, - {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54"}, - {file = "msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248"}, - {file = "msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc"}, - {file = "msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8"}, - {file = "msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650"}, - {file = "msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c"}, - {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3"}, - {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3"}, - {file = "msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22"}, - {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839"}, - {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929"}, - {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7"}, - {file = "msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e"}, - {file = "msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36"}, - {file = "msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f"}, - {file = "msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5"}, - {file = "msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986"}, - {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516"}, - {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21"}, - {file = "msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9"}, - {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a"}, - {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5"}, - {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b"}, - {file = "msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf"}, - {file = "msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff"}, - {file = "msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808"}, - {file = "msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8"}, - {file = "msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84"}, - {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b"}, - {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782"}, - {file = "msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f"}, - {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695"}, - {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23"}, - {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212"}, - {file = "msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc"}, - {file = "msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d"}, - {file = "msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754"}, - {file = "msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a"}, + {file = "msgpack-1.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec0030361cc861ac699b2ef1c695b741fa145c88f8667fa3d7e3f73deeb648a3"}, + {file = "msgpack-1.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c1efdd9181cb1b719ee46865f368a927f1c0c65d577798340b1194545b7515a"}, + {file = "msgpack-1.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c309a7abae1d14ba29a8bd0ddbd704a5e469d8e9bd9c3dee0e4ff53d7ae01d56"}, + {file = "msgpack-1.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bf390259cb25a6a1cd197c65810999b811f64cd38683251538bcc5a1e41f7d3"}, + {file = "msgpack-1.2.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39b6986c19e1f2dfa549d185dba6ccf1de2e4c0ba10d8cfc0048935b1c5f9109"}, + {file = "msgpack-1.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcc6800daac4922960f6eeb7a0dda3dd4105e0bf7bce0e83ebc465a78cb7bdba"}, + {file = "msgpack-1.2.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:968583e956d0427878050b371308c5f8647088732ef3e66a117dbe1192ec91e0"}, + {file = "msgpack-1.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1d6bcec3dbbdb89ca385d3a73e63ceae7b841fa0d7ca7c676f1a7bfe7fb2cdb8"}, + {file = "msgpack-1.2.3-cp310-cp310-win32.whl", hash = "sha256:a6b63917d60d6df451f328bd6afba8565e33c4afe1f62ec4ad758b78731c827b"}, + {file = "msgpack-1.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:4c0780095871ecc49a58b2ff6b1b43b25214704da67646557ca287a3f49fb2dd"}, + {file = "msgpack-1.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec90a9ae3e1169fa1171147340f0e97d941aa19fcd3b34e8339a55933ed042af"}, + {file = "msgpack-1.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9d7e9cbb0998bbfd363fd9a09c330520d5e9cb323c05b5a1a05865d23ccf2226"}, + {file = "msgpack-1.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6707d2fa2aa1bb5424ea0b05f44ffc989b15ab41a73ff5855bff4944fec7c8ac"}, + {file = "msgpack-1.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382b219de3d436de3baba0f4b0c6d4336e8f5858d0eb047918b13b69a71c6c55"}, + {file = "msgpack-1.2.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:186e6c602b8a9968b8e864c67d622a69279f7d1e55ae25f40e3bff7e815b2b62"}, + {file = "msgpack-1.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9276ba88891338f2617044429dfd080ae008c9868a25f6f1a7d004a35dc9ac0a"}, + {file = "msgpack-1.2.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c942c21a93f36b3a69e828c8945bb72c94dc2ffe488a2086950c812f3edf046c"}, + {file = "msgpack-1.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18a6ed513023001b28dcd3ba54966f6bb90a38274ba8d2640464bcab3a1b81d4"}, + {file = "msgpack-1.2.3-cp311-cp311-win32.whl", hash = "sha256:d0238cd05dec9ffbe0de1071df685ba63e30a36ac155285b1a094e727c38cbe9"}, + {file = "msgpack-1.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:30e1522e4173230dca4d9ad896f038f73c0da6c1edd42f4dbad88ac583cf5d46"}, + {file = "msgpack-1.2.3-cp311-cp311-win_arm64.whl", hash = "sha256:8ca67f77938ea6a3663aa9bd22b3e031f6da84d665be850abab910ee90728dfd"}, + {file = "msgpack-1.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89c930aece4e972b208ba589c8410b4167b05e411a5ea2cb25fd96f8bc47ee43"}, + {file = "msgpack-1.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:905a189853d6bdb204c7ae5f4ab77fb857448abfff574d3d93c62e2815b24b4f"}, + {file = "msgpack-1.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7b3d0018746b5997dd6b14a1870b07cc4c327d9101145d94a1fc264a51a06"}, + {file = "msgpack-1.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede33b2892ceb976283e009ad12fa1834cfdf1f9c43ee9c97849fc588d00a618"}, + {file = "msgpack-1.2.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:666ef5601ab0e6e345e47febc96aa81143cc932201543480cbb9499164f05ffb"}, + {file = "msgpack-1.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87cf2ef05ff2f2493ba29fcdaef27e960ca64dacfd13460ae29e6f92e0ed05bb"}, + {file = "msgpack-1.2.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b774ff994d844e541439ac5d2d49a14def4104830c3465e9394c153f86200ffb"}, + {file = "msgpack-1.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaf7e82249837e3aa97297b34a0bb9ff562027381631e057cea6e1367f10b438"}, + {file = "msgpack-1.2.3-cp312-cp312-win32.whl", hash = "sha256:7c047250096f9fc19dba26e3d1639b5e7a84114003605c94def667149a70ced1"}, + {file = "msgpack-1.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:3ec409b0d6aa8e9eec6eaf881b893caa215dbe68c5319ca96e8a271d81bb111d"}, + {file = "msgpack-1.2.3-cp312-cp312-win_arm64.whl", hash = "sha256:59612b4ed48a04cf024584218e813562f3b30a3bafa5f55abe300b15da314751"}, + {file = "msgpack-1.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:21bfa4d2aa0b04c1806ef778a1199e9e53ea2441bcbf284420a32083896320b8"}, + {file = "msgpack-1.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db84203b13aecc222f465061397fdd5b53b7ae73d2c95ffc1c8dc5be0153a709"}, + {file = "msgpack-1.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e0d7950ca3c1bbae291d0552dd3bb2792fc680629c4c0d44e47e5bab969f3ca"}, + {file = "msgpack-1.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07c9733089d1b176c3dd2f7fa268452f9d5d784d076473499d754a58e8d1fbbb"}, + {file = "msgpack-1.2.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f24a43b3560e20f825b807fe1e874bd73d53abaf8bbdcf258a6eb152cddbc1f5"}, + {file = "msgpack-1.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6576f348ed6cc4f31db6fd915a8e94245f042f50eae08d48732425e70638ea37"}, + {file = "msgpack-1.2.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd5a9f9f86a52c24713679aa2631956835f3842512964ff93f736ff76f1f530d"}, + {file = "msgpack-1.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9ddd28d3e9bbc602a9dced1591882c7fb9ab776eef8837da2c326fde19e2853"}, + {file = "msgpack-1.2.3-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:62cc1a4ef0e553bac32c8342e1f04834aca7de276b92744eb7307db77759b890"}, + {file = "msgpack-1.2.3-cp313-cp313-win32.whl", hash = "sha256:d2f9c4f85e47a44d26d5baf3b041eef23436e224d44eed273f01bd8a12048d9f"}, + {file = "msgpack-1.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb89b5dc30469c84bbf8684826eb851d82412ca95690e111b9ac5e8fb343961a"}, + {file = "msgpack-1.2.3-cp313-cp313-win_arm64.whl", hash = "sha256:471e12a6a42498a31490c206e0069e343b6a7c35db540be73a879eb06f5be047"}, + {file = "msgpack-1.2.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3a31905206722103a84c1f72633fe30692cff6732c9d262e09a27dbc468797c8"}, + {file = "msgpack-1.2.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3372475211a9ce1a23acefe512cb3e121d18c95dc74ed56cb1819ef40836ebf4"}, + {file = "msgpack-1.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9324c54995641c3d1f92a9d55093c8cde0ffa2fbc87a467a688ef60428393220"}, + {file = "msgpack-1.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8ef3a66e4b52d2d7fdd90df2984670124b2ff7546d76bb25dcf68ef47f7df58"}, + {file = "msgpack-1.2.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:902f3490db0e07a7d40b48536a85c9b28fbf1397e7e1658a45a55f958e303620"}, + {file = "msgpack-1.2.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e51eca14fbb65c4e0a5a9657346962bd3dca78c08e04e3d4dee70ef48687d30"}, + {file = "msgpack-1.2.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f42f146752eedb6765f07dcc04d72dab0a25779ec8d4a88c0085263ce114f22c"}, + {file = "msgpack-1.2.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ed5823c4efc20fe87d3530665f40ec18a002be003114814c21235cc8d256207"}, + {file = "msgpack-1.2.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:2487453ca1b6104442c6442f9a1a8fee1fe8f428a70d99d4cba799108b304150"}, + {file = "msgpack-1.2.3-cp314-cp314-win32.whl", hash = "sha256:6df430419f2338cb71e4a34d6e64f83c88ccd321f91f40ba4513400b36d864ec"}, + {file = "msgpack-1.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:84a6616d396ec1bc18a1e83e67c96a393ec35dfe5e17434a5be7b9aa0fe988ab"}, + {file = "msgpack-1.2.3-cp314-cp314-win_arm64.whl", hash = "sha256:7a003b02c6ee2eea6dfe0bb08818631e3597e69f0131f2a8250488a1cc553290"}, + {file = "msgpack-1.2.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ccea05b5542f6d283fef3f0a8e93a7f0be90af0ddeeef84c25c0216ba76dcae1"}, + {file = "msgpack-1.2.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1631e12fe572e181cd77e831f69335d6cd5278eac22e3db3f33cf264ac2ac18"}, + {file = "msgpack-1.2.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e54394b7dbe2e12ab032d9d21feef7bb61a90a150a2623633ba3781ba69dcb1f"}, + {file = "msgpack-1.2.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63bb7448a1e9111319ae2430c09a5596140c160422830d6271bc75730ff2ff9a"}, + {file = "msgpack-1.2.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:382bc88fe90f29f5ac8a0b65c7046ff255356f2f2f3186c30e370215736fa1dc"}, + {file = "msgpack-1.2.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c77e27790ad72989db783d5303825fba0b71550f00a490efba35cde7dc4b719f"}, + {file = "msgpack-1.2.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:700bc0fc9e968a292b9137ee70e7a012f7e115bf0107ce45e3a88202788dfc1e"}, + {file = "msgpack-1.2.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bd5f91ea75c45cafcc5433ba8fae59b708b736ec178d2441c40c499e9e079db"}, + {file = "msgpack-1.2.3-cp314-cp314t-win32.whl", hash = "sha256:7995a7c6a62a1d6e7df211b4a16de513bd99fd053525050a319f80f44fb8015e"}, + {file = "msgpack-1.2.3-cp314-cp314t-win_amd64.whl", hash = "sha256:bfe7d5b62cbe7aa664f0b3e2c49077f10fcdd06183d3014f8271ff3c5edbfbf9"}, + {file = "msgpack-1.2.3-cp314-cp314t-win_arm64.whl", hash = "sha256:1f585407f740a9eac04a3bb82c61d68a0ea78f90e29e670bfb086b9ce3a518dd"}, + {file = "msgpack-1.2.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:13221a6c81ebb8e43ea63a7251c35d54e4175cea37ebf3a62e911bdf42562a3c"}, + {file = "msgpack-1.2.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0955b9000725573d1457c1676944b370dd9643c8d18f25bda5ac72913f850949"}, + {file = "msgpack-1.2.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c91762c48cd686dc9cf2b142c0bc544083952de32f5853d6624c956e54b85e5"}, + {file = "msgpack-1.2.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f4ae8bd4ad9ba085fde95e95d055a896d19210238a4199a771a3cf36dceed49"}, + {file = "msgpack-1.2.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7013534a7163aa4f213c4d9864f1a8a7555daac6fcd48f699a198e29b436bfab"}, + {file = "msgpack-1.2.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:6a834097144aabe948b8ca9020a833e8026f7d0abbd0ec54bc7e50f45a8ce012"}, + {file = "msgpack-1.2.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d31864ba3933a589b6a00249f89c0eb422197f49128fc10da550e57e9cb0f377"}, + {file = "msgpack-1.2.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e15f70588f4db8cd10df0930145b186de70feb9db51710cd378b1399009655bd"}, + {file = "msgpack-1.2.3-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:b949cc25e4a09252cbcc54e66e507de914d0e94a3a7039bd54c299bf7037c098"}, + {file = "msgpack-1.2.3-cp315-cp315-win32.whl", hash = "sha256:8ec7a1d49ca6c2569d722ab5ec86e90089b0713900aa31905b47b4c4d9e78ce0"}, + {file = "msgpack-1.2.3-cp315-cp315-win_amd64.whl", hash = "sha256:79dfa38faf92f804aa61beec140d70b18418e1dde1778dbb77a87a4cce85aa8a"}, + {file = "msgpack-1.2.3-cp315-cp315-win_arm64.whl", hash = "sha256:ed899d73a22f286a72bd9528d63f2ab3030dbad8bf1527fc249319a50d61fb9d"}, + {file = "msgpack-1.2.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f56fba61b2516be7917cb00151f0d060b5b21184e3499bb57f0f7d9259bea124"}, + {file = "msgpack-1.2.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:69ad12cedb674c73527bed869cddb42b742cac79a207a614202a4abaa24ea173"}, + {file = "msgpack-1.2.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db9fb67a3a2e75247bae569d34ebb5ff61c0448a4f0d6dbf991dae68af39b007"}, + {file = "msgpack-1.2.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2574ef81c1c8c38b10e330f3f9406fd09198a776b002030fafcf8e7647e9e06e"}, + {file = "msgpack-1.2.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fafc3b8898b432b841d30a61082c599fa7f4d06885f9dc58ad72259e12059fa6"}, + {file = "msgpack-1.2.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a393e428f6ffb0dcb73308c1fff5593041c16ff42da66e5bac8a83a6107a54b0"}, + {file = "msgpack-1.2.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d1c1e8989a855b7f1f2a64ec4a80b23a631822903952770813857b2e4f460471"}, + {file = "msgpack-1.2.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e0bd394e999949c814f7912284243298de1b5a17b6a3dcb6cc8a79b156ffc4fa"}, + {file = "msgpack-1.2.3-cp315-cp315t-win32.whl", hash = "sha256:3d4c807ed050fe3ddbea5ba7e9f63d7136871ce42861be1f50ff739f0e91047a"}, + {file = "msgpack-1.2.3-cp315-cp315t-win_amd64.whl", hash = "sha256:5f304123b90e8b2e49867981b7f6061612c39f50cca51ee88de007c084cf68d3"}, + {file = "msgpack-1.2.3-cp315-cp315t-win_arm64.whl", hash = "sha256:f41ca154b7737b11893cdce3c78c61d703398a1cd54d4297bdad908392338a8e"}, + {file = "msgpack-1.2.3.tar.gz", hash = "sha256:32edb81a2b5eb7cd7c9d941b2bfbbb082fd2cd09e0e725930316af6b708db186"}, ] [[package]] @@ -2161,16 +2164,19 @@ files = [ [[package]] name = "nodeenv" -version = "1.10.0" +version = "1.11.0" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] files = [ - {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, - {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, + {file = "nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25"}, + {file = "nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146"}, ] +[package.extras] +certifi = ["certifi"] + [[package]] name = "owlrl" version = "7.6.2" @@ -2279,14 +2285,14 @@ docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-pars [[package]] name = "platformdirs" -version = "4.11.8" +version = "4.12.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs"] files = [ - {file = "platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081"}, - {file = "platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc"}, + {file = "platformdirs-4.12.2-py3-none-any.whl", hash = "sha256:29dbf06d96c500bc6bdbce75fb0a14d63279c93b1842f97e72a135b33e856983"}, + {file = "platformdirs-4.12.2.tar.gz", hash = "sha256:eab5f70271a490ef74618bb314fbb86e3c7e82fa3b9c922c2ea0e0a1a155d329"}, ] [[package]] @@ -2480,14 +2486,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pyparsing" -version = "3.3.2" +version = "3.3.3" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, - {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, + {file = "pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4"}, + {file = "pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36"}, ] [package.extras] @@ -2604,14 +2610,14 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.6.0" +version = "1.6.1" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "python_discovery-1.6.0-py3-none-any.whl", hash = "sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a"}, - {file = "python_discovery-1.6.0.tar.gz", hash = "sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c"}, + {file = "python_discovery-1.6.1-py3-none-any.whl", hash = "sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3"}, + {file = "python_discovery-1.6.1.tar.gz", hash = "sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e"}, ] [package.dependencies] @@ -3284,14 +3290,14 @@ files = [ [[package]] name = "soupsieve" -version = "2.9.2" +version = "2.10" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823"}, - {file = "soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74"}, + {file = "soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502"}, + {file = "soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0"}, ] [[package]] @@ -3891,14 +3897,14 @@ markers = {test = "python_version == \"3.10\""} [[package]] name = "url-normalize" -version = "3.0.0" +version = "3.0.1" description = "URL normalization for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "url_normalize-3.0.0-py3-none-any.whl", hash = "sha256:95234bd359f86831c1fd87c248877f2a6887db2f3b5087120083f2fffcba4889"}, - {file = "url_normalize-3.0.0.tar.gz", hash = "sha256:0552cbf2831a32a28994a13d29bca58a60e10ff6c0380e343ec6d1c2a0d232d8"}, + {file = "url_normalize-3.0.1-py3-none-any.whl", hash = "sha256:97ea68fc543b1fc9f270f34c90cf164453e7d490da2ec653dcd8ebd4e3ac1faf"}, + {file = "url_normalize-3.0.1.tar.gz", hash = "sha256:1655cd214159d9d47dc37aa6ce993c2149da44fa35cac6bafd90036a4eda3ac3"}, ] [package.dependencies] @@ -3927,33 +3933,53 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" -version = "21.7.10" +version = "21.14.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.7.10-py3-none-any.whl", hash = "sha256:d7ac9669ba19e675ffadbb97fe8e887ef92fb1743fe9a0032be62b947657327a"}, - {file = "virtualenv-21.7.10.tar.gz", hash = "sha256:a7bf10f37ecc36f1942d6e469d6b59f5fe308f60ac711f66f51ef3bd8cb2c9aa"}, + {file = "virtualenv-21.14.1-py3-none-any.whl", hash = "sha256:6fd04089fc0dc33549e7abdff70fc3b63d4e15799f2dbf3281f80d13b9fce522"}, + {file = "virtualenv-21.14.1.tar.gz", hash = "sha256:719b189804e66678017d9f63bbfc590c44f6b96ab4829513806394f9b375929c"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} +filelock = {version = ">=3.24.2,<5", markers = "python_version >= \"3.10\""} +packaging = ">=23.1" platformdirs = ">=3.9.1,<5" python-discovery = ">=1.6" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [[package]] name = "wcwidth" -version = "0.8.3" +version = "0.9.1" description = "Measures the displayed width of unicode strings in a terminal" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4"}, - {file = "wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb"}, + {file = "wcwidth-0.9.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:10b00ba23482e352f874d2e8135e7ace9da838646c7dd800566246bbd46125ff"}, + {file = "wcwidth-0.9.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:40d936d72c9bdc10df43f93a8be502bc5024b487259139f66a328722c07f34a9"}, + {file = "wcwidth-0.9.1-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:69bb970cf5652b88cfdb9d3fdd1764fc15e5f8ad531643e7bb3e894cd969740e"}, + {file = "wcwidth-0.9.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b5da43d6967668982e44a52fb551967d293f86d26cd86036ac95bbdd34394ed9"}, + {file = "wcwidth-0.9.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:747fb724223f417a17541a95a17c1dac3a8ef9a0cf41684950f0eab191a35f65"}, + {file = "wcwidth-0.9.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6e1272b7986cefe79783737e38bdb9eaae0682b333c7bd132024441193dd5ce7"}, + {file = "wcwidth-0.9.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:fe021c4d8de9d36c31a0cb41d0d2546dadd3b0708a301f1a0c66ce200851831f"}, + {file = "wcwidth-0.9.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:708158c082364af442f9983de7b6ec9ac0d2e1b825ada25f0911b1f138d55405"}, + {file = "wcwidth-0.9.1-cp310-abi3-win32.whl", hash = "sha256:356376852357b8fca71fe5415808ec421679e04b4a98eb7c9cb6a7984b911a05"}, + {file = "wcwidth-0.9.1-cp310-abi3-win_amd64.whl", hash = "sha256:991d1c8834f548e9c1f16432075ee84638e122312556bbf1ed595ea8fffc4673"}, + {file = "wcwidth-0.9.1-cp310-abi3-win_arm64.whl", hash = "sha256:61bd7aef9cafb6cb77a37a169998d7928ce82a51522146d11f60db9e7d1cb43a"}, + {file = "wcwidth-0.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dc10e262c3ac0abbfd0a2a51e45a848b1b7f500b21ff512630277973ba25674d"}, + {file = "wcwidth-0.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bcb9ed4a367cc025bf1092ac679a156759605182d60b7ed3c28f7221a42ddf55"}, + {file = "wcwidth-0.9.1-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:0d68a30d504c68cfdff2a5f804675c1e7ab4c0bbe878024c8b680ec5579cde67"}, + {file = "wcwidth-0.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9f1636c5075ffd5c2e835b4561874f6e5dd2bbeba3c6c2c99f067d0d16883af8"}, + {file = "wcwidth-0.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c4cead196551112cb8f43cdd1f80c235ef2456e34b1a9955c424537ec99961b2"}, + {file = "wcwidth-0.9.1-cp314-cp314t-win32.whl", hash = "sha256:0665ee822ea04e25801e6e82e5407528be0863e88a17b0e7ca038843a4a3ac0c"}, + {file = "wcwidth-0.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eab587e18e7cadf1a750b0098fc8bebfb62c125eb9306f2268f0443a282a3d78"}, + {file = "wcwidth-0.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:03cfca3dcbffa86564290fe3c9978a6191ba003e8ced7f7dbda315fcb3fbe725"}, + {file = "wcwidth-0.9.1-py3-none-any.whl", hash = "sha256:e0c3a1c45c5b9550c6919a4449e95f5b177f6e165786376981db8f1addae9b21"}, + {file = "wcwidth-0.9.1.tar.gz", hash = "sha256:5823209b0d43af322ce698c689380d7c15ca31fa8e6e3be8459f27031bef0af5"}, ] [[package]] From 7a895d55ed69b1ca7dc43b3d5ae148aedfb40301 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 30 Sep 2026 10:26:25 +0200 Subject: [PATCH 146/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.12.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d6f41c305..9582b1b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.11.4" +version = "0.12.0" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 6c065b84709f9ec663675ac1c3f168c47cf798f7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 09:27:11 +0200 Subject: [PATCH 147/271] =?UTF-8?q?fix(validation):=20=F0=9F=A7=AD=20repor?= =?UTF-8?q?t=20missing=20hasPart=20Data=20Entities=20accurately?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/1.2/must/2_root_data_entity_haspart.ttl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl index 44adba7a4..0293ea371 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl @@ -24,13 +24,13 @@ ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; a sh:SPARQLConstraint ; sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ - SELECT $this ?unreferenced + SELECT $this ?value WHERE { $this a ro-crate:RootDataEntity . - ?unreferenced a ro-crate:DataEntity . - FILTER(?unreferenced != $this) + ?value a ro-crate:DataEntity . + FILTER(?value != $this) FILTER NOT EXISTS { - $this schema:hasPart+ ?unreferenced + $this schema:hasPart+ ?value } } """ ; From 865f39c729f44d09456ab605fa44702680395d26 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 09:31:53 +0200 Subject: [PATCH 148/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20profile=20Root=20Data=20Entity=20hasPart=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate-metadata.json | 67 +++++++++++++++++++ .../test_metadata_rootDataEntity_v1_3.py | 44 +++++++++++- tests/ro_crates_v1_3.py | 8 +++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json new file mode 100644 index 000000000..1942090cc --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json @@ -0,0 +1,67 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": [ + "Dataset", + "Profile" + ], + "name": "Test RO-Crate Profile", + "description": "A Profile Crate with contextual Dataset references outside hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "isBasedOn": { + "@id": "https://w3id.org/ro/crate/1.2" + }, + "citation": { + "@id": "https://doi.org/10.3233/DS-210053" + }, + "hasPart": [ + { + "@id": "https://example.org/profile-document" + } + ] + }, + { + "@id": "https://w3id.org/ro/crate/1.2", + "@type": "Dataset", + "name": "RO-Crate specification 1.2" + }, + { + "@id": "https://doi.org/10.3233/DS-210053", + "@type": "ScholarlyArticle", + "name": "Example citation", + "mainEntityOfPage": { + "@id": "https://example.org/referenced-dataset" + } + }, + { + "@id": "https://example.org/referenced-dataset", + "@type": "Dataset", + "name": "Referenced dataset" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "https://example.org/profile-document", + "@type": "File", + "name": "Profile document" + } + ] +} diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py index de4296703..5d2ecbe82 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py @@ -14,7 +14,7 @@ import logging -from rocrate_validator import models +from rocrate_validator import models, services from rocrate_validator.utils.http import HttpRequester from tests.ro_crates_v1_3 import RootDataEntity from tests.shared import do_entity_test @@ -385,6 +385,28 @@ def test_valid_required_hasPart_all_data_entities(): ) +def test_profile_root_contextual_datasets_are_reported_when_missing_from_hasPart(): + """Dataset references remain Data Entities even when they are contextual to a Profile.""" + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_root_data_entity_crates__.profile_root_contextual_datasets), + requirement_severity=models.Severity.REQUIRED, + profile_identifier="ro-crate-1.3", + ) + ) + + has_part_issues = [ + issue + for issue in result.get_issues(models.Severity.REQUIRED) + if issue.check.name == "Root Data Entity: hasPart MUST reference all Data Entities" + ] + + assert {str(issue.violatingPropertyValue) for issue in has_part_issues} == { + "https://w3id.org/ro/crate/1.2", + "https://example.org/referenced-dataset", + } + + def test_invalid_required_hasPart_all_data_entities(): """ Root Data Entity that does NOT reference all Data Entities via hasPart @@ -401,6 +423,26 @@ def test_invalid_required_hasPart_all_data_entities(): ) +def test_invalid_required_hasPart_reports_unreferenced_data_entity(): + """The hasPart check reports the missing Data Entity, not the root itself.""" + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_root_data_entity_crates__.invalid_required_hasPart_all_data_entities), + requirement_severity=models.Severity.REQUIRED, + profile_identifier="ro-crate-1.3", + ) + ) + + has_part_issues = [ + issue + for issue in result.get_issues(models.Severity.REQUIRED) + if issue.check.name == "Root Data Entity: hasPart MUST reference all Data Entities" + ] + + assert len(has_part_issues) == 1 + assert str(has_part_issues[0].violatingPropertyValue).endswith("/file2.txt") + + def test_invalid_hasPart_workflow_not_in_haspart(): """ A Workflow File entity (typed as File/SoftwareSourceCode/ComputationalWorkflow) diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index 4c393b79c..a893fe2ae 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -342,6 +342,14 @@ def invalid_recommended_datePublished_day_precision(self) -> Path: def valid_required_hasPart_all_data_entities(self) -> Path: return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "valid" + @property + def profile_root_contextual_datasets(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "required_haspart_all_data_entities" + / "profile_root_contextual_datasets" + ) + @property def invalid_required_hasPart_all_data_entities(self) -> Path: return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid" From def3ded1189e4861afea4af8f868fd64e7f714c3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 09:52:13 +0200 Subject: [PATCH 149/271] =?UTF-8?q?fix(validation):=20=F0=9F=A7=AD=20show?= =?UTF-8?q?=20overlay=20check=20IDs=20in=20text=20reports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io_helpers/output/text/formatters.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/utils/io_helpers/output/text/formatters.py b/rocrate_validator/utils/io_helpers/output/text/formatters.py index 748409370..a922f2173 100644 --- a/rocrate_validator/utils/io_helpers/output/text/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/text/formatters.py @@ -45,26 +45,38 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR # Print validation details # Print the list of failed requirements yield Padding("\n[bold]The following requirements have not been met: [/bold]", (0, 2)) + context = result.context for requirement in sorted(result.failed_requirements, key=lambda x: x.identifier): - yield Align(f"\n[profile: [magenta bold]{requirement.profile.name}[/magenta bold]]", align="right") + failed_checks = sorted( + result.get_failed_checks_by_requirement(requirement), + key=context.effective_check_identifier, + ) + if not failed_checks: + continue + + effective_profile = context.effective_check_profile(failed_checks[0]) + effective_requirement_identifier = context.effective_check_identifier(failed_checks[0]).rsplit(".", 1)[0] + + yield Align(f"\n[profile: [magenta bold]{effective_profile.name}[/magenta bold]]", align="right") yield Padding( - f"[bold][cyan][u][ {requirement.identifier} ]: {Markdown(requirement.name).markup}[/u][/cyan][/bold]", + f"[bold][cyan][u][ {effective_requirement_identifier} ]: " + f"{Markdown(requirement.name).markup}[/u][/cyan][/bold]", (0, 5), ) yield Padding(Markdown(requirement.description), (1, 6)) yield Padding("[white bold u] Failed checks [/white bold u]\n", (0, 8)) - for check in sorted( - result.get_failed_checks_by_requirement(requirement), key=lambda x: (-x.severity.value, x) - ): + failed_checks.sort(key=lambda check: (-check.severity.value, check)) + for check in failed_checks: issue_color = get_severity_color(check.level.severity) + effective_check_identifier = context.effective_check_identifier(check) yield Padding( - f"[bold][{issue_color}][ {check.identifier.center(16)} ][/{issue_color}] " + f"[bold][{issue_color}][ {effective_check_identifier.center(16)} ][/{issue_color}] " f"[hot_pink]{check.name}[/hot_pink][/bold]:", (1, 8, 0, 8), ) - yield Padding(Markdown(check.description), (0, 0, 0, len(check.identifier) + 13)) + yield Padding(Markdown(check.description), (0, 0, 0, len(effective_check_identifier) + 13)) yield Padding("[u] Detected issues [/u]", (0, 8)) for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): path = "" From c51cf5de4362f4d4da92b4e40a070aa7a17e683b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 09:59:08 +0200 Subject: [PATCH 150/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20veri?= =?UTF-8?q?fy=20effective=20overlay=20IDs=20in=20text=20reports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_validation_statistics.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/test_validation_statistics.py b/tests/unit/test_validation_statistics.py index 2fedb16e7..91476a9bc 100644 --- a/tests/unit/test_validation_statistics.py +++ b/tests/unit/test_validation_statistics.py @@ -15,12 +15,14 @@ from pathlib import Path import pytest +from rich.console import Console from rocrate_validator import services from rocrate_validator.events import EventType, Subscriber from rocrate_validator.models import CheckResult, SkipCategory, ValidationContext, ValidationSettings, Validator from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.validator import SHACLValidator +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter from rocrate_validator.utils.io_helpers.output.text.layout.progress import ProgressMonitor @@ -163,6 +165,24 @@ def test_batched_target_failures_are_counted(statistics_settings): assert len(statistics.validated_checks) + statistics.total_skipped_checks == statistics.total_checks +def test_text_report_uses_effective_overlay_check_identity(statistics_settings): + """Text reports identify inherited checks in the active overlay profile.""" + del statistics_settings.metadata_dict["@graph"][0]["name"] + result = services.validate(statistics_settings) + issue = result.get_issues()[0] + effective_identifier = result.context.effective_check_identifier(issue.check) + + console = Console(record=True, color_system=None, width=120) + console.print(ValidationResultTextOutputFormatter(result)) + report = console.export_text() + + assert issue.source_profile_identifier == "base" + assert issue.profile_identifier == "overlay" + assert issue.identifier == effective_identifier + assert effective_identifier in report + assert issue.check.identifier not in report + + @pytest.mark.parametrize("abort", [False, True]) def test_deferred_checks_become_skipped_when_validation_stops(statistics_settings, monkeypatch, abort): statistics_settings.abort_on_first = not abort From 54560d6ad6460497ea611f9d0f2b4c6ec290be1e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 10:58:43 +0200 Subject: [PATCH 151/271] =?UTF-8?q?ci:=20=F0=9F=94=84=20replace=20release?= =?UTF-8?q?=20check=20polling=20with=20native=20workflow=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yaml | 28 ++++++---------------------- .github/workflows/testing.yaml | 6 ++---- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b3e2be23b..40210b204 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -36,35 +36,19 @@ env: VENV_PATH: .venv jobs: - # Wait for the testing pipeline to finish - wait-for-testing: - name: 🕒 Wait for Testing Pipeline - runs-on: ubuntu-latest + # Run the test workflow as a prerequisite for release builds. + test: + name: 🧪 Run tests + uses: ./.github/workflows/testing.yaml if: ${{ github.repository == 'crs4/rocrate-validator' }} - steps: - - name: Wait for testing pipeline to succeed - uses: fountainhead/action-wait-for-check@v1.2.0 - id: wait-for-testing - with: - token: ${{ secrets.GITHUB_TOKEN }} - checkName: ⌛ Run tests - ref: ${{ github.sha }} - - - name: Do something with a passing build - if: steps.wait-for-testing.outputs.conclusion == 'success' - run: echo "Testing pipeline passed" && exit 0 - - - name: Do something with a failing build - if: steps.wait-for-testing.outputs.conclusion == 'failure' - run: echo "Testing pipeline failed" && exit 1 # Check and Build Distribution build: name: 🏗 Check and Build Distribution runs-on: ubuntu-latest - needs: wait-for-testing + needs: test if: ${{ github.repository == 'crs4/rocrate-validator' }} steps: - # Access the tag from the first workflow's outputs + # Checkout the commit associated with the release tag. - name: ⬇️ Checkout code uses: actions/checkout@v6 - name: 🐍 Set up Python diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index a86581955..01749ef54 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -14,14 +14,11 @@ name: 🧪 CI Pipeline 1 -- Testing -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch +# Runs on pushes and pull requests, and can be called by the release workflow. on: push: branches: - "**" - tags: - - "*.*.*" paths: - "**" - "!docs/**" @@ -31,6 +28,7 @@ on: - "**" - "!docs/**" - "!examples/**" + workflow_call: env: TERM: xterm From e0ccc19df2c153fa186546c49fe5e649c41b26c9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 11:02:54 +0200 Subject: [PATCH 152/271] =?UTF-8?q?ci(actions):=20=E2=AC=86=EF=B8=8F=20upd?= =?UTF-8?q?ate=20checkout=20to=20v7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yaml | 2 +- .github/workflows/testing.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 40210b204..fbc2d8bf4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -50,7 +50,7 @@ jobs: steps: # Checkout the commit associated with the release tag. - name: ⬇️ Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🐍 Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 01749ef54..a54303a0c 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -44,7 +44,7 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: ⬇️ Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🐍 Set up Python v${{ env.PYTHON_VERSION }} uses: actions/setup-python@v6 with: @@ -81,7 +81,7 @@ jobs: needs: [lint] steps: - name: ⬇️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🐍 Set up Python v${{ env.PYTHON_VERSION }} uses: actions/setup-python@v6 with: From 0b33ccf643d4a1df9e6b17068a334d437581b08e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 11:04:38 +0200 Subject: [PATCH 153/271] =?UTF-8?q?ci(actions):=20=E2=AC=86=EF=B8=8F=20upd?= =?UTF-8?q?ate=20setup-python=20to=20v7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yaml | 2 +- .github/workflows/testing.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fbc2d8bf4..aeae8f353 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -52,7 +52,7 @@ jobs: - name: ⬇️ Checkout code uses: actions/checkout@v7 - name: 🐍 Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.x" - name: 🚧 Set up Python Environment diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index a54303a0c..28887d053 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -46,7 +46,7 @@ jobs: - name: ⬇️ Checkout code uses: actions/checkout@v7 - name: 🐍 Set up Python v${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ env.PYTHON_VERSION }} - name: 🔽 Install ruff (version from pyproject.toml) @@ -83,7 +83,7 @@ jobs: - name: ⬇️ Checkout uses: actions/checkout@v7 - name: 🐍 Set up Python v${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ env.PYTHON_VERSION }} - name: 🔄 Upgrade pip From d531e4576e7a0a33742ed2f30ddbbbfb6c480f56 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 11:05:24 +0200 Subject: [PATCH 154/271] =?UTF-8?q?ci(typos):=20=E2=AC=86=EF=B8=8F=20align?= =?UTF-8?q?=20CI=20and=20pre-commit=20on=20v1.50.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/testing.yaml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 28887d053..b872f93f5 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -72,7 +72,7 @@ jobs: # Emit a compact rule-count summary and fail the job if needed. ruff check rocrate_validator tests --statistics - name: ⌛ Spell check code and profiles (covers Python and SHACL) - uses: crate-ci/typos@v1.47.0 + uses: crate-ci/typos@v1.50.3 # Runs the tests test: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 370ed027a..e2fad595d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: # Run typos to check for common spelling mistakes in the codebase. - repo: https://github.com/crate-ci/typos - rev: v1.47.2 + rev: v1.50.3 hooks: - id: typos # Pre-commit passes explicit file paths to the hook, bypassing the From 42896acda7075c5b5ed776a95a91a1210653e68f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 11:06:26 +0200 Subject: [PATCH 155/271] =?UTF-8?q?ci(release):=20=F0=9F=94=8F=20update=20?= =?UTF-8?q?Sigstore=20action=20to=20v3.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aeae8f353..caab4c3b7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -140,7 +140,7 @@ jobs: name: python-package-distributions path: dist/ - name: 🖊️ Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v3.3.0 + uses: sigstore/gh-action-sigstore-python@v3.5.0 with: inputs: >- ./dist/*.tar.gz From 039f8f136d7062122314c44fbb1644cab00e73fb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 11:11:24 +0200 Subject: [PATCH 156/271] =?UTF-8?q?ci(deps):=20=F0=9F=A4=96=20configure=20?= =?UTF-8?q?weekly=20Dependabot=20updates=20for=20GitHub=20Actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..ca79ca5b4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly From e150f0dcadf6e43b0e6362ed547d2c0bda1aaad9 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 1 Oct 2026 12:27:31 +0200 Subject: [PATCH 157/271] fix: correct two check descriptions --- .../profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl | 2 +- .../profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl index c10007a89..db6fcb9bb 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl @@ -136,7 +136,7 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; sh:name "Data Entity: REQUIRED Root linkage" ; - sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; + sh:description """A `DataEntity` MUST be linked, either directly or indirectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; sh:property [ diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl index a2dc53a04..c2a27ae09 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl @@ -143,7 +143,7 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; sh:name "Data Entity: REQUIRED Root linkage" ; - sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; + sh:description """A `DataEntity` MUST be linked, either directly or indirectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; sh:property [ From 06f5520ec4866afaf2eb6bae59a1ddb52781b132 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 12:44:38 +0200 Subject: [PATCH 158/271] =?UTF-8?q?fix(profiles):=20=F0=9F=A7=AD=20identif?= =?UTF-8?q?y=20metadata=20descriptors=20by=20filename=20suffix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.1/must/1_file-descriptor_metadata.ttl | 10 ++++++++-- .../1.1/must/2_root_data_entity_metadata.ttl | 10 ++++++++-- .../ro-crate/1.1/must/4_data_entity_metadata.ttl | 15 ++++++++++++--- .../1.2/must/1_file-descriptor_metadata.ttl | 5 ++++- .../1.2/must/2_root_data_entity_metadata.ttl | 10 ++++++++-- .../ro-crate/1.2/must/4_data_entity_metadata.ttl | 15 ++++++++++++--- .../1.2/must/5_web_data_entity_metadata.ttl | 5 ++++- .../ro-crate/1.2/should/0_entity_metadata.ttl | 5 ++++- .../1.2/should/4_data_entity_metadata.ttl | 5 ++++- .../ro-crate/1.2/should/4_dataset_data_entity.ttl | 5 ++++- .../ro-crate/1.2/should/4_referenced_rocrate.ttl | 10 ++++++++-- .../workflow-ro-crate/must/0_main-workflow.ttl | 5 ++++- 12 files changed, 80 insertions(+), 20 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl index d9d184c9a..4c19c70c7 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl @@ -37,7 +37,10 @@ ro-crate:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; ?crate a schema:Dataset . ?md schema:about ?crate . ?md a schema:CreativeWork . - FILTER( STRENDS(STR(?md), "ro-crate-metadata.json") ) + FILTER( + STRENDS(STR(?md), "ro-crate-metadata.json") || + STRENDS(STR(?md), "ro-crate-metadata.jsonld") + ) } } """ ; @@ -56,7 +59,10 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:Hidde WHERE { ?crate a schema:Dataset . ?this schema:about ?crate . - FILTER(contains(str(?this), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?this), "ro-crate-metadata.json") || + STRENDS(str(?this), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl index 236d3cdc4..24043c2a9 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl @@ -32,7 +32,10 @@ ro-crate:RootDataEntityType SELECT ?this WHERE { ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; @@ -73,7 +76,10 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape; WHERE { ?this a schema:Dataset . ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl index c10007a89..c7d7d1bd9 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl @@ -49,7 +49,10 @@ ro-crate:FileDataEntity a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?this a schema:MediaObject . FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) @@ -90,7 +93,10 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?this a schema:Dataset . # Exclude all dataset entities that ends with `./#` @@ -163,7 +169,10 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root ?metadatafile WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?root schema:hasPart ?this . FILTER(?this != ?root) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl index 6c90fb03a..77577fd76 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl @@ -32,7 +32,10 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape ; WHERE { ?crate a schema:Dataset . ?this schema:about ?crate . - FILTER(contains(str(?this), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?this), "ro-crate-metadata.json") || + STRENDS(str(?this), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl index 68ce7aaf3..bc0a4690e 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl @@ -28,7 +28,10 @@ ro-crate:MetadataAboutTarget a sh:SPARQLTarget ; SELECT ?this WHERE { ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ . @@ -46,7 +49,10 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape ; WHERE { ?this a schema:Dataset . ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl index a2dc53a04..b19983cfc 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl @@ -46,7 +46,10 @@ ro-crate:FileDataEntity a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?this a schema:MediaObject . FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) @@ -97,7 +100,10 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?this a schema:Dataset . # Exclude all dataset entities that ends with `./#` @@ -167,7 +173,10 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root ?metadatafile WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?root schema:hasPart ?this . FILTER(?this != ?root) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl index db63676ad..68d31785f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl @@ -34,7 +34,10 @@ ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; SELECT ?this WHERE { ?this a schema:MediaObject . - FILTER(!contains(str(?this), "ro-crate-metadata.json")) + FILTER( + !STRENDS(str(?this), "ro-crate-metadata.json") && + !STRENDS(str(?this), "ro-crate-metadata.jsonld") + ) FILTER regex(str(?this), "^(https?|ftps?)://", "i") } """ diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl index f92c1e9c0..462270a3c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl @@ -34,7 +34,10 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; { SELECT DISTINCT ?root ?metadatafile WHERE { ?root a schema:Dataset . ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } FILTER(?this != ?metadatafile) FILTER (?this != ?root) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl index a1dda19d0..d098d2abc 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl @@ -166,7 +166,10 @@ ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; WHERE { { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?this a schema:Dataset . FILTER(?this != ?root) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl index f5ed4db9f..951bf4dcf 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl @@ -23,7 +23,10 @@ ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; WHERE { { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } ?this a schema:Dataset . FILTER(?this != ?root) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl index e7bd059ae..4b1138d71 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl @@ -37,7 +37,10 @@ ro-crate:ReferencedROCrateDataEntityTarget a sh:SPARQLTarget ; FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } FILTER(?this != ?root) } @@ -60,7 +63,10 @@ ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) { SELECT DISTINCT ?root WHERE { ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } } FILTER(?crate != ?root) } diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl index aefb42e7a..b3b161f2b 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl @@ -45,7 +45,10 @@ workflow-ro-crate:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; ?root schema:mainEntity ?this . ?root a schema:Dataset . ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; From a09b795a0f9945397fd0d1583edd27d1e6629ae9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 12:46:06 +0200 Subject: [PATCH 159/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20metadata=20descriptor=20targeting=20across=20core=20version?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_metadata_rootDataEntity_v1_3.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py index 5d2ecbe82..855556104 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py @@ -13,8 +13,13 @@ # limitations under the License. import logging +from pathlib import Path + +from rdflib import Graph, Literal, Namespace, URIRef +from rdflib.namespace import RDF from rocrate_validator import models, services +from rocrate_validator.requirements.shacl.validator import SHACLValidator from rocrate_validator.utils.http import HttpRequester from tests.ro_crates_v1_3 import RootDataEntity from tests.shared import do_entity_test @@ -25,6 +30,42 @@ __metadata_root_data_entity_crates__ = RootDataEntity() +def test_root_data_entity_type_target_ignores_preview_about_literal(): + """Core 1.1/1.2 root selection ignores preview about values and supports descriptor suffixes.""" + schema = Namespace("http://schema.org/") + root = URIRef("https://example.org/crate/ro-crate-metadata.json/") + preview = URIRef(f"{root}ro-crate-preview.html") + descriptor_cases = ( + ("1.1", "ro-crate-metadata.json"), + ("1.1", "ro-crate-metadata.jsonld"), + ("1.2", "ro-crate-metadata.json"), + ("1.2", "test-ro-crate-metadata.json"), + ("1.2", "ro-crate-metadata.jsonld"), + ) + + for profile_version, descriptor_name in descriptor_cases: + profile_path = ( + Path(__file__).resolve().parents[4] / "rocrate_validator/profiles/ro-crate" / profile_version + ) + shapes = Graph() + shapes.parse(profile_path / "prefixes.ttl", format="turtle") + shapes.parse(profile_path / "must/2_root_data_entity_metadata.ttl", format="turtle") + + data = Graph() + data.add((URIRef(f"{root}{descriptor_name}"), schema.about, root)) + data.add((preview, schema.about, Literal("./"))) + data.add((root, RDF.type, schema.Dataset)) + data.add((root, RDF.type, URIRef("http://www.w3.org/ns/dx/prof/Profile"))) + data.add((root, schema.name, Literal("Workflow RO-Crate profile"))) + data.add((root, schema.description, Literal("A profile crate."))) + data.add((root, schema.datePublished, Literal("2024-01-01"))) + data.add((root, schema.license, URIRef("https://creativecommons.org/licenses/by/4.0/"))) + + result = SHACLValidator(shapes_graph=shapes).validate(data_graph=data) + + assert result.conforms, f"Core profile {profile_version}, descriptor {descriptor_name}" + + def test_valid_required_datePublished(): """ Test that the Root Data Entity is valid when it includes a `datePublished` property. From 088d7f4310e245d989bc25058d088558fa7d3d16 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 18:12:10 +0200 Subject: [PATCH 160/271] =?UTF-8?q?chore(deps):=20=F0=9F=A4=96=20group=20w?= =?UTF-8?q?eekly=20dependency=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ca79ca5b4..6968afc79 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,8 @@ updates: directory: / schedule: interval: weekly + groups: + weekly-batch: + patterns: + - "*" + From d63284e8fbd92368cbcae97ed2ea3acc30c8a0bd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 18:44:52 +0200 Subject: [PATCH 161/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.12.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb0346fc..2eed4c183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.12.1] - 2026-10-01 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.0...0.12.1 + +### 🔧 Changed + +- chore(deps): group weekly dependency updates ([088d7f43](https://github.com/kikkomep/rocrate-validator/commit/088d7f4310e245d989bc25058d088558fa7d3d16)) +- test(validation): cover metadata descriptor targeting across core versions ([a09b795a](https://github.com/kikkomep/rocrate-validator/commit/a09b795a0f9945397fd0d1583edd27d1e6629ae9)) +- ci(deps): configure weekly Dependabot updates for GitHub Actions ([039f8f13](https://github.com/kikkomep/rocrate-validator/commit/039f8f136d7062122314c44fbb1644cab00e73fb)) +- ci(release): update Sigstore action to v3.5.0 ([42896acd](https://github.com/kikkomep/rocrate-validator/commit/42896acda7075c5b5ed776a95a91a1210653e68f)) +- ci(typos): align CI and pre-commit on v1.50.3 ([d531e457](https://github.com/kikkomep/rocrate-validator/commit/d531e4576e7a0a33742ed2f30ddbbbfb6c480f56)) +- ci(actions): update setup-python to v7 ([0b33ccf6](https://github.com/kikkomep/rocrate-validator/commit/0b33ccf643d4a1df9e6b17068a334d437581b08e)) +- ci(actions): update checkout to v7 ([e0ccc19d](https://github.com/kikkomep/rocrate-validator/commit/e0ccc19df2c153fa186546c49fe5e649c41b26c9)) +- ci: replace release check polling with native workflow dependency ([54560d6a](https://github.com/kikkomep/rocrate-validator/commit/54560d6ad6460497ea611f9d0f2b4c6ec290be1e)) +- test(validation): verify effective overlay IDs in text reports ([c51cf5de](https://github.com/kikkomep/rocrate-validator/commit/c51cf5de4362f4d4da92b4e40a070aa7a17e683b)) +- test(validation): cover profile Root Data Entity hasPart cases ([865f39c7](https://github.com/kikkomep/rocrate-validator/commit/865f39c729f44d09456ab605fa44702680395d26)) + +### 🐛 Fixed + +- fix(profiles): identify metadata descriptors by filename suffix ([06f5520e](https://github.com/kikkomep/rocrate-validator/commit/06f5520ec4866afaf2eb6bae59a1ddb52781b132)) +- fix: correct two check descriptions ([e150f0dc](https://github.com/kikkomep/rocrate-validator/commit/e150f0dcadf6e43b0e6362ed547d2c0bda1aaad9)) +- fix(validation): show overlay check IDs in text reports ([def3ded1](https://github.com/kikkomep/rocrate-validator/commit/def3ded1189e4861afea4af8f868fd64e7f714c3)) +- fix(validation): report missing hasPart Data Entities accurately ([6c065b84](https://github.com/kikkomep/rocrate-validator/commit/6c065b84709f9ec663675ac1c3f168c47cf798f7)) + ## [0.12.0] - 2026-09-30 Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.11.4...0.12.0 From d91a1b148586e21a607f71b62e393de719b7f8db Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Oct 2026 18:46:22 +0200 Subject: [PATCH 162/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.12.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9582b1b50..858857558 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.12.0" +version = "0.12.1" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 050dd18a2e3185d490ea95ee2d68cbb0470903a8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 09:31:04 +0200 Subject: [PATCH 163/271] =?UTF-8?q?fix(profiles):=20=F0=9F=90=9B=20accept?= =?UTF-8?q?=20declared=20compact=20IRIs=20in=20JSON-LD=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.1/must/0_file_descriptor_format.py | 56 +------- .../1.2/must/0_file_descriptor_format.py | 45 +----- rocrate_validator/utils/jsonld.py | 134 +++++++++++++++++- 3 files changed, 146 insertions(+), 89 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index 53c78de57..0f683c082 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -25,6 +25,7 @@ from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError +from rocrate_validator.utils.jsonld import find_unexpected_compaction_keys, resolve_compaction_context # set up logging logger = logging.getLogger(__name__) @@ -447,49 +448,6 @@ def __get_remote_context_keys__(self, context_uri: str) -> set: raise TypeError("The context is not a dictionary") return set(jsonLD_ctx.keys()) - # Reserved JSON-LD keywords that are always allowed as entity keys. - __RESERVED_JSONLD_KEYS__ = frozenset({"@id", "@type", "@context", "@value", "@language"}) - - def __check_entity_keys__( - self, entity: Any, context_keys: set, unexpected_keys: dict[str, int] | None = None - ) -> dict[str, int]: - """Check if the entity is in the correct format""" - # Ensure unexpected_keys is initialized - if unexpected_keys is None: - unexpected_keys = {} - - # If the entity is a dictionary, classify each key and recurse into values - if isinstance(entity, dict): - for k, v in entity.items(): - self.__record_unexpected_key__(k, context_keys, unexpected_keys) - # If the value is a dictionary or a list, check its keys recursively - if isinstance(v, (dict, list)): - self.__check_entity_keys__(v, context_keys, unexpected_keys) - - # If the entity is a list, check each element - elif isinstance(entity, list): - for elem in entity: - self.__check_entity_keys__(elem, context_keys, unexpected_keys) - - return unexpected_keys - - def __record_unexpected_key__(self, k: str, context_keys: set, unexpected_keys: dict[str, int]) -> None: - """Record ``k`` as unexpected unless it is reserved or a valid compact IRI prefix""" - # If the key is a reserved JSON-LD keyword, skip it - if k in self.__RESERVED_JSONLD_KEYS__: - logger.debug(f"Key {k} is a reserved JSON-LD keyword, skipping") - return - - # A key not in the context can still be valid in compacted format if it is - # a compact IRI whose prefix is in the context. - if k not in context_keys: - logger.debug(f"Key {k} not in context keys") - prefix = k.split(":", 1)[0] if ":" in k else None - logger.debug(f"Checking prefix {prefix} of key {k}") - if prefix is None or prefix not in context_keys: - logger.debug(f"Key {k} does not have a valid prefix in context keys, adding to unexpected keys") - unexpected_keys[k] = unexpected_keys.get(k, 0) + 1 - @check( name="Validation of the compaction format of the file descriptor", depends_on=("File Descriptor JSON format",), @@ -505,8 +463,8 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n logger.debug(f"Context: {jsonld_context}") try: - context_keys = self.__get_context_keys__(jsonld_context) - logger.debug(f"{context_keys}") + context_definitions = resolve_compaction_context(jsonld_context, self.__get_remote_context__) + logger.debug(f"{context_definitions}") except OfflineCacheMissError: raise except _HANDLED_REMOTE_CONTEXT_ERRORS as e: @@ -515,7 +473,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n context.result.add_issue(str(e), self) return False - unexpected_keys = self.__check_entity_keys__(json_dict.get("@graph"), context_keys) + unexpected_keys = find_unexpected_compaction_keys(json_dict.get("@graph"), context_definitions) logger.debug(f"Unexpected keys: {unexpected_keys}") if len(unexpected_keys) > 0: for k, v in unexpected_keys.items(): @@ -525,8 +483,8 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n # Check if k is a term or a URI if k.startswith("http"): context.result.add_issue( - f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "' - "because the compacted format requires simple terms as keys " + f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} ' + "because it is not mapped by the document context to a term or compact IRI " "(see https://www.w3.org/TR/json-ld-api/#compaction for more details).", self, ) @@ -534,7 +492,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n context.result.add_issue( f'The {v} occurrence{suffix} of the JSON-LD key "{k}" ' f"{'is' if v == 1 else 'are'} not allowed in the compacted format " - "because it is not present in the @context of the document", + "because it is not defined as a term or compact IRI by the @context of the document", self, ) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index ab5c8876d..749d258a8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -23,6 +23,7 @@ from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError +from rocrate_validator.utils.jsonld import find_unexpected_compaction_keys, resolve_compaction_context # set up logging logger = logging.getLogger(__name__) @@ -631,38 +632,6 @@ def __get_remote_context_keys__(self, context_uri: str) -> set: raise TypeError("The context is not a dictionary") return set(jsonLD_ctx.keys()) - def __check_entity_keys__( - self, entity: object, context_keys: set, unexpected_keys: dict[str, int] | None = None - ) -> dict[str, int]: - """Check if the entity is in the correct format""" - - def add_unexpected_key(k: str, u_keys: dict) -> None: - """Add a key to the unexpected keys dictionary""" - u_keys[k] = u_keys.get(k, 0) + 1 - - # Keys that should be skipped - SKIP_KEYS = {"@id", "@type", "@context", "@value", "@language"} - - # Ensure unexpected_keys is initialized - if unexpected_keys is None: - unexpected_keys = {} - - # If the entity is a dictionary, check each key - if isinstance(entity, dict): - for k, v in entity.items(): - if k not in context_keys and k not in SKIP_KEYS: - logger.debug(f"Key {k} not in context keys") - add_unexpected_key(k, unexpected_keys) - if isinstance(v, (dict, list)): - self.__check_entity_keys__(v, context_keys, unexpected_keys) - - # If the entity is a list, check each element - elif isinstance(entity, list): - for elem in entity: - self.__check_entity_keys__(elem, context_keys, unexpected_keys) - - return unexpected_keys - @check( name="Validation of the compaction format of the file descriptor", depends_on=("File Descriptor JSON format",), @@ -678,8 +647,8 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n logger.debug(f"Context: {jsonld_context}") try: - context_keys = self.__get_context_keys__(jsonld_context) - logger.debug(f"{context_keys}") + context_definitions = resolve_compaction_context(jsonld_context, self.__get_remote_context__) + logger.debug(f"{context_definitions}") except OfflineCacheMissError: raise except _HANDLED_REMOTE_CONTEXT_ERRORS as e: @@ -688,7 +657,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n context.result.add_issue(str(e), self) return False - unexpected_keys = self.__check_entity_keys__(json_dict.get("@graph", []), context_keys) + unexpected_keys = find_unexpected_compaction_keys(json_dict.get("@graph", []), context_definitions) logger.debug(f"Unexpected keys: {unexpected_keys}") if len(unexpected_keys) > 0: for k, v in unexpected_keys.items(): @@ -698,8 +667,8 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n # Check if k is a term or a URI if k.startswith("http"): context.result.add_issue( - f'The The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "' - "because the compacted format requires simple terms as keys " + f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} ' + "because it is not mapped by the document context to a term or compact IRI " "(see https://www.w3.org/TR/json-ld-api/#compaction for more details).", self, ) @@ -707,7 +676,7 @@ def check_compaction(self, context: ValidationContext) -> CheckResultValue: # n context.result.add_issue( f'The {v} occurrence{suffix} of the JSON-LD key "{k}" ' f"{'is' if v == 1 else 'are'} not allowed in the compacted format " - "because it is not present in the @context of the document", + "because it is not defined as a term or compact IRI by the @context of the document", self, ) return False diff --git a/rocrate_validator/utils/jsonld.py b/rocrate_validator/utils/jsonld.py index 88b4ba6d1..8d00a4edf 100644 --- a/rocrate_validator/utils/jsonld.py +++ b/rocrate_validator/utils/jsonld.py @@ -14,8 +14,9 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping from typing import Any +from urllib.parse import urljoin def _context_containers(context: object) -> dict[str, set[str]]: @@ -44,7 +45,8 @@ def _context_containers(context: object) -> dict[str, set[str]]: def find_singleton_property_arrays(metadata: Mapping[str, Any]) -> list[tuple[str | None, str]]: - """Find singleton arrays used for properties of flattened JSON-LD entities. + """ + Find singleton arrays used for properties of flattened JSON-LD entities. The RO-Crate ``@graph`` array and ``@context`` are structural JSON-LD arrays, not entity property values. JSON-LD keyword properties are also @@ -73,3 +75,131 @@ def find_singleton_property_arrays(metadata: Mapping[str, Any]) -> list[tuple[st ): findings.append((entity_id, property_name)) return findings + + +_COMPACTION_KEYWORDS = frozenset({"@id", "@type", "@context", "@value", "@language"}) +_JSONLD_11 = 1.1 +_URI_GEN_DELIMS = (":", "/", "?", "#", "[", "]", "@") + + +def resolve_compaction_context( + context: object, + load_remote: Callable[[str], object], + *, + base: str = "", + remote_stack: tuple[str, ...] = (), +) -> dict[str, Any]: + """ + Keep ordered term definitions instead of losing prefix mappings in a set. + + Remote retrieval belongs to the calling profile, preserving its HTTP, + alternate-link and offline-cache behavior. Null contexts reset definitions; + null term definitions remain present so they can disable an earlier mapping. + """ + definitions: dict[str, Any] = {} + + def apply(source: object, source_base: str, stack: tuple[str, ...]) -> None: + if source is None: + definitions.clear() + elif isinstance(source, list): + for item in source: + apply(item, source_base, stack) + elif isinstance(source, str): + uri = urljoin(source_base, source) + if uri in stack: + raise ValueError(f"Recursive JSON-LD context: {uri}") + apply(load_remote(uri), uri, (*stack, uri)) + elif isinstance(source, dict): + definitions.update(source) + # Freeze namespace mappings when they are defined: redefining a + # referenced prefix in a later context must not change earlier IRIs. + for term, definition in source.items(): + iri = _mapped_iri(term, definitions) + if iri is not None: + definitions[term] = {**definition, "@id": iri} if isinstance(definition, dict) else iri + else: + raise TypeError("JSON-LD context must be an object, URL, array or null") + + apply(context, base, remote_stack) + return definitions + + +def _mapped_iri(term: str, definitions: Mapping[str, Any], seen: frozenset[str] = frozenset()) -> str | None: + """Resolve a mapping expressed using another declared prefix, without cycles.""" + if term in seen: + return None + definition = definitions.get(term) + iri = definition.get("@id") if isinstance(definition, dict) else definition + if not isinstance(iri, str) or iri.startswith(("@", "_:")): + return None + prefix, separator, suffix = iri.partition(":") + if separator and prefix in definitions and not suffix.startswith("//"): + namespace = _mapped_iri(prefix, definitions, seen | {term}) + return namespace + suffix if namespace else None + if not separator and iri in definitions: + return _mapped_iri(iri, definitions, seen | {term}) + return iri if separator else None + + +def _is_compact_iri(key: str, definitions: Mapping[str, Any]) -> bool: + prefix, separator, suffix = key.partition(":") + if not separator or prefix == "_" or suffix.startswith("//"): + return False + iri = _mapped_iri(prefix, definitions) + if iri is None: + return False + definition = definitions[prefix] + # For this check, contexts without @version use the JSON-LD 1.0 policy, + # matching the requirement for RO-Crate 1.1 and 1.2. This is a validator + # policy; it does not infer the mode selected by a JSON-LD processor: + # https://www.researchobject.org/ro-crate/specification/1.1/structure.html + # https://www.researchobject.org/ro-crate/specification/1.2/structure.html + # https://www.w3.org/TR/json-ld11/#json-ld-1-1-processing-mode + if definitions.get("@version", 1.0) != _JSONLD_11: + # In this check, a resolved mapping is enough to accept the key as a + # compact IRI. JSON-LD 1.1's @prefix flag is handled explicitly below. + return True + # JSON-LD 1.1 distinguishes simple (string) and expanded (object) term + # definitions when deciding whether a term can be used as a prefix: + # https://www.w3.org/TR/json-ld11/#compact-iris + if isinstance(definition, dict): + # An object such as {"@id": "https://example.org/"} needs @prefix: true + # to enable prefix use. Missing/false @prefix does not invalidate the + # term definition itself; it only prevents using it as a prefix here. + return definition.get("@prefix") is True + # A simple mapping such as "ex": "https://example.org/" needs no @prefix, + # but its IRI must end in a URI general delimiter (e.g. '/' or '#'). + return iri.endswith(_URI_GEN_DELIMS) + + +def find_unexpected_compaction_keys(entity: object, definitions: Mapping[str, Any]) -> dict[str, int]: + """ + Count unmapped properties in the flat RO-Crate JSON-LD representation. + + Keep the existing explicit-term policy (including rejection of unmapped + absolute IRI keys), but also accept eligible compact IRIs. Context and + literal payloads are not node objects and must not be traversed as such. + """ + unexpected: dict[str, int] = {} + + def visit(value: object) -> None: + if isinstance(value, list): + for item in value: + visit(item) + elif isinstance(value, dict): + for key, item in value.items(): + definition = definitions.get(key) + defined = definition is not None and not ( + isinstance(definition, dict) and "@id" in definition and definition["@id"] is None + ) + if ( + key not in _COMPACTION_KEYWORDS + and not defined + and (key in definitions or not _is_compact_iri(key, definitions)) + ): + unexpected[key] = unexpected.get(key, 0) + 1 + if key not in {"@context", "@value"}: + visit(item) + + visit(entity) + return unexpected From afb7b0d2b5e245f81f5f36f6d1ade43b0f2c53cb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 09:33:35 +0200 Subject: [PATCH 164/271] =?UTF-8?q?test(jsonld):=20=F0=9F=A7=AA=20cover=20?= =?UTF-8?q?compact=20IRI=20prefixes=20across=20RO-Crate=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_compact_iri_validation.py | 90 ++++++++++ tests/unit/test_jsonld_compact_iris.py | 159 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 tests/integration/test_compact_iri_validation.py create mode 100644 tests/unit/test_jsonld_compact_iris.py diff --git a/tests/integration/test_compact_iri_validation.py b/tests/integration/test_compact_iri_validation.py new file mode 100644 index 000000000..1889353f7 --- /dev/null +++ b/tests/integration/test_compact_iri_validation.py @@ -0,0 +1,90 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exercise compact IRIs through the public validator and profile overlays.""" + +import json +from copy import deepcopy +from unittest.mock import Mock + +import pytest + +from rocrate_validator import models, services +from rocrate_validator.utils.http import HttpRequester + +COMPACTION_CHECK = "Validation of the compaction format of the file descriptor" + + +@pytest.mark.parametrize("version", ["1.1", "1.2", "1.3"]) +@pytest.mark.parametrize("property_name", ["ex:property", "property", "unknown"]) +def test_compact_property_validation(version, property_name, tmp_path, monkeypatch): + context = { + "Dataset": "http://schema.org/Dataset", + "CreativeWork": "http://schema.org/CreativeWork", + "name": "http://schema.org/name", + "description": "http://schema.org/description", + "datePublished": "http://schema.org/datePublished", + "license": "http://schema.org/license", + "about": "http://schema.org/about", + "conformsTo": "http://purl.org/dc/terms/conformsTo", + } + response = Mock(status_code=200, headers={"Content-Type": "application/ld+json"}) + response.json.side_effect = lambda: {"@context": deepcopy(context)} + monkeypatch.setattr(HttpRequester(), "get", Mock(return_value=response)) + metadata = { + "@context": [ + f"https://w3id.org/ro/crate/{version}/context", + { + "ex": "https://example.org/", + "property": "https://example.org/property", + }, + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": f"https://w3id.org/ro/crate/{version}"}, + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Prefix regression", + "description": "Test crate", + "datePublished": "2026-10-01", + "license": "https://creativecommons.org/publicdomain/zero/1.0/", + property_name: "value", + }, + ], + } + (tmp_path / "ro-crate-metadata.json").write_text(json.dumps(metadata)) + result = services.validate( + models.ValidationSettings( + rocrate_uri=str(tmp_path), + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + ) + checks = [check for check in result.executed_checks if check.name == COMPACTION_CHECK] + assert len(checks) == 1 + source_version = "1.2" if version == "1.3" else version + assert checks[0].requirement.profile.identifier == f"ro-crate-{source_version}" + accepted = property_name != "unknown" + assert result.get_executed_check_result(checks[0]) is accepted + issues = [issue for issue in result.get_issues() if issue.check.name == COMPACTION_CHECK] + assert len(issues) == (0 if accepted else 1) + if issues: + assert issues[0].profile_identifier == f"ro-crate-{version}" + assert issues[0].source_profile_identifier == f"ro-crate-{source_version}" + assert result.passed() is accepted, [issue.message for issue in result.get_issues()] diff --git a/tests/unit/test_jsonld_compact_iris.py b/tests/unit/test_jsonld_compact_iris.py new file mode 100644 index 000000000..83ff40f0a --- /dev/null +++ b/tests/unit/test_jsonld_compact_iris.py @@ -0,0 +1,159 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regression coverage for the RO-Crate compact property-name policy.""" + +# Empty dictionaries are part of the helper contract; None must not pass. +# pylint: disable=use-implicit-booleaness-not-comparison + +import json +from copy import deepcopy +from unittest.mock import Mock + +import pytest +from rdflib import Graph +from rdflib.compare import isomorphic + +from rocrate_validator.utils.http import OfflineCacheMissError +from rocrate_validator.utils.jsonld import find_unexpected_compaction_keys, resolve_compaction_context + + +@pytest.mark.parametrize( + ("definition", "version", "accepted"), + [ + ("https://example.org/", 1.0, True), + ("https://example.org/ns", 1.0, True), + ("https://example.org#", 1.0, True), + ({"@id": "https://example.org/"}, 1.0, True), + ("https://example.org/", 1.1, True), + ({"@id": "https://example.org/", "@prefix": True}, 1.1, True), + ({"@id": "https://example.org/", "@prefix": False}, 1.1, False), + ({"@id": "https://example.org/"}, 1.1, False), + ({"@id": "https://example.org/ns", "@prefix": True}, 1.1, True), + ("https://example.org/name", 1.1, False), + (None, 1.0, False), + ({"@id": None}, 1.0, False), + ("@id", 1.0, False), + ("relative/", 1.0, False), + ], +) +def test_prefix_eligibility(definition, version, accepted): + context = {"@version": version, "ex": definition} + unexpected = find_unexpected_compaction_keys({"@id": "./", "ex:property": "value"}, context) + assert unexpected == ({} if accepted else {"ex:property": 1}) + + +def test_context_order_nullification_and_remote_loader(): + loader = Mock(return_value={"ex": "https://example.org/", "name": "http://schema.org/name"}) + context = ["https://example.org/context", {"ex": None}] + before = deepcopy(context) + definitions = resolve_compaction_context(context, loader) + assert find_unexpected_compaction_keys({"ex:property": 1, "name": "test"}, definitions) == {"ex:property": 1} + loader.assert_called_once_with("https://example.org/context") + assert context == before + assert resolve_compaction_context([{"ex": "https://example.org/"}, None], loader) == {} + + +def test_remote_cycles_fail(): + loader = Mock(return_value="https://example.org/context") + with pytest.raises(ValueError, match="Recursive JSON-LD context"): + resolve_compaction_context("https://example.org/context", loader) + + +def test_remote_cache_miss_propagates(): + loader = Mock(side_effect=OfflineCacheMissError("https://example.org/context")) + with pytest.raises(OfflineCacheMissError): + resolve_compaction_context("https://example.org/context", loader) + + +def test_compact_namespace_definition(): + definitions = {"base": "https://example.org/", "ex": "base:terms#"} + assert find_unexpected_compaction_keys({"ex:property": "value"}, definitions) == {} + + +def test_counts_and_non_node_payloads(): + entity = [ + {"@id": "./", "ex:property": [{"@value": "value", "@language": "en"}], "unknown": 1}, + {"@id": "#node", "unknown": 2, "@context": {"ex": "https://example.org/"}}, + ] + assert find_unexpected_compaction_keys(entity, {"ex": "https://example.org/"}) == {"unknown": 2} + + +def test_explicit_terms_and_absolute_iri_policy(): + definitions = {"ex:property": "https://example.org/property", "ex": "https://example.org/"} + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {} + assert find_unexpected_compaction_keys({"https://example.org/property": 1}, definitions) == { + "https://example.org/property": 1 + } + + +def test_prefix_and_explicit_term_preserve_rdf(): + prefixed = {"@context": {"ex": "https://example.org/"}, "@id": "https://example.org/node", "ex:property": "value"} + explicit = {"@context": {"property": "https://example.org/property"}, "@id": prefixed["@id"], "property": "value"} + assert isomorphic( + Graph().parse(data=json.dumps(prefixed), format="json-ld"), + Graph().parse(data=json.dumps(explicit), format="json-ld"), + ) + + +@pytest.mark.parametrize("disabled", [None, {"@id": None}]) +def test_explicitly_disabled_compact_key_does_not_fall_back_to_prefix(disabled): + definitions = {"ex": "https://example.org/", "ex:property": disabled} + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {"ex:property": 1} + + +def test_remote_context_reset_clears_previous_definitions(): + loader = Mock(return_value=[None, {"new": "https://example.org/"}]) + definitions = resolve_compaction_context([{"old": "https://example.org/"}, "https://example.org/context"], loader) + assert definitions == {"new": "https://example.org/"} + + +def test_later_namespace_override_does_not_rewrite_previous_mapping(): + definitions = resolve_compaction_context( + [ + {"base": "https://example.org/", "ex": "base:terms#"}, + {"base": "https://other.example/"}, + ], + Mock(), + ) + assert definitions["ex"] == "https://example.org/terms#" + assert definitions["base"] == "https://other.example/" + + +def test_remote_failure_is_not_ignored(): + loader = Mock(side_effect=RuntimeError("Unable to retrieve context")) + with pytest.raises(RuntimeError, match="Unable to retrieve"): + resolve_compaction_context("https://example.org/context", loader) + + +def test_relative_remote_reference_uses_parent_context_url(): + loader = Mock(side_effect=["nested.jsonld", {"ex": "https://example.org/"}]) + definitions = resolve_compaction_context("https://example.org/context/index.jsonld", loader) + assert loader.call_args_list[-1].args == ("https://example.org/context/nested.jsonld",) + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {} + + +@pytest.mark.parametrize( + "definitions", + [ + {"base": "https://example.org/", "ex": "base"}, + {"base": "https://example.org/", "ex": {"@id": "base"}}, + ], +) +def test_namespace_alias(definitions): + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {} + + +def test_cyclic_namespace_alias_is_not_a_prefix(): + assert find_unexpected_compaction_keys({"ex:property": 1}, {"ex": "other", "other": "ex"}) == {"ex:property": 1} From cdb6e04edcfba8a8eb29b36a443ce3e631f008d4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 14:31:12 +0200 Subject: [PATCH 165/271] =?UTF-8?q?fix(profiles):=20=F0=9F=90=9B=20handle?= =?UTF-8?q?=20absolute=20file=20URI=20Data=20Entity=20identifiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.2/must/4_data_entity_metadata.py | 111 +++++++++++++----- 1 file changed, 79 insertions(+), 32 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 3453324be..7f6aed04d 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -14,17 +14,52 @@ import contextlib import re +from pathlib import Path +from urllib.parse import unquote, urlsplit from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable +from rocrate_validator.utils.uri import is_external_reference # set up logging logger = logging.getLogger(__name__) +def _data_entity_path_error(identifier: str) -> str | None: + """ + Validate path forms without inferring transport or payload membership. + + This is not a general URI validator. RFC 8089 requires an absolute path + after ``file:``, whereas RFC 3986 network-path references have an authority + but no scheme. Neither a network-path reference nor a bare filesystem path + expresses a path relative to the crate root. + """ + if "\\" in identifier or " " in identifier: + return "has an invalid @id; use URI-compatible paths" + # Match the existing URI utility's drive-letter disambiguation on every OS. + if re.match(r"^[A-Za-z]:/", identifier): + return "has a Windows drive path; use a relative URI path or an absolute file URI" + try: + parsed = urlsplit(identifier) + except ValueError: + return "has an invalid @id; use a valid URI reference" + if parsed.scheme == "file" and not parsed.path.startswith("/"): + return "has an invalid @id; the file URI path MUST be absolute (RFC 8089 section 2)" + path_error = None + if not parsed.scheme: + if identifier.startswith("//"): + path_error = ( + "has a network-path reference; use an absolute URI with a scheme " + "or a path relative to the RO-Crate root" + ) + elif parsed.path.startswith("/"): + path_error = "MUST use a relative @id within the RO-Crate root" + return path_error + + @requirement(name="Data Entity: REQUIRED resource availability") class DataEntityRequiredChecker(PyFunctionCheck): """ @@ -74,7 +109,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # entity.id, ) continue - if not entity.has_relative_path(): + if is_external_reference(entity.id) or not entity.has_relative_path(): logger.debug( "Ignoring the Data Entity '%s' as it is a local entity with an absolute path. " "According to the RO-Crate specification, local entities with absolute paths " @@ -131,7 +166,7 @@ def check_detached_entities(self, context: ValidationContext) -> CheckResultValu for entity in entities: if root_entity_id and entity.id == root_entity_id: continue - if not entity.is_remote(): + if not is_external_reference(entity.id): context.result.add_issue( f"Data Entity '{entity.id}' is not web-based, " f"but in a detached RO-Crate all Data Entities " @@ -151,18 +186,16 @@ class DataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: result = True root_entity_id = None root_entity_is_local = False - root_entity_absolute_path = None with contextlib.suppress(ValueError): root_data_entity = context.ro_crate.metadata.get_root_data_entity() root_entity_id = root_data_entity.id root_entity_is_local = ( root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False ) - root_entity_absolute_path = root_data_entity.id_as_path if root_data_entity.has_absolute_path() else None try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: @@ -172,7 +205,7 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # for entity in entities: if root_entity_id and entity.id == root_entity_id: continue - if not root_entity_is_local and not entity.is_remote(): + if not root_entity_is_local and not is_external_reference(entity.id): context.result.add_issue( f"Data Entity '{entity.id}' has a local identifier but the Root Data Entity " "does not have a local identifier", @@ -183,22 +216,9 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: # return False if entity.has_local_identifier(): continue - if "\\" in entity.id or " " in entity.id: - context.result.add_issue( - f"Data Entity '{entity.id}' has an invalid @id; use URI-compatible paths", self - ) - result = False - if context.fail_fast: - return False - if ( - root_entity_is_local - and not str(entity.id_as_path).startswith(str(root_entity_absolute_path)) - and not str(entity.id).startswith("./") - and (str(entity.id).startswith(("/", "file://"))) - ): - context.result.add_issue( - f"Data Entity '{entity.id}' MUST use a relative @id within the RO-Crate root", self - ) + path_error = _data_entity_path_error(entity.id) + if path_error: + context.result.add_issue(f"Data Entity '{entity.id}' {path_error}", self) result = False if context.fail_fast: return False @@ -223,17 +243,44 @@ def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: context.record_skip(self, "metadata descriptor is not available", "exception") return CheckResult.SKIPPED for entity in entities: - if entity.has_local_identifier() or entity.is_remote(): - continue - if entity.has_absolute_path() and ( - context.ro_crate.has_file(entity.id_as_path) or context.ro_crate.has_directory(entity.id_as_path) + # This check only applies to attached crates with local filesystem + # resources; local IDs and malformed paths are handled elsewhere. + if ( + entity.has_local_identifier() + or _data_entity_path_error(entity.id) + or entity.is_remote() + or not context.ro_crate.uri.is_local_resource() ): - context.result.add_issue( - f"Data Entity '{entity.id}' should use a relative @id within the RO-Crate root", self - ) - result = False - if context.fail_fast: - return False + continue + # An absolute file URI may identify an external local resource. Only + # files actually inside the package must use a relative identifier. + # Parse URI paths explicitly to handle file:/ and localhost, and + # compare path components rather than textual prefixes. + if not (is_external_reference(entity.id) or entity.has_absolute_path()): + continue + + # URI paths are percent-encoded, so decode them before creating a + # filesystem path. Non-file absolute paths already have a path form. + parsed = urlsplit(entity.id) + path = Path(unquote(parsed.path)) if parsed.scheme == "file" else entity.id_as_path + # Resolve both paths before comparing them; this also follows symlinks, + # so containment is evaluated against their canonical filesystem targets. + root_path = context.ro_crate.uri.as_path().resolve() + path = path.resolve() + if not path.is_relative_to(root_path): + continue + + # An absolute path inside the root is only subject to this rule if + # it resolves to an actual file or directory in the crate payload. + payload_path = path.relative_to(root_path) + if not (context.ro_crate.has_file(payload_path) or context.ro_crate.has_directory(payload_path)): + continue + context.result.add_issue( + f"Data Entity '{entity.id}' MUST use a relative @id within the RO-Crate root", self + ) + result = False + if context.fail_fast: + return False return result From 9f12712b3543afd61f9b8b559f32d37527669ae4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 14:40:51 +0200 Subject: [PATCH 166/271] =?UTF-8?q?test(profiles):=20=F0=9F=A7=AA=20cover?= =?UTF-8?q?=20absolute=20file=20URI=20Data=20Entities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../invalid/ro-crate-metadata.json | 4 +- .../invalid/ro-crate-metadata.json | 4 +- .../ro-crate/test_file_uri_data_entities.py | 99 +++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/integration/profiles/ro-crate/test_file_uri_data_entities.py diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json index 23199ec88..088c7b8ba 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json @@ -22,12 +22,12 @@ }, "hasPart": [ { - "@id": "file://./my-test-file.txt" + "@id": "/my-test-file.txt" } ] }, { - "@id": "file://./my-test-file.txt", + "@id": "/my-test-file.txt", "@type": "File", "name": "My Test File", "description": "A test file for RO-Crate.", diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json index 0e16314aa..67e894e40 100644 --- a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json @@ -22,12 +22,12 @@ }, "hasPart": [ { - "@id": "file://./my-test-file.txt" + "@id": "/my-test-file.txt" } ] }, { - "@id": "file://./my-test-file.txt", + "@id": "/my-test-file.txt", "@type": "File", "name": "My Test File", "description": "A test file for RO-Crate.", diff --git a/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py b/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py new file mode 100644 index 000000000..7fb72ae62 --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py @@ -0,0 +1,99 @@ +"""Absolute file URIs identify resources independently of payload membership.""" + +import json +from pathlib import Path + +import pytest + +from rocrate_validator import models +from tests.shared import do_entity_test + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize("form", ["file://", "file:", "file://localhost", "FILE://LOCALHOST"]) +@pytest.mark.parametrize("location", ["external-missing", "external-present", "payload"]) +def test_file_uri_payload_membership(tmp_path, version, form, location): + crate = tmp_path / "crate" + crate.mkdir() + # A sibling sharing the textual prefix must not count as payload. + external = tmp_path / "crate-external" + external.mkdir() + target = (crate if location == "payload" else external) / "a file.txt" + if location != "external-missing": + target.write_text("data") + entity_id = form + target.as_posix().replace(" ", "%20") + _validate(crate, version, entity_id, location != "payload") + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize( + "entity_id", + [ + "file:///nonexistent/data.txt", + "file:/nonexistent/data.txt", + "file://localhost/nonexistent/data.txt", + "file://example.org/data.txt", + ], +) +def test_detached_file_uri(tmp_path, version, entity_id): + _validate(tmp_path, version, entity_id, True, detached=True) + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize("entity_id", ["missing.txt", "/missing.txt", "file:///a file.txt"]) +def test_invalid_data_entity_references(tmp_path, version, entity_id): + _validate(tmp_path, version, entity_id, False) + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize( + "entity_id, issue", + [ + ("file:relative.txt", "file URI path MUST be absolute"), + ("file://localhost", "file URI path MUST be absolute"), + ("file://example.org", "file URI path MUST be absolute"), + ("C:/data/x", "Windows drive path"), + ("//example.org/data.txt", "network-path reference"), + ], +) +@pytest.mark.parametrize("detached", [False, True]) +def test_identifier_path_syntax(tmp_path, version, entity_id, issue, detached): + _validate(tmp_path, version, entity_id, False, detached=detached, expected_issue=issue) + + +def _validate(crate, version, entity_id, valid, detached=False, expected_issue=None): + source = Path( + f"tests/data/crates/rocrate-{version}/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json" + ) + metadata = json.loads(source.read_text()) + graph = metadata["@graph"] + root = next(e for e in graph if e["@id"] == "./") + files = [e for e in graph if e.get("@type") == "File"] + graph[:] = [e for e in graph if e not in files] + root["hasPart"] = [{"@id": entity_id}] + graph.append({"@id": entity_id, "@type": "File", "name": "Test file"}) + if detached: + root["@id"] = "https://example.org/crate/" + graph[0]["about"] = {"@id": root["@id"]} + descriptor = crate / "ro-crate-metadata.json" + descriptor.write_text(json.dumps(metadata)) + do_entity_test( + descriptor if detached else crate, + models.Severity.REQUIRED, + valid, + expected_triggered_issues=( + [expected_issue] + if expected_issue + else [] + if valid + else [ + "does not include the Data Entity" + if entity_id == "missing.txt" + else "invalid @id" + if " " in entity_id + else "MUST use a relative @id within the RO-Crate root" + ] + ), + profile_identifier=f"ro-crate-{version}", + skip_availability_check=True, + ) From afaf98dacef3d46cdf0cf1b1c303e6f8155281f5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 16:28:29 +0200 Subject: [PATCH 167/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.12.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eed4c183..7f4fb64f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.12.2] - 2026-10-05 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.1...0.12.2 + +### 🐛 Fixed + +- fix(profiles): handle absolute file URI Data Entity identifiers ([afb7b0d2](https://github.com/kikkomep/rocrate-validator/commit/afb7b0d2b5e245f81f5f36f6d1ade43b0f2c53cb)) +- fix(profiles): accept declared compact IRIs in JSON-LD properties ([050dd18a](https://github.com/kikkomep/rocrate-validator/commit/050dd18a2e3185d490ea95ee2d68cbb0470903a8)) + +### 🔧 Changed + +- test(profiles): cover absolute file URI Data Entities ([9f12712b](https://github.com/kikkomep/rocrate-validator/commit/9f12712b3543afd61f9b8b559f32d37527669ae4)) +- test(jsonld): cover compact IRI prefixes across RO-Crate profiles ([cdb6e04e](https://github.com/kikkomep/rocrate-validator/commit/cdb6e04edcfba8a8eb29b36a443ce3e631f008d4)) + ## [0.12.1] - 2026-10-01 Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.0...0.12.1 From 36b98e316b7c2b24b293ffeabf0271f5e005962d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 16:29:25 +0200 Subject: [PATCH 168/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.12.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 858857558..adf0cf2fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.12.1" +version = "0.12.2" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 76812ca15d9fc1a28a94faef3fd516e8c2f0c8f4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 16:43:22 +0200 Subject: [PATCH 169/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20alig?= =?UTF-8?q?n=20overlay=20loading=20test=20with=20prepared=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/requirements/test_profiles.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 5f7f760b8..dad313987 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -665,7 +665,7 @@ def test_shacl_overlay_profiles_are_loaded_once_before_processing( check_overriding_profiles_path: str, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Load every overlay profile graph once while allowing deferred re-entry.""" + """Add each overlay profile's shapes once while allowing deferred re-entry.""" settings = ValidationSettings( profiles_path=Path(check_overriding_profiles_path), profile_identifier="b", @@ -677,21 +677,21 @@ def test_shacl_overlay_profiles_are_loaded_once_before_processing( source = next(profile for profile in context.profiles if profile.identifier == "a") target = next(profile for profile in context.profiles if profile.identifier == "b") shacl_context = SHACLValidationContext.get_instance(context) - loaded_paths: list[Path] = [] + shape_registry_loads = 0 + extend_shapes = shacl_context.shapes_registry.extend - def load_ontology(profile_path: Path) -> Graph: - """Record ontology loads without parsing external graph content.""" - loaded_paths.append(profile_path) - return Graph() + def record_shape_load(shapes: dict[str, Any], graph: Graph) -> None: + """Count each profile contribution to the combined SHACL graph.""" + nonlocal shape_registry_loads + shape_registry_loads += 1 + extend_shapes(shapes, graph) - monkeypatch.setattr(shacl_context, "__load_ontology_graph__", load_ontology) + monkeypatch.setattr(shacl_context.shapes_registry, "extend", record_shape_load) assert shacl_context.__set_current_validation_profile__(source) assert shacl_context.__set_current_validation_profile__(source) assert shacl_context.__set_current_validation_profile__(target) - assert loaded_paths == [source.path, target.path] - assert shacl_context.__get_ontology_path__(source.path) == source.path / "ontology.ttl" - assert shacl_context.__get_ontology_path__(target.path) == target.path / "ontology.ttl" + assert shape_registry_loads == 2 shacl_context.current_validation_result = True with pytest.raises(SHACLValidationAlreadyProcessed): From 153efa655ca9b210fe3f796e0ba3bf3a58543e8d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Oct 2026 17:05:01 +0200 Subject: [PATCH 170/271] fix(docs): prevent Sphinx failures on README includes --- docs/1_installation.rst | 4 ++-- docs/2_usage_cli.rst | 6 ++++-- docs/3_usage_api.rst | 4 ++-- docs/ack.rst | 2 +- docs/conf.py | 7 +++---- docs/index.rst | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/1_installation.rst b/docs/1_installation.rst index eba3d6ec3..c05754086 100644 --- a/docs/1_installation.rst +++ b/docs/1_installation.rst @@ -28,5 +28,5 @@ Installation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 34 - :end-line: 92 + :start-after: ## Installation + :end-before: ## CLI-based Validation diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index b9bb522c8..4169efce3 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -14,6 +14,8 @@ limitations under the License. +.. _cli-based-validation: + CLI Validation ============== @@ -29,8 +31,8 @@ CLI Validation .. Include the README section for the CLI validation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 94 - :end-line: 120 + :start-after: ## CLI-based Validation + :end-before: ## Programmatic Validation .. seealso:: diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index ad034a9eb..673f6ec27 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -27,8 +27,8 @@ Programmatic Validation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 122 - :end-line: 162 + :start-after: ## Programmatic Validation + :end-before: ## Running the tests .. seealso:: diff --git a/docs/ack.rst b/docs/ack.rst index e8d5b726e..2e95aa310 100644 --- a/docs/ack.rst +++ b/docs/ack.rst @@ -20,4 +20,4 @@ Acknowledgements .. "## Acknowledgements" heading is skipped: the page title above replaces it). .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 227 + :start-after: ## Acknowledgements diff --git a/docs/conf.py b/docs/conf.py index 2402e9861..d04c02732 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,10 +73,9 @@ "sphinx_copybutton", ] -# Only auto-generate section labels for the top two heading levels: deeper -# subsections (e.g. the repeated "SHACL checks" / "Python checks" headings) -# would otherwise produce duplicate-label warnings within the same document. -autosectionlabel_maxdepth = 2 +# Leave autosectionlabel_maxdepth unset: nested sections imported from Markdown +# via the include directive's parser option retain a temporary document, which +# breaks Sphinx's section-depth calculation. # Warnings raised while embedding the Markdown README into the Sphinx pages. # The README is the canonical GitHub document: its slices intentionally start diff --git a/docs/index.rst b/docs/index.rst index 9983d1ad6..88f26c486 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,8 +21,8 @@ Welcome to rocrate-validator's documentation! .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 2 - :end-line: 32 + :start-after: # rocrate-validator + :end-before: ## Installation .. include:: 0_toc.rst From 2f4627bbf77cd600852f73996af7ce30fce714a3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 15:00:52 +0200 Subject: [PATCH 171/271] =?UTF-8?q?docs(validation):=20=F0=9F=93=9D=20clar?= =?UTF-8?q?ify=20explicit=20JSON-LD=20base=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/3_usage_api.rst | 6 ++++-- docs/4_how_it_works.rst | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 801fb0e0a..91127ff79 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -110,8 +110,10 @@ public ID, relative IRIs in profile artifacts are compiled against a stable internal base. Per-run copies of the SHACL and ontology graphs rebase only those relative terms to the current crate, while structural shape identifiers and the crate data graph remain unchanged. Metadata whose explicit ``@base`` differs -from its crate public ID retains a base-specific prepared plan. After editing -profile files in a long-running process, call +from its crate public ID causes the validator to keep a separate prepared plan +for that base combination, limiting plan reuse across crate IDs. Validation +behavior is unchanged. After editing profile files in a long-running process, +call ``validator.clear_prepared_profiles()`` or ``validator.prepare(refresh=True)`` before the next validation. diff --git a/docs/4_how_it_works.rst b/docs/4_how_it_works.rst index 876f0775d..e2ca8f08c 100644 --- a/docs/4_how_it_works.rst +++ b/docs/4_how_it_works.rst @@ -59,9 +59,12 @@ and preserves structural shape identifiers. The crate data graph itself is never rewritten. This permits different crate locations to share preparation without leaking their public IDs into one another. -An explicit JSON-LD ``@base`` different from the crate public ID retains a -base-specific prepared plan. This conservative boundary preserves the two RDF -spaces used by existing profiles rather than collapsing them during rebasing. +If the metadata declares an explicit JSON-LD ``@base`` that differs from the +crate public ID, relative identifiers in the metadata resolve against that +``@base`` rather than the crate root. Since profile artifacts may contain terms +from both bases, the validator keeps a separate prepared plan for this base +combination instead of rebasing it for reuse across crate IDs. Validation +behavior is unchanged; only cross-crate plan reuse is more limited. Validation profile selection From 5335934b826aec4e78186251e7be45a3f0098112 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 16:15:44 +0200 Subject: [PATCH 172/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20resol?= =?UTF-8?q?ve=20cached=20profile=20identifier=20for=20per-call=20validatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 8 +++++- tests/unit/test_warm_start.py | 34 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 500ce2f79..20e0a96c1 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -70,6 +70,7 @@ class PreparedValidationPlan: profiles: tuple[Profile, ...] ontology_graph: Graph + resolved_profile_identifier: str class Validator(Publisher): @@ -161,6 +162,7 @@ def __get_prepared_validation_plan__(self, context: ValidationContext) -> Prepar plan = self.__prepared_validation_plans.get(key) if plan is None: profiles = tuple(context.__load_profiles__()) + resolved_profile_identifier = context.profile_identifier ontology_graph = Graph() for profile in profiles: # Materialize lazy requirements and their per-profile shape @@ -174,12 +176,16 @@ def __get_prepared_validation_plan__(self, context: ValidationContext) -> Prepar format="ttl", publicID=context.prepared_ontology_base, ) - plan = PreparedValidationPlan(profiles, ontology_graph) + plan = PreparedValidationPlan(profiles, ontology_graph, resolved_profile_identifier) self.__prepared_validation_plans[key] = plan # Loading may resolve a bare profile token (for example # ``ro-crate``) to a versioned identifier and update the settings. # Store the canonical key as an alias for subsequent warm runs. self.__prepared_validation_plans[self.__prepared_validation_plan_key__(context)] = plan + # A per-call input uses a copy of the validator settings. On a cache hit + # that copy still contains a bare token, so restore the resolved target + # profile identifier before SHACL dispatch compares it with each profile. + context.settings.profile_identifier = plan.resolved_profile_identifier return plan def clear_prepared_profiles(self) -> None: diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 2fe25af85..653607911 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -207,6 +207,40 @@ def test_distinct_explicit_metadata_bases_keep_separate_plans(): assert first.context.prepared_validation_plan is not second.context.prepared_validation_plan +def test_cached_plan_resolves_bare_profile_token_for_each_per_call_validation(tmp_path): + """A cache hit must not defer every SHACL check for a per-call bare token.""" + crate = tmp_path / "crate" + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + # The selected versioned profile requires this descriptor to have + # an RDF type. Leave it out so each call must report a violation. + {"@id": "ro-crate-metadata.json", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + settings = ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=Path("tests/data/profiles/fake_versioned_profiles"), + profile_identifier="c", + offline=True, + ) + validator = Validator(settings) + + first_result = validator.validate(crate) + second_result = validator.validate(crate) + + assert not first_result.passed() + assert not second_result.passed() + assert len(first_result.executed_checks) > 0 + assert len(second_result.executed_checks) == len(first_result.executed_checks) + assert [issue.message for issue in second_result.get_issues()] == [ + issue.message for issue in first_result.get_issues() + ] + + def test_metadata_service_does_not_mutate_caller_settings(): crate = ValidROC().wrroc_paper with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: From 5edb72e4c19e41eb2b839293bb969610909722cc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 16:46:00 +0200 Subject: [PATCH 173/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20rebas?= =?UTF-8?q?e=20parent-relative=20profile=20identifiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/utils/rdf.py | 18 +++++++++++++ tests/unit/test_rdf.py | 46 ++++++++++++++++++++++++++++++++++ tests/unit/test_warm_start.py | 41 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 tests/unit/test_rdf.py diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 5b638efbe..228303232 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -13,6 +13,8 @@ # limitations under the License. from collections.abc import Iterable +from posixpath import relpath +from urllib.parse import urljoin, urlsplit, urlunsplit from rdflib import Graph, URIRef from rdflib.term import Node @@ -41,6 +43,22 @@ def rebase_node(node: Node, base_mappings: Iterable[tuple[str, str]]) -> Node: for source_base, target_base in base_mappings: if value.startswith(source_base): return URIRef(f"{target_base}{value[len(source_base) :]}") + + # Parsing resolves parent-relative references (for example + # ``../target``) against their source base, so the resulting URI no + # longer starts with that base. Preserve the reference's relative path + # when moving it to the prepared base. Restrict this fallback to the + # same URI authority; unrelated hosts are never affected. + value_parts = urlsplit(value) + for source_base, target_base in base_mappings: + source_parts = urlsplit(source_base) + if (value_parts.scheme, value_parts.netloc) != (source_parts.scheme, source_parts.netloc): + continue + relative_path = relpath(value_parts.path, source_parts.path) + rebased = urlsplit(urljoin(target_base, relative_path)) + return URIRef( + urlunsplit((rebased.scheme, rebased.netloc, rebased.path, value_parts.query, value_parts.fragment)) + ) return node return node diff --git a/tests/unit/test_rdf.py b/tests/unit/test_rdf.py new file mode 100644 index 000000000..c72898919 --- /dev/null +++ b/tests/unit/test_rdf.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rdflib import Graph, URIRef + +from rocrate_validator.utils.rdf import rebase_graph + + +def test_rebase_graph_maps_parent_relative_references_after_rdf_parsing(): + source_base = "https://example.org/crate/" + prepared_base = "https://example.invalid/rocrate-validator/prepared/crate/" + graph = Graph().parse( + data=""" + @prefix ex: <../> . + @prefix rdf: . + + ex:target rdf:type ex:Entity . + rdf:type ex:Entity . + """, + format="turtle", + publicID=source_base, + ) + + rebased = rebase_graph(graph, ((source_base, prepared_base),)) + + assert ( + URIRef("https://example.invalid/rocrate-validator/prepared/target"), + URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + URIRef("https://example.invalid/rocrate-validator/prepared/Entity"), + ) in rebased + assert ( + URIRef("https://outside.example/entity"), + URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + URIRef("https://example.invalid/rocrate-validator/prepared/Entity"), + ) in rebased diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 653607911..1b0a36ab5 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -14,6 +14,7 @@ import copy import json +import shutil import threading import time from pathlib import Path @@ -166,6 +167,46 @@ def counted_shape_load(self, *args, **kwargs): assert all(issue.violatingEntity == "./ro-crate-metadata.json" for issue in invalid_result.get_issues()) +def test_parent_relative_shape_target_is_rebased_for_validation(tmp_path): + """A parsed ``../`` target must follow each crate base after preparation.""" + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + shape_path = profiles_path / "c" / "shape_c.ttl" + shape_path.write_text( + """ + @prefix sh: . + @prefix schema: . + + <../ShapeC> a sh:NodeShape ; + sh:targetNode <../target> ; + sh:property [ sh:path schema:name ; sh:minCount 1 ; sh:message "target must have a name" ] . + """, + encoding="utf-8", + ) + crate = tmp_path / "crate" + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "../target", "@type": "Dataset"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + settings = ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + + result = Validator(settings).validate() + + assert not result.passed() + assert any(issue.message == "target must have a name" for issue in result.get_issues()) + + def test_per_call_metadata_does_not_mutate_validator_settings(): crate = ValidROC().wrroc_paper with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: From 3d35fbf8f0b9a4c113c8d6ecdb4314e5011c8116 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 16:54:21 +0200 Subject: [PATCH 174/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20rebas?= =?UTF-8?q?e=20class=20targets=20in=20structural=20shapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/shacl/validator.py | 5 ++ rocrate_validator/utils/rdf.py | 14 ++++-- tests/unit/test_rdf.py | 23 ++++++++- tests/unit/test_warm_start.py | 48 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index f39b4302c..39cb8e5fe 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -174,10 +174,15 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: for subject, predicate, object_ in profile_shapes_graph if str(predicate).startswith(SHACL_NS) or (predicate == RDF.type and str(object_).startswith(SHACL_NS)) } + shacl_ns = Namespace(SHACL_NS) profile_shapes_graph = rebase_graph( profile_shapes_graph, self._run_base_mappings, preserve_nodes=structural_nodes, + # A URI can identify both a shape and an RDF class. Keep its + # shape identifier stable in structural positions, while + # rebasing it when it supplies SHACL class semantics. + rebase_objects_for=(shacl_ns.targetClass, shacl_ns["class"]), ) logger.debug("Loaded shapes: %s", profile_shapes) diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 228303232..64a3e43de 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -68,6 +68,7 @@ def rebase_graph( base_mappings: Iterable[tuple[str, str]], *, preserve_nodes: Iterable[Node] = (), + rebase_objects_for: Iterable[Node] = (), ) -> Graph: """ Copy a graph while replacing selected URI bases. @@ -78,15 +79,22 @@ def rebase_graph( mappings = tuple(base_mappings) preserved = frozenset(preserve_nodes) + rebased_object_predicates = frozenset(rebase_objects_for) - def transform(node: Node) -> Node: - return node if node in preserved else rebase_node(node, mappings) + def transform(node: Node, *, force_rebase: bool = False) -> Node: + return node if node in preserved and not force_rebase else rebase_node(node, mappings) rebased = Graph() for prefix, namespace in graph.namespaces(): rebased.bind(prefix, namespace) for subject, predicate, object_ in graph: - rebased.add((transform(subject), transform(predicate), transform(object_))) + rebased.add( + ( + transform(subject), + transform(predicate), + transform(object_, force_rebase=predicate in rebased_object_predicates), + ) + ) return rebased diff --git a/tests/unit/test_rdf.py b/tests/unit/test_rdf.py index c72898919..b50a969ca 100644 --- a/tests/unit/test_rdf.py +++ b/tests/unit/test_rdf.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rdflib import Graph, URIRef +from rdflib import Graph, Namespace, URIRef +from rdflib.namespace import RDF from rocrate_validator.utils.rdf import rebase_graph @@ -44,3 +45,23 @@ def test_rebase_graph_maps_parent_relative_references_after_rdf_parsing(): URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), URIRef("https://example.invalid/rocrate-validator/prepared/Entity"), ) in rebased + + +def test_rebase_graph_rebases_structural_node_when_used_as_target_class(): + prepared_base = "https://example.invalid/rocrate-validator/prepared/crate/" + crate_base = "file:///tmp/crate/" + sh = Namespace("http://www.w3.org/ns/shacl#") + class_shape = URIRef(f"{prepared_base}Dataset") + graph = Graph() + graph.add((class_shape, RDF.type, sh.NodeShape)) + graph.add((class_shape, sh.targetClass, class_shape)) + + rebased = rebase_graph( + graph, + ((prepared_base, crate_base),), + preserve_nodes=(class_shape,), + rebase_objects_for=(sh.targetClass,), + ) + + assert (class_shape, RDF.type, sh.NodeShape) in rebased + assert (class_shape, sh.targetClass, URIRef("file:///tmp/crate/Dataset")) in rebased diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 1b0a36ab5..03f0726a3 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -207,6 +207,54 @@ def test_parent_relative_shape_target_is_rebased_for_validation(tmp_path): assert any(issue.message == "target must have a name" for issue in result.get_issues()) +def test_structural_shape_identifier_is_rebased_as_target_class(tmp_path): + """A shape URI that is also a class must rebase in ``sh:targetClass``.""" + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + shape_path = profiles_path / "c" / "shape_c.ttl" + shape_path.write_text( + """ + @prefix rel: <./> . + @prefix rdfs: . + @prefix schema: . + @prefix sh: . + + rel:Dataset a sh:NodeShape, rdfs:Class ; + sh:targetClass rel:Dataset ; + sh:property [ sh:path schema:name ; sh:minCount 1 ; + sh:message "class-target check must run" ] . + """, + encoding="utf-8", + ) + crate = tmp_path / "crate" + crate.mkdir() + target_class = f"{crate.as_uri()}/Dataset" + metadata = { + "@context": { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "TargetDataset": {"@id": target_class}, + }, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "./item", "@type": "TargetDataset"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + settings = ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + + result = Validator(settings).validate() + + assert not result.passed() + assert any(issue.message == "class-target check must run" for issue in result.get_issues()) + + def test_per_call_metadata_does_not_mutate_validator_settings(): crate = ValidROC().wrroc_paper with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: From 6741426d692d5f4e23be00904968393fe154ddbd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 17:09:48 +0200 Subject: [PATCH 175/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20prese?= =?UTF-8?q?rve=20implicit=20class=20targets=20across=20crate=20bases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/shacl/checks.py | 2 +- .../requirements/shacl/validator.py | 26 ++++- tests/unit/test_warm_start.py | 95 +++++++++++++------ 3 files changed, 88 insertions(+), 35 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 5cb028789..fda7868a3 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -297,7 +297,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # Begin the timer start_time = timer() - shapes_graph = shapes_registry.shapes_graph + shapes_graph = shacl_context.shapes_graph end_time = timer() logger.debug(f"Execution time for getting shapes: {end_time - start_time} seconds") diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 39cb8e5fe..e764f9d35 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -19,7 +19,7 @@ import pyshacl from rdflib import BNode, Graph, Literal, Namespace -from rdflib.namespace import RDF +from rdflib.namespace import OWL, RDF, RDFS from rdflib.term import Node, URIRef if TYPE_CHECKING: @@ -50,7 +50,7 @@ ) from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging -from rocrate_validator.utils.rdf import rebase_graph +from rocrate_validator.utils.rdf import rebase_graph, rebase_node # set up logging logger = logging.getLogger(__name__) @@ -250,7 +250,27 @@ def shapes_registry(self) -> ShapesRegistry: @property def shapes_graph(self) -> Graph: - return self.shapes_registry.shapes_graph + graph = self.shapes_registry.shapes_graph + # Resolve implicit targets after profiles have been merged: the class + # declaration and its metaclass definition may belong to different profiles. + # Include owl:Class, which pySHACL treats as a subclass of rdfs:Class. + class_types: set[Node] = {RDFS.Class, OWL.Class} + class_types.update(graph.subjects(RDFS.subClassOf, RDFS.Class)) + shacl = Namespace(SHACL_NS) + for shape in self.shapes_registry.get_shapes().values(): + node = shape.node + target_class = rebase_node(node, self._run_base_mappings) + if target_class == node: + continue + for class_type in set(graph.objects(node, RDF.type)) & class_types: + # Retain the canonical shape identity for check/report lookup, + # but express its implicit class target using the real crate URI. + # Moving the class declaration also prevents pySHACL from adding + # an unintended second target at the canonical preparation URI. + graph.remove((node, RDF.type, class_type)) + graph.add((target_class, RDF.type, class_type)) + graph.add((node, shacl.targetClass, target_class)) + return graph @property def ontology_graph(self) -> Graph: diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 03f0726a3..2d9994f16 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -207,8 +207,13 @@ def test_parent_relative_shape_target_is_rebased_for_validation(tmp_path): assert any(issue.message == "target must have a name" for issue in result.get_issues()) -def test_structural_shape_identifier_is_rebased_as_target_class(tmp_path): - """A shape URI that is also a class must rebase in ``sh:targetClass``.""" +@pytest.mark.parametrize("explicit_target", [False, True], ids=["implicit", "explicit"]) +@pytest.mark.parametrize("class_type", ["rdfs:Class", "owl:Class", "rel:MetaClass"]) +@pytest.mark.parametrize("canonical_instance", [False, True], ids=["real-only", "canonical-decoy"]) +def test_structural_shape_identifier_is_rebased_as_target_class( + tmp_path, explicit_target, class_type, canonical_instance +): + """Class targets must follow the crate without selecting canonical-base instances.""" profiles_path = tmp_path / "profiles" shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) shape_path = profiles_path / "c" / "shape_c.ttl" @@ -216,43 +221,71 @@ def test_structural_shape_identifier_is_rebased_as_target_class(tmp_path): """ @prefix rel: <./> . @prefix rdfs: . + @prefix owl: . @prefix schema: . @prefix sh: . - rel:Dataset a sh:NodeShape, rdfs:Class ; - sh:targetClass rel:Dataset ; + rel:Dataset a sh:NodeShape, CLASS_TYPE ; + TARGET sh:property [ sh:path schema:name ; sh:minCount 1 ; sh:message "class-target check must run" ] . - """, + """.replace("CLASS_TYPE", class_type).replace( + "TARGET", "sh:targetClass rel:Dataset ;" if explicit_target else "" + ), encoding="utf-8", ) - crate = tmp_path / "crate" - crate.mkdir() - target_class = f"{crate.as_uri()}/Dataset" - metadata = { - "@context": { - "@vocab": "http://schema.org/", - "about": {"@type": "@id"}, - "TargetDataset": {"@id": target_class}, - }, - "@graph": [ - {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, - {"@id": "./", "@type": "Dataset"}, - {"@id": "./item", "@type": "TargetDataset"}, - ], - } - (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") - settings = ValidationSettings( - rocrate_uri=URI(crate), - profiles_path=profiles_path, - profile_identifier="c", - offline=True, + inherited_shape = profiles_path / "a" / "shape_a.ttl" + inherited_shape.write_text( + inherited_shape.read_text(encoding="utf-8") + "\n " + " .\n", + encoding="utf-8", ) - - result = Validator(settings).validate() - - assert not result.passed() - assert any(issue.message == "class-target check must run" for issue in result.get_issues()) + first_plan = None + for crate_name in ("crate-a", "crate-b"): + crate = tmp_path / crate_name + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "./item", "@type": f"{crate.as_uri()}/Dataset", "name": "Present"}, + ], + } + if canonical_instance: + # This unrelated instance must not become an extra implicit target. + metadata["@graph"].append( + { + "@id": "./unrelated", + "@type": "https://example.invalid/rocrate-validator/prepared/crate/Dataset", + } + ) + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + if first_plan is None: + validator = Validator( + ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + ) + for valid in (True, False): + if not valid: + metadata["@graph"][2].pop("name") + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + result = validator.validate(crate) + assert result.passed() is valid + if not valid: + assert [issue.message for issue in result.get_issues()] == ["class-target check must run"] + assert result.get_issues()[0].violatingEntity == "./item" + plan = result.context.prepared_validation_plan + registry = ShapesRegistry.get_instance(result.context.profiles[-1]) + if first_plan is None: + first_plan = plan + prepared_shapes = registry.shapes_graph + assert plan is first_plan + assert isomorphic(prepared_shapes, registry.shapes_graph) def test_per_call_metadata_does_not_mutate_validator_settings(): From 6d7021f5914763d26ecda5b4410ac618d58effc6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 17:29:19 +0200 Subject: [PATCH 176/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20prese?= =?UTF-8?q?rve=20URI=20path=20semantics=20when=20rebasing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/utils/rdf.py | 22 +++++++++++++++++++++- tests/unit/test_rdf.py | 32 ++++++++++++++++++++++++++++++-- tests/unit/test_warm_start.py | 15 ++++++++++++--- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 64a3e43de..883babdaf 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -13,7 +13,7 @@ # limitations under the License. from collections.abc import Iterable -from posixpath import relpath +from posixpath import commonpath, relpath from urllib.parse import urljoin, urlsplit, urlunsplit from rdflib import Graph, URIRef @@ -54,7 +54,27 @@ def rebase_node(node: Node, base_mappings: Iterable[tuple[str, str]]) -> Node: source_parts = urlsplit(source_base) if (value_parts.scheme, value_parts.netloc) != (source_parts.scheme, source_parts.netloc): continue + + # A root-relative identifier is anchored at the URI authority, not + # at the crate directory. Preserve its absolute path while mapping + # the URI scheme/authority to the current crate's URI space. + if value_parts.path.startswith("/") and commonpath((source_parts.path, value_parts.path)) == "/": + target_parts = urlsplit(target_base) + return URIRef( + urlunsplit( + ( + target_parts.scheme, + target_parts.netloc, + value_parts.path, + value_parts.query, + value_parts.fragment, + ) + ) + ) + relative_path = relpath(value_parts.path, source_parts.path) + if value_parts.path.endswith("/") and not relative_path.endswith("/"): + relative_path += "/" rebased = urlsplit(urljoin(target_base, relative_path)) return URIRef( urlunsplit((rebased.scheme, rebased.netloc, rebased.path, value_parts.query, value_parts.fragment)) diff --git a/tests/unit/test_rdf.py b/tests/unit/test_rdf.py index b50a969ca..963b668aa 100644 --- a/tests/unit/test_rdf.py +++ b/tests/unit/test_rdf.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from urllib.parse import urljoin + from rdflib import Graph, Namespace, URIRef from rdflib.namespace import RDF -from rocrate_validator.utils.rdf import rebase_graph +from rocrate_validator.utils.rdf import PREPARED_PROFILE_BASE, rebase_graph, rebase_node def test_rebase_graph_maps_parent_relative_references_after_rdf_parsing(): - source_base = "https://example.org/crate/" + source_base = "https://example.org/a/b/crate/" prepared_base = "https://example.invalid/rocrate-validator/prepared/crate/" graph = Graph().parse( data=""" @@ -65,3 +67,29 @@ def test_rebase_graph_rebases_structural_node_when_used_as_target_class(): assert (class_shape, RDF.type, sh.NodeShape) in rebased assert (class_shape, sh.targetClass, URIRef("file:///tmp/crate/Dataset")) in rebased + + +def test_rebase_node_preserves_parent_relative_trailing_slash(): + target_base = "file:///data/team/project/crate/" + source_uri = URIRef(urljoin(PREPARED_PROFILE_BASE, "../Dataset/")) + + rebased = rebase_node(source_uri, ((PREPARED_PROFILE_BASE, target_base),)) + + assert rebased == URIRef("file:///data/team/project/Dataset/") + + +def test_rebase_node_preserves_root_relative_path(): + target_base = "file:///data/team/project/crate/" + source_uri = URIRef("https://example.invalid/Dataset") + + rebased = rebase_node(source_uri, ((PREPARED_PROFILE_BASE, target_base),)) + + assert rebased == URIRef("file:///Dataset") + + +def test_rebase_node_leaves_absolute_iri_on_another_authority_unchanged(): + absolute_iri = URIRef("https://schema.org/Dataset") + + rebased = rebase_node(absolute_iri, ((PREPARED_PROFILE_BASE, "file:///data/crate/"),)) + + assert rebased == absolute_iri diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 2d9994f16..2df4c1021 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -200,11 +200,20 @@ def test_parent_relative_shape_target_is_rebased_for_validation(tmp_path): profile_identifier="c", offline=True, ) + validator = Validator(settings) - result = Validator(settings).validate() + for valid in (True, False): + if valid: + metadata["@graph"][2]["name"] = "Present" + else: + metadata["@graph"][2].pop("name") + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + result = validator.validate() - assert not result.passed() - assert any(issue.message == "target must have a name" for issue in result.get_issues()) + assert result.passed() is valid + if not valid: + assert any(issue.message == "target must have a name" for issue in result.get_issues()) + assert result.get_issues()[0].violatingEntity == (crate.parent / "target").as_uri() @pytest.mark.parametrize("explicit_target", [False, True], ids=["implicit", "explicit"]) From dc258c3a9a384df9cee9181151d67d9e4a936c5f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 18:01:02 +0200 Subject: [PATCH 177/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20prese?= =?UTF-8?q?rve=20absolute=20IRIs=20during=20prepared=20graph=20rebasing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 20 +++- .../requirements/shacl/models.py | 8 ++ rocrate_validator/requirements/shacl/utils.py | 17 +++- .../requirements/shacl/validator.py | 2 + rocrate_validator/utils/rdf.py | 97 +++++++++++++++++++ tests/unit/requirements/test_shacl_utils.py | 46 +++++++++ tests/unit/test_warm_start.py | 60 +++++++++++- 7 files changed, 235 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 20e0a96c1..340a2e482 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -50,10 +50,12 @@ from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import find_offline_cache_miss -from rocrate_validator.utils.rdf import PREPARED_PROFILE_BASE, extract_base_from_jsonld +from rocrate_validator.utils.rdf import PREPARED_PROFILE_BASE, extract_base_from_jsonld, parse_turtle_with_relative_iris from rocrate_validator.utils.uri import URI if TYPE_CHECKING: + from rdflib.term import Node + from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck @@ -70,6 +72,7 @@ class PreparedValidationPlan: profiles: tuple[Profile, ...] ontology_graph: Graph + ontology_relative_iris: frozenset[Node] resolved_profile_identifier: str @@ -164,6 +167,7 @@ def __get_prepared_validation_plan__(self, context: ValidationContext) -> Prepar profiles = tuple(context.__load_profiles__()) resolved_profile_identifier = context.profile_identifier ontology_graph = Graph() + ontology_relative_iris: set[Node] = set() for profile in profiles: # Materialize lazy requirements and their per-profile shape # registries once. Contextual graphs passed to pySHACL are still @@ -171,12 +175,18 @@ def __get_prepared_validation_plan__(self, context: ValidationContext) -> Prepar _ = profile.requirements ontology_path = profile.path / "ontology.ttl" if ontology_path.exists(): - ontology_graph.parse( + profile_ontology_graph, relative_iris = parse_turtle_with_relative_iris( ontology_path, - format="ttl", - publicID=context.prepared_ontology_base, + context.prepared_ontology_base, ) - plan = PreparedValidationPlan(profiles, ontology_graph, resolved_profile_identifier) + ontology_graph += profile_ontology_graph + ontology_relative_iris.update(relative_iris) + plan = PreparedValidationPlan( + profiles, + ontology_graph, + frozenset(ontology_relative_iris), + resolved_profile_identifier, + ) self.__prepared_validation_plans[key] = plan # Loading may resolve a bare profile token (for example # ``ro-crate``) to a versioned identifier and update the settings. diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index c7b439c89..6a0e336a7 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -301,6 +301,7 @@ class ShapesRegistry: def __init__(self): self._shapes: dict[str, Shape] = {} self._shapes_graph: Graph = Graph() + self._relative_iris: set[Node] = set() def add_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" @@ -338,6 +339,11 @@ def shapes_graph(self) -> Graph: g += self._shapes_graph return g + @property + def relative_iris(self) -> set[Node]: + """IRIs in loaded Turtle shapes that were written as relative references.""" + return self._relative_iris.copy() + def is_node_deactivated(self, node: Node) -> bool: """Return True if the underlying shapes graph asserts ` sh:deactivated true`. Avoids the copy made by `shapes_graph` @@ -355,6 +361,7 @@ def load_shapes(self, shapes_path: str | Path, publicID: str | None = None) -> l logger.debug(f"Loading shapes from: {shapes_path}") # load shapes (nodes and properties) from the shapes graph shapes_list: ShapesList = ShapesList.load_from_file(str(shapes_path), publicID) + self._relative_iris.update(shapes_list.relative_iris) logger.debug(f"Shapes List: {shapes_list}") # append the partial shapes graph to the global shapes graph @@ -429,6 +436,7 @@ def __repr__(self): def clear(self): self._shapes.clear() self._shapes_graph = Graph() + self._relative_iris.clear() @classmethod def get_instance(cls, ctx: object): diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 2eff9df10..d518ca352 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -27,6 +27,7 @@ from rocrate_validator.errors import BadSyntaxError from rocrate_validator.models import Severity from rocrate_validator.utils import log as logging +from rocrate_validator.utils.rdf import parse_turtle_with_relative_iris if TYPE_CHECKING: from rocrate_validator.requirements.shacl.models import Shape @@ -171,11 +172,18 @@ def __init__( property_shapes: list[Node], shapes_graphs: dict[Node, Graph], shapes_graph: Graph, + relative_iris: set[Node] | None = None, ): self._node_shapes = node_shapes self._property_shapes = property_shapes self._shapes_graph = shapes_graph self._shapes_graphs = shapes_graphs + self._relative_iris = set() if relative_iris is None else relative_iris + + @property + def relative_iris(self) -> set[Node]: + """Return IRIs whose Turtle spelling was relative before RDFLib resolved it.""" + return self._relative_iris.copy() @property def node_shapes(self) -> list[Node]: @@ -290,10 +298,9 @@ def load_shapes_from_file(file_path: str, publicID: str | None = None) -> Shapes # Check the file path is not None assert file_path is not None, "The file path cannot be None" # Load the graph from the file - g = Graph() - g.parse(file_path, format="turtle", publicID=publicID) + g, relative_iris = parse_turtle_with_relative_iris(file_path, publicID) # Extract the shapes from the graph - return load_shapes_from_graph(g) + return load_shapes_from_graph(g, relative_iris=relative_iris) except Exception as e: line = getattr(e, "lines", None) line = line + 1 if isinstance(line, int) else None @@ -313,7 +320,7 @@ def _get_syntax_error_character(error: Exception) -> int | None: return index - line_start + 1 -def load_shapes_from_graph(g: Graph) -> ShapesList: +def load_shapes_from_graph(g: Graph, relative_iris: set[Node] | None = None) -> ShapesList: # define the SHACL namespace SHACL = Namespace(SHACL_NS) # find all NodeShapes @@ -335,7 +342,7 @@ def load_shapes_from_graph(g: Graph) -> ShapesList: subgraph.add((s, p, o)) subgraphs[shape] = subgraph - return ShapesList(node_shapes, property_shapes, subgraphs, g) + return ShapesList(node_shapes, property_shapes, subgraphs, g, relative_iris) def resolve_parent_shape(shapes_graph: Graph, source_shape_node: Node, shapes_registry) -> Shape | None: diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index e764f9d35..bce5da9d2 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -145,6 +145,7 @@ def __init__(self, context: ValidationContext): self._ontology_graph = rebase_graph( context.prepared_validation_plan.ontology_graph, self._run_base_mappings, + rebase_nodes=context.prepared_validation_plan.ontology_relative_iris, ) def __set_current_validation_profile__(self, profile: Profile) -> bool: @@ -179,6 +180,7 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: profile_shapes_graph, self._run_base_mappings, preserve_nodes=structural_nodes, + rebase_nodes=profile_registry.relative_iris, # A URI can identify both a shape and an RDF class. Keep its # shape identifier stable in structural positions, while # rebasing it when it supplies SHACL class semantics. diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 883babdaf..5ad966008 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -12,11 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re from collections.abc import Iterable +from pathlib import Path from posixpath import commonpath, relpath from urllib.parse import urljoin, urlsplit, urlunsplit from rdflib import Graph, URIRef +from rdflib.parser import create_input_source +from rdflib.plugins.parsers.notation3 import RDFSink, SinkParser from rdflib.term import Node from rocrate_validator import constants @@ -28,6 +32,95 @@ PREPARED_PROFILE_BASE = "https://example.invalid/rocrate-validator/prepared/crate/" +_PREFIX_DECLARATION = re.compile(r"@?prefix\b\s+([^:\s]*):\s*<([^>]*)>", re.IGNORECASE) + + +def _prefix_declaration(argstr: str, index: int) -> tuple[str, str] | None: + directive_index = argstr[index:].lstrip() if index >= 0 else "" + match = _PREFIX_DECLARATION.match(directive_index) + if match is None: + return None + prefix, namespace = match.group(1), match.group(2) + assert prefix is not None and namespace is not None + return prefix, namespace + + +def _is_relative_iri(reference: str) -> bool: + parts = urlsplit(reference) + return not parts.scheme and not parts.netloc + + +class _TurtleProvenanceParser(SinkParser): + """RDFLib Turtle parser that records URI terms originating in relative syntax.""" + + def __init__(self, store: RDFSink, base_uri: str): + super().__init__(store, baseURI=base_uri, turtle=True) + self.relative_iris: set[Node] = set() + self.relative_prefixes: set[str] = set() + + def _record_prefix_declaration(self, declaration: tuple[str, str] | None, parsed_at: int) -> None: + if parsed_at < 0 or declaration is None: + return + prefix, namespace = declaration + if _is_relative_iri(namespace): + self.relative_prefixes.add(prefix) + else: + self.relative_prefixes.discard(prefix) + + def directive(self, argstr: str, index: int) -> int: + declaration = _prefix_declaration(argstr, index) + parsed_at = super().directive(argstr, index) + self._record_prefix_declaration(declaration, parsed_at) + return parsed_at + + def sparqlDirective(self, argstr: str, index: int) -> int: + declaration = _prefix_declaration(argstr, index) + parsed_at = super().sparqlDirective(argstr, index) + self._record_prefix_declaration(declaration, parsed_at) + return parsed_at + + def uri_ref2(self, argstr: str, index: int, result: list) -> int: + qname: list = [] + self.qname(argstr, index, qname) + relative_qname = bool(qname and qname[0][0] in self.relative_prefixes) + start = self.skipSpace(argstr, index) + relative_ref = start >= 0 and argstr[start] == "<" + if relative_ref: + end = argstr.find(">", start + 1) + if end >= 0: + relative_ref = _is_relative_iri(argstr[start + 1 : end]) + + previous_length = len(result) + parsed_at = super().uri_ref2(argstr, index, result) + if parsed_at >= 0 and len(result) > previous_length and (relative_ref or relative_qname): + value = result[-1] + if isinstance(value, Node): + self.relative_iris.add(value) + return parsed_at + + +def parse_turtle_with_relative_iris(file_path: str | Path, public_id: str | None = None) -> tuple[Graph, set[Node]]: + """Parse a Turtle file and retain URI terms written as relative references.""" + + source = create_input_source(source=file_path, publicID=public_id) + graph = Graph() + try: + base_uri = graph.absolutize(source.getPublicId() or source.getSystemId() or "") + parser = _TurtleProvenanceParser(RDFSink(graph), base_uri) + stream = source.getCharacterStream() or source.getByteStream() + source_text = stream.read() + if isinstance(source_text, bytes): + source_text = source_text.decode("utf-8") + source_text = source_text.removeprefix("\ufeff") + + from io import StringIO # noqa: PLC0415 + + parser.loadStream(StringIO(source_text)) + for prefix, namespace in parser._bindings.items(): + graph.bind(prefix, namespace) + return graph, parser.relative_iris + finally: + source.close() def rebase_node(node: Node, base_mappings: Iterable[tuple[str, str]]) -> Node: @@ -89,6 +182,7 @@ def rebase_graph( *, preserve_nodes: Iterable[Node] = (), rebase_objects_for: Iterable[Node] = (), + rebase_nodes: Iterable[Node] | None = None, ) -> Graph: """ Copy a graph while replacing selected URI bases. @@ -100,8 +194,11 @@ def rebase_graph( mappings = tuple(base_mappings) preserved = frozenset(preserve_nodes) rebased_object_predicates = frozenset(rebase_objects_for) + relative_nodes = None if rebase_nodes is None else frozenset(rebase_nodes) def transform(node: Node, *, force_rebase: bool = False) -> Node: + if relative_nodes is not None and node not in relative_nodes: + return node return node if node in preserved and not force_rebase else rebase_node(node, mappings) rebased = Graph() diff --git a/tests/unit/requirements/test_shacl_utils.py b/tests/unit/requirements/test_shacl_utils.py index eb6c54b54..050914e8c 100644 --- a/tests/unit/requirements/test_shacl_utils.py +++ b/tests/unit/requirements/test_shacl_utils.py @@ -29,6 +29,7 @@ from rocrate_validator.constants import SHACL_NS from rocrate_validator.errors import BadSyntaxError from rocrate_validator.requirements.shacl.utils import load_shapes_from_file, load_shapes_from_graph +from rocrate_validator.utils.rdf import rebase_graph SH = Namespace(SHACL_NS) EX = Namespace("http://example.org/") @@ -66,6 +67,51 @@ def _build_two_property_shape() -> tuple[Graph, URIRef, BNode, BNode]: return g, node_shape, prop_a, prop_b +@pytest.mark.parametrize("directive", ["@prefix", "PREFIX"]) +def test_turtle_rebasing_only_changes_lexically_relative_iris(tmp_path, directive): + source_base = "https://example.invalid/rocrate-validator/prepared/crate/" + target_base = "file:///tmp/crate/" + absolute_target = URIRef("https://example.invalid/rocrate-validator/prepared/crate/AbsoluteClass") + external_target = URIRef("https://example.invalid/external/AbsoluteClass") + shapes_path = tmp_path / "relative-and-absolute.ttl" + relative_prefix = "@prefix rel: <../> ." if directive == "@prefix" else "PREFIX rel: <../>" + shapes_path.write_text( + f""" + @prefix sh: . + {relative_prefix} + PREFIX abs: + + rel:Shape a sh:NodeShape ; sh:targetNode rel:target . + abs:AbsoluteShape a sh:NodeShape ; sh:targetNode abs:AbsoluteClass . + + a sh:NodeShape ; sh:targetNode . + """, + encoding="utf-8", + ) + + shapes = load_shapes_from_file(str(shapes_path), source_base) + rebased = rebase_graph( + shapes.shapes_graph, + ((source_base, target_base),), + rebase_nodes=shapes.relative_iris, + ) + + assert any(object_ == URIRef("file:///tmp/target") for _, _, object_ in rebased) + assert any(object_ == absolute_target for _, _, object_ in rebased) + assert any(object_ == external_target for _, _, object_ in rebased) + + +def test_turtle_loader_accepts_utf8_bom(tmp_path): + shapes_path = tmp_path / "bom.ttl" + shapes_path.write_bytes( + b"\xef\xbb\xbf@prefix sh: . a sh:NodeShape ." + ) + + shapes = load_shapes_from_file(str(shapes_path)) + + assert URIRef("https://example.org/Shape") in shapes.node_shapes + + def test_returns_link_triple_to_target_property(): """The link ``(node_shape, sh:property, shape_property)`` must be present.""" g, node_shape, prop_a, prop_b = _build_two_property_shape() diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 2df4c1021..a4029d476 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -18,6 +18,7 @@ import threading import time from pathlib import Path +from urllib.parse import urljoin import pytest from rdflib import Graph, Namespace, URIRef @@ -25,6 +26,7 @@ from rocrate_validator.errors import ValidationExecutionError from rocrate_validator.models import URI, Profile, Requirement, ValidationContext, ValidationSettings, Validator +from rocrate_validator.models import validation as validation_module from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.models import ShapesRegistry from rocrate_validator.requirements.shacl.validator import SHACLValidationContext @@ -116,6 +118,53 @@ def counted_load_profiles(cls, *args, **kwargs): assert str(first_target).startswith(first_result.context.prepared_profile_base) +def test_ontology_rebasing_only_changes_lexically_relative_iris(tmp_path): + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + (profiles_path / "c" / "ontology.ttl").write_text( + """ + PREFIX rel: <../> + PREFIX ex: + + rel:RelativeSubject ex:link rel:RelativeObject . + + ex:link . + + ex:link . + """, + encoding="utf-8", + ) + validator = Validator( + ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + ) + + result = validator.validate() + shacl_context = SHACLValidationContext.get_instance(result.context) + ontology_graph = shacl_context.ontology_graph + ex = Namespace("http://example.org/") + + assert ( + URIRef(urljoin(result.context.publicID, "../RelativeSubject")), + ex.link, + URIRef(urljoin(result.context.publicID, "../RelativeObject")), + ) in ontology_graph + assert ( + URIRef("https://example.invalid/rocrate-validator/prepared/crate/AbsoluteSubject"), + ex.link, + URIRef("https://example.invalid/rocrate-validator/prepared/crate/AbsoluteObject"), + ) in ontology_graph + assert ( + URIRef("https://example.invalid/external/AbsoluteSubject"), + ex.link, + URIRef("https://example.invalid/external/AbsoluteObject"), + ) in ontology_graph + + def test_cross_base_reuse_keeps_each_crates_data_and_public_output_isolated(tmp_path, monkeypatch): profile_loads = 0 shape_loads = 0 @@ -481,15 +530,16 @@ def counted_load_profiles(cls, *args, **kwargs): def test_inherited_ontologies_are_prepared_once(monkeypatch): ontology_parses = 0 - original_parse = Graph.parse + original_parse = validation_module.parse_turtle_with_relative_iris + graph_parse = Graph.parse - def counted_parse(self, source=None, *args, **kwargs): + def counted_parse(source, *args, **kwargs): nonlocal ontology_parses if source is not None and str(source).endswith("ontology.ttl"): ontology_parses += 1 - return original_parse(self, source, *args, **kwargs) + return original_parse(source, *args, **kwargs) - monkeypatch.setattr(Graph, "parse", counted_parse) + monkeypatch.setattr(validation_module, "parse_turtle_with_relative_iris", counted_parse) settings = ValidationSettings( rocrate_uri=URI(ValidROC().wrroc_paper), profile_identifier="isa-ro-crate", @@ -506,7 +556,7 @@ def counted_parse(self, source=None, *args, **kwargs): for profile in first_plan.profiles: ontology_path = profile.path / "ontology.ttl" if ontology_path.exists(): - original_parse(expected, ontology_path, format="ttl", publicID=first_context.prepared_ontology_base) + graph_parse(expected, ontology_path, format="ttl", publicID=first_context.prepared_ontology_base) assert ontology_parses == 2 assert first_plan is second_plan From 230856a199688e7af3dc6cd49bc462de36b2f910 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Oct 2026 18:38:08 +0200 Subject: [PATCH 178/271] =?UTF-8?q?fix(validation):=20=F0=9F=90=9B=20prese?= =?UTF-8?q?rve=20absolute=20implicit=20class=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/shacl/validator.py | 4 ++ tests/unit/test_warm_start.py | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index bce5da9d2..78615a047 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -126,6 +126,7 @@ def __init__(self, context: ValidationContext): # profile may expose several SHACL checks, but its graphs must be # loaded only once before the combined validation is executed. self._loaded_profiles: set[str] = set() + self._relative_shape_iris: set[Node] = set() # Checks awaiting the combined run have no result yet; they are not skips. self.deferred_checks: set[RequirementCheck] = set() @@ -168,6 +169,7 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: if profile.identifier not in self._loaded_profiles: # augment the shapes registry with the profile shapes profile_registry = ShapesRegistry.get_instance(profile) + self._relative_shape_iris.update(profile_registry.relative_iris) profile_shapes = profile_registry.get_shapes() profile_shapes_graph = profile_registry.shapes_graph structural_nodes = { @@ -261,6 +263,8 @@ def shapes_graph(self) -> Graph: shacl = Namespace(SHACL_NS) for shape in self.shapes_registry.get_shapes().values(): node = shape.node + if node not in self._relative_shape_iris: + continue target_class = rebase_node(node, self._run_base_mappings) if target_class == node: continue diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index a4029d476..3c3a20926 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -346,6 +346,55 @@ def test_structural_shape_identifier_is_rebased_as_target_class( assert isomorphic(prepared_shapes, registry.shapes_graph) +def test_absolute_canonical_class_target_is_not_rebased(tmp_path): + """An absolute class IRI on the preparation authority must retain its identity.""" + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + absolute_class = "https://example.invalid/rocrate-validator/prepared/crate/Dataset" + shape_path = profiles_path / "c" / "shape_c.ttl" + shape_path.write_text( + f""" + @prefix rdfs: . + @prefix schema: . + @prefix sh: . + + <{absolute_class}> a sh:NodeShape, rdfs:Class ; + sh:property [ sh:path schema:name ; sh:minCount 1 ; + sh:message "absolute-class check must run" ] . + """, + encoding="utf-8", + ) + crate = tmp_path / "crate" + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "./item", "@type": absolute_class, "name": "Present"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + validator = Validator( + ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + ) + + valid_result = validator.validate() + assert valid_result.passed() + metadata["@graph"][2].pop("name") + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + invalid_result = validator.validate() + + assert not invalid_result.passed() + assert any(issue.message == "absolute-class check must run" for issue in invalid_result.get_issues()) + assert invalid_result.get_issues()[0].violatingEntity == "./item" + + def test_per_call_metadata_does_not_mutate_validator_settings(): crate = ValidROC().wrroc_paper with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: From 3bcf7c824e4ddf122644e2db0ca55739518682cb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 7 Oct 2026 10:40:38 +0200 Subject: [PATCH 179/271] =?UTF-8?q?chore(lint):=20=F0=9F=A7=B9=20fix=20lin?= =?UTF-8?q?t=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/requirement.py | 9 +----- rocrate_validator/utils/cache_warmup.py | 2 +- rocrate_validator/utils/rdf.py | 36 +++++++++++---------- tests/unit/requirements/test_shacl_utils.py | 6 ++-- tests/unit/test_rdf.py | 12 +++---- tests/unit/test_warm_start.py | 12 +++---- 6 files changed, 36 insertions(+), 41 deletions(-) diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 37401c627..9e6004314 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -34,6 +34,7 @@ DeferredRequirementCheck, normalize_check_result, ) +from rocrate_validator.models.events import RequirementCheckValidationEvent from rocrate_validator.models.severity import ( LevelCollection, RequirementLevel, @@ -297,10 +298,6 @@ def __record_skipped_check__( message: str = "Check was skipped", category: SkipCategory = SkipCategory.RETURNED, ) -> None: - from rocrate_validator.models.events import ( # noqa: PLC0415 - RequirementCheckValidationEvent, - ) - # Overlay replacements and batched SHACL checks may already have an # outcome before traversal reaches their own requirement. Aborting # must not overwrite that outcome or emit a misleading skipped event. @@ -432,10 +429,6 @@ def __execute_check__( profile is visited. The returned tuple contains the accumulated pass state and whether fail-fast processing must stop after this check. """ - from rocrate_validator.models.events import ( # noqa: PLC0415 - RequirementCheckValidationEvent, - ) - # An overlay replacement may already have run in the source profile's # slot. Keep the target pass a no-op instead of executing the same # Python check twice. diff --git a/rocrate_validator/utils/cache_warmup.py b/rocrate_validator/utils/cache_warmup.py index b2dceff29..5eef9e880 100644 --- a/rocrate_validator/utils/cache_warmup.py +++ b/rocrate_validator/utils/cache_warmup.py @@ -32,6 +32,7 @@ from rocrate_validator import constants from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import OFFLINE_CACHE_MISS_STATUS, HttpRequester +from rocrate_validator.utils.paths import get_profiles_path if TYPE_CHECKING: from collections.abc import Iterable, Sequence @@ -194,7 +195,6 @@ def _find_profile(identifier, settings) -> Profile | None: """ # Import here to avoid a circular import with models.py. from rocrate_validator.models import Profile # noqa: PLC0415 - from rocrate_validator.utils.paths import get_profiles_path # noqa: PLC0415 # Load profiles to ensure the requested one is available and its graph is parsed. global __profiles_loaded # noqa: PLW0603 diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 5ad966008..f6ee4a3e2 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -13,9 +13,11 @@ # limitations under the License. import re -from collections.abc import Iterable +from collections.abc import Iterable, MutableSequence +from io import StringIO from pathlib import Path from posixpath import commonpath, relpath +from typing import Any from urllib.parse import urljoin, urlsplit, urlunsplit from rdflib import Graph, URIRef @@ -31,7 +33,7 @@ logger = logging.getLogger(__name__) -PREPARED_PROFILE_BASE = "https://example.invalid/rocrate-validator/prepared/crate/" +PREPARED_PROFILE_BASE = "https://github.com/crs4/rocrate-validator/prepared/crate/" _PREFIX_DECLARATION = re.compile(r"@?prefix\b\s+([^:\s]*):\s*<([^>]*)>", re.IGNORECASE) @@ -67,33 +69,33 @@ def _record_prefix_declaration(self, declaration: tuple[str, str] | None, parsed else: self.relative_prefixes.discard(prefix) - def directive(self, argstr: str, index: int) -> int: - declaration = _prefix_declaration(argstr, index) - parsed_at = super().directive(argstr, index) + def directive(self, argstr: str, i: int) -> int: + declaration = _prefix_declaration(argstr, i) + parsed_at = super().directive(argstr, i) self._record_prefix_declaration(declaration, parsed_at) return parsed_at - def sparqlDirective(self, argstr: str, index: int) -> int: - declaration = _prefix_declaration(argstr, index) - parsed_at = super().sparqlDirective(argstr, index) + def sparqlDirective(self, argstr: str, i: int) -> int: + declaration = _prefix_declaration(argstr, i) + parsed_at = super().sparqlDirective(argstr, i) self._record_prefix_declaration(declaration, parsed_at) return parsed_at - def uri_ref2(self, argstr: str, index: int, result: list) -> int: + def uri_ref2(self, argstr: str, i: int, res: MutableSequence[Any]) -> int: qname: list = [] - self.qname(argstr, index, qname) + self.qname(argstr, i, qname) relative_qname = bool(qname and qname[0][0] in self.relative_prefixes) - start = self.skipSpace(argstr, index) + start = self.skipSpace(argstr, i) relative_ref = start >= 0 and argstr[start] == "<" if relative_ref: end = argstr.find(">", start + 1) if end >= 0: relative_ref = _is_relative_iri(argstr[start + 1 : end]) - previous_length = len(result) - parsed_at = super().uri_ref2(argstr, index, result) - if parsed_at >= 0 and len(result) > previous_length and (relative_ref or relative_qname): - value = result[-1] + previous_length = len(res) + parsed_at = super().uri_ref2(argstr, i, res) + if parsed_at >= 0 and len(res) > previous_length and (relative_ref or relative_qname): + value = res[-1] if isinstance(value, Node): self.relative_iris.add(value) return parsed_at @@ -108,13 +110,13 @@ def parse_turtle_with_relative_iris(file_path: str | Path, public_id: str | None base_uri = graph.absolutize(source.getPublicId() or source.getSystemId() or "") parser = _TurtleProvenanceParser(RDFSink(graph), base_uri) stream = source.getCharacterStream() or source.getByteStream() + if stream is None: + raise ValueError("Input source does not provide a readable stream") source_text = stream.read() if isinstance(source_text, bytes): source_text = source_text.decode("utf-8") source_text = source_text.removeprefix("\ufeff") - from io import StringIO # noqa: PLC0415 - parser.loadStream(StringIO(source_text)) for prefix, namespace in parser._bindings.items(): graph.bind(prefix, namespace) diff --git a/tests/unit/requirements/test_shacl_utils.py b/tests/unit/requirements/test_shacl_utils.py index 050914e8c..18cc97dcf 100644 --- a/tests/unit/requirements/test_shacl_utils.py +++ b/tests/unit/requirements/test_shacl_utils.py @@ -69,9 +69,9 @@ def _build_two_property_shape() -> tuple[Graph, URIRef, BNode, BNode]: @pytest.mark.parametrize("directive", ["@prefix", "PREFIX"]) def test_turtle_rebasing_only_changes_lexically_relative_iris(tmp_path, directive): - source_base = "https://example.invalid/rocrate-validator/prepared/crate/" + source_base = "https://github.com/crs4/rocrate-validator/prepared/crate/" target_base = "file:///tmp/crate/" - absolute_target = URIRef("https://example.invalid/rocrate-validator/prepared/crate/AbsoluteClass") + absolute_target = URIRef("https://github.com/crs4/rocrate-validator/prepared/crate/AbsoluteClass") external_target = URIRef("https://example.invalid/external/AbsoluteClass") shapes_path = tmp_path / "relative-and-absolute.ttl" relative_prefix = "@prefix rel: <../> ." if directive == "@prefix" else "PREFIX rel: <../>" @@ -79,7 +79,7 @@ def test_turtle_rebasing_only_changes_lexically_relative_iris(tmp_path, directiv f""" @prefix sh: . {relative_prefix} - PREFIX abs: + PREFIX abs: rel:Shape a sh:NodeShape ; sh:targetNode rel:target . abs:AbsoluteShape a sh:NodeShape ; sh:targetNode abs:AbsoluteClass . diff --git a/tests/unit/test_rdf.py b/tests/unit/test_rdf.py index 963b668aa..6a707eb6e 100644 --- a/tests/unit/test_rdf.py +++ b/tests/unit/test_rdf.py @@ -22,7 +22,7 @@ def test_rebase_graph_maps_parent_relative_references_after_rdf_parsing(): source_base = "https://example.org/a/b/crate/" - prepared_base = "https://example.invalid/rocrate-validator/prepared/crate/" + prepared_base = PREPARED_PROFILE_BASE graph = Graph().parse( data=""" @prefix ex: <../> . @@ -38,19 +38,19 @@ def test_rebase_graph_maps_parent_relative_references_after_rdf_parsing(): rebased = rebase_graph(graph, ((source_base, prepared_base),)) assert ( - URIRef("https://example.invalid/rocrate-validator/prepared/target"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/target"), URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - URIRef("https://example.invalid/rocrate-validator/prepared/Entity"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/Entity"), ) in rebased assert ( URIRef("https://outside.example/entity"), URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - URIRef("https://example.invalid/rocrate-validator/prepared/Entity"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/Entity"), ) in rebased def test_rebase_graph_rebases_structural_node_when_used_as_target_class(): - prepared_base = "https://example.invalid/rocrate-validator/prepared/crate/" + prepared_base = PREPARED_PROFILE_BASE crate_base = "file:///tmp/crate/" sh = Namespace("http://www.w3.org/ns/shacl#") class_shape = URIRef(f"{prepared_base}Dataset") @@ -80,7 +80,7 @@ def test_rebase_node_preserves_parent_relative_trailing_slash(): def test_rebase_node_preserves_root_relative_path(): target_base = "file:///data/team/project/crate/" - source_uri = URIRef("https://example.invalid/Dataset") + source_uri = URIRef("https://github.com/Dataset") rebased = rebase_node(source_uri, ((PREPARED_PROFILE_BASE, target_base),)) diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py index 3c3a20926..6f5750e11 100644 --- a/tests/unit/test_warm_start.py +++ b/tests/unit/test_warm_start.py @@ -127,8 +127,8 @@ def test_ontology_rebasing_only_changes_lexically_relative_iris(tmp_path): PREFIX ex: rel:RelativeSubject ex:link rel:RelativeObject . - - ex:link . + + ex:link . ex:link . """, @@ -154,9 +154,9 @@ def test_ontology_rebasing_only_changes_lexically_relative_iris(tmp_path): URIRef(urljoin(result.context.publicID, "../RelativeObject")), ) in ontology_graph assert ( - URIRef("https://example.invalid/rocrate-validator/prepared/crate/AbsoluteSubject"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/crate/AbsoluteSubject"), ex.link, - URIRef("https://example.invalid/rocrate-validator/prepared/crate/AbsoluteObject"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/crate/AbsoluteObject"), ) in ontology_graph assert ( URIRef("https://example.invalid/external/AbsoluteSubject"), @@ -315,7 +315,7 @@ def test_structural_shape_identifier_is_rebased_as_target_class( metadata["@graph"].append( { "@id": "./unrelated", - "@type": "https://example.invalid/rocrate-validator/prepared/crate/Dataset", + "@type": "https://github.com/crs4/rocrate-validator/prepared/crate/Dataset", } ) (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") @@ -350,7 +350,7 @@ def test_absolute_canonical_class_target_is_not_rebased(tmp_path): """An absolute class IRI on the preparation authority must retain its identity.""" profiles_path = tmp_path / "profiles" shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) - absolute_class = "https://example.invalid/rocrate-validator/prepared/crate/Dataset" + absolute_class = "https://github.com/crs4/rocrate-validator/prepared/crate/Dataset" shape_path = profiles_path / "c" / "shape_c.ttl" shape_path.write_text( f""" From bebee42bad8783fd696bcaa51a1cd5fbcf25ff35 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 15:57:03 +0200 Subject: [PATCH 180/271] =?UTF-8?q?feat(rocrate):=20=F0=9F=93=A6=20detect?= =?UTF-8?q?=20package=20type=20from=20input=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/settings.py | 17 +++- rocrate_validator/models/validation.py | 13 +++- rocrate_validator/rocrate/__init__.py | 2 + rocrate_validator/rocrate/bagit.py | 4 + rocrate_validator/rocrate/base.py | 95 +++++++++++++++++------ rocrate_validator/rocrate/entity.py | 6 +- rocrate_validator/rocrate/metadata.py | 6 +- rocrate_validator/rocrate/package_type.py | 16 ++++ rocrate_validator/rocrate/plain.py | 60 ++++++++++++-- rocrate_validator/services.py | 65 ++++++++++++---- 10 files changed, 223 insertions(+), 61 deletions(-) create mode 100644 rocrate_validator/rocrate/package_type.py diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index 43be3a660..918c3435c 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -23,6 +23,7 @@ ) from rocrate_validator.models._logging import logger from rocrate_validator.models.severity import Severity +from rocrate_validator.rocrate.package_type import parse_packaging_mode from rocrate_validator.utils.cache_warmup import auto_warm_up_for_settings from rocrate_validator.utils.document_loader import install_document_loader from rocrate_validator.utils.http import HttpRequester @@ -47,7 +48,8 @@ class ValidationSettings: """ #: The URI of the RO-Crate - rocrate_uri: URI # pyright: ignore[reportRedeclaration] + rocrate_uri: URI | None # pyright: ignore[reportRedeclaration] + _source_rocrate_uri: URI | None = None #: The relative root path of the RO-Crate rocrate_relative_root_path: Path | None = None # Profile settings @@ -90,6 +92,8 @@ class ValidationSettings: skip_checks: list[str] | None = None #: Flag to validate only the metadata of the RO-Crate metadata_only: bool = False + #: Packaging context: auto, attached, or detached + packaging_mode: str = "auto" #: RO-Crate metadata as dictionary metadata_dict: dict | None = None #: Verbose output @@ -110,6 +114,7 @@ class ValidationSettings: skip_availability_check: bool = False def __post_init__(self): + self.packaging_mode = parse_packaging_mode(self.packaging_mode) # if requirement_severity is a str, convert to Severity if isinstance(self.requirement_severity, str): self.requirement_severity = Severity[self.requirement_severity] @@ -165,7 +170,8 @@ def to_dict(self): Convert the ValidationSettings to a dictionary """ result = asdict(self) - result["rocrate_uri"] = str(self.rocrate_uri) + source_uri = getattr(self, "_source_rocrate_uri", self.rocrate_uri) + result["rocrate_uri"] = str(source_uri) if source_uri is not None else None result.pop("metadata_dict", None) # exclude metadata_dict from the dict representation # Remove disable_crate_download from the dict representation result.pop("disable_remote_crate_download", None) @@ -184,16 +190,19 @@ def rocrate_uri(self) -> URI | None: return self._rocrate_uri @rocrate_uri.setter - def rocrate_uri(self, value: str | Path | URI): + def rocrate_uri(self, value: str | Path | URI | None): """ Set the RO-Crate URI. :param value: The RO-Crate URI. :type value: Union[str, Path, URI] """ + if value is None or isinstance(value, property): + self._rocrate_uri = None + return if not value: raise ValueError("Invalid RO-Crate URI") - self._rocrate_uri: URI = URI(str(value)) + self._rocrate_uri = URI(str(value)) @classmethod def parse(cls, settings: dict | ValidationSettings) -> ValidationSettings: diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 340a2e482..641e6534c 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -54,6 +54,8 @@ from rocrate_validator.utils.uri import URI if TYPE_CHECKING: + import tempfile + from rdflib.term import Node from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck @@ -103,6 +105,7 @@ class Validator(Publisher): def __init__(self, settings: dict | ValidationSettings): self._validation_settings = ValidationSettings.parse(settings) + self._extracted_crate_directory: tempfile.TemporaryDirectory | None = None super().__init__() # initialize the current context self.__current_context__: ValidationContext | None = None @@ -350,7 +353,6 @@ def __do_validate__( *, settings: ValidationSettings, ) -> ValidationResult: - with self.__run_lock: return self.__do_validate_locked__(requirements, settings=settings) @@ -548,14 +550,15 @@ def __init__(self, validator: Validator, settings: ValidationSettings): self._abort_reason: str | None = None # initialize the ROCrate object - if settings.metadata_dict: - self._rocrate = ROCrate.from_metadata_dict(settings.metadata_dict) + if settings.metadata_dict is not None: + self._rocrate = ROCrate.from_metadata_dict(settings.metadata_dict, packaging_mode=settings.packaging_mode) else: rocrate_uri = settings.rocrate_uri assert rocrate_uri is not None, "RO-Crate URI is required when metadata_dict is not provided" self._rocrate = ROCrate.new_instance( rocrate_uri, relative_root_path=settings.rocrate_relative_root_path, + packaging_mode=settings.packaging_mode, ) assert isinstance(self._rocrate, ROCrate), "Invalid RO-Crate instance" @@ -615,6 +618,8 @@ def publicID(self) -> str: """ The root URI of the RO-Crate """ + if getattr(self.ro_crate, "is_in_memory", False): + return "https://example.invalid/rocrate-validator/in-memory/" path = str(self.ro_crate.uri.base_uri) if not path.endswith("/"): path = f"{path}/" @@ -722,7 +727,7 @@ def rocrate_uri(self) -> URI: """ rocrate_uri = self.settings.rocrate_uri if rocrate_uri is None: - raise ValueError("RO-Crate URI is not set") + return self.ro_crate.uri return rocrate_uri @property diff --git a/rocrate_validator/rocrate/__init__.py b/rocrate_validator/rocrate/__init__.py index 2615e7b0a..978bc2c55 100644 --- a/rocrate_validator/rocrate/__init__.py +++ b/rocrate_validator/rocrate/__init__.py @@ -30,10 +30,12 @@ from .base import ROCrate from .entity import ROCrateEntity from .metadata import ROCrateMetadata +from .package_type import PackageType from .plain import ROCrateLocalFolder, ROCrateLocalZip, ROCrateRemoteZip __all__ = [ "BagitROCrate", + "PackageType", "ROCrate", "ROCrateBagitLocalFolder", "ROCrateBagitLocalZip", diff --git a/rocrate_validator/rocrate/bagit.py b/rocrate_validator/rocrate/bagit.py index 17149c46b..1ea46cdde 100644 --- a/rocrate_validator/rocrate/bagit.py +++ b/rocrate_validator/rocrate/bagit.py @@ -33,6 +33,10 @@ class BagitROCrate(ROCrate, ABC): + @property + def attached_descriptor_id(self) -> str: + return str(Path("data") / (self.relative_root_path or Path()) / "ro-crate-metadata.json") + def __init__(self, uri, relative_root_path=None): super().__init__(uri, relative_root_path) diff --git a/rocrate_validator/rocrate/base.py b/rocrate_validator/rocrate/base.py index e284b842c..781ed129c 100644 --- a/rocrate_validator/rocrate/base.py +++ b/rocrate_validator/rocrate/base.py @@ -28,6 +28,7 @@ from .entity import ROCrateEntity from .metadata import ROCrateMetadata +from .package_type import PackageType, parse_packaging_mode if TYPE_CHECKING: from rdflib import Graph @@ -41,6 +42,12 @@ class ROCrate(ABC): Base class for representing and interacting with a Research Object Crate (RO-Crate). """ + # Set by the factory after selecting a concrete input adapter. They are + # declared here for static analysis; getattr-based defaults below preserve + # the behavior of directly constructed subclasses. + _package_type: PackageType + _packaging_mode_explicit: bool + def __new__(cls, uri: str | Path | URI, relative_root_path: Path | None = None): """ Factory method to create the appropriate ROCrate subclass instance. @@ -110,21 +117,31 @@ def metadata(self) -> ROCrateMetadata: self._metadata = ROCrateMetadata(self) return self._metadata + @property + def package_type(self) -> PackageType: + """Resolved RO-Crate package type, based on the input source.""" + return getattr(self, "_package_type", PackageType.ATTACHED) + + @property + def packaging_mode_explicit(self) -> bool: + return getattr(self, "_packaging_mode_explicit", False) + def is_detached(self) -> bool: - root = self.metadata.get_root_data_entity() - if root and root.has_type("Dataset") and root.id == "./": - return False - if root and root.id_as_uri.is_remote_resource(): - # An absolute root @id doesn't necessarily mean detached; - # check if there are any local (non-web) data entities - local_data_entities = self.metadata.get_data_entities(exclude_web_data_entities=True) - return all(entity.id == root.id for entity in local_data_entities) - return False + return self.package_type is PackageType.DETACHED + + def is_attached(self) -> bool: + return self.package_type is PackageType.ATTACHED @property def metadata_descriptor_id(self) -> str: return ROCrateMetadata.METADATA_FILE_DESCRIPTOR + @property + def attached_descriptor_id(self) -> str: + """Canonical descriptor at the selected Attached package root.""" + root = self.relative_root_path or Path() + return str(root / ROCrateMetadata.METADATA_FILE_DESCRIPTOR) + @property @abstractmethod def size(self) -> int: @@ -343,7 +360,7 @@ def get_external_file_size(uri: str) -> int | None: return int(content_length) @staticmethod - def from_metadata_dict(metadata_dict: dict) -> ROCrate: + def from_metadata_dict(metadata_dict: dict, packaging_mode: str = "auto") -> ROCrate: """ Create a new instance of the RO-Crate based on the metadata dictionary. @@ -352,16 +369,21 @@ def from_metadata_dict(metadata_dict: dict) -> ROCrate: :raises ROCrateInvalidURIError: if the URI is invalid """ - # create a new instance based on the URI (the ROCrate factory __new__ - # dispatches to a concrete subclass, so this is not truly abstract) - ro_crate = ROCrate(URI("./"), relative_root_path=None) # type: ignore[abstract] + from .plain import ROCrateInMemory # noqa: PLC0415 + + mode = parse_packaging_mode(packaging_mode) + ro_crate = ROCrateInMemory("urn:rocrate-validator:in-memory/") + ro_crate._package_type = PackageType.UNSPECIFIED if mode == "auto" else PackageType(mode) + ro_crate._packaging_mode_explicit = mode != "auto" # override the metadata with the provided dictionary ro_crate._metadata = ROCrateMetadata(ro_crate, metadata_dict=metadata_dict) return ro_crate @staticmethod - def new_instance(uri: str | Path | URI, relative_root_path: Path | None = None) -> ROCrate: + def new_instance( # noqa: C901 # pylint: disable=too-many-locals + uri: str | Path | URI, relative_root_path: Path | None = None, packaging_mode: str = "auto" + ) -> ROCrate: """ Create a new instance of the RO-Crate based on the URI. @@ -373,6 +395,7 @@ def new_instance(uri: str | Path | URI, relative_root_path: Path | None = None) :raises ROCrateInvalidURIError: if the URI is invalid """ + mode = parse_packaging_mode(packaging_mode) # Lazy imports break a cycle: bagit/plain inherit from this class, # but the factory needs runtime references to dispatch to them. from .bagit import ( # noqa: PLC0415 @@ -389,40 +412,64 @@ def new_instance(uri: str | Path | URI, relative_root_path: Path | None = None) ROCrateRemoteZip, ) - # check if the URI is valid - validate_rocrate_uri(uri, silent=False) - # create a new instance based on the URI + # Reject incompatible source/mode combinations before network access. if not isinstance(uri, URI): uri = URI(uri) - # check if the URI is a BagIt-wrapped crate - is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + if mode == "attached" and uri.is_remote_resource() and Path(uri.get_path()).suffix.lower() != ".zip": + raise ValueError("Attached mode for a remote standalone document requires a package adapter") + if mode == "detached" and (uri.is_local_directory() or Path(uri.get_path()).suffix.lower() == ".zip"): + raise ValueError("Detached mode requires a standalone metadata document, not a directory or ZIP") + # check if the URI is valid + validate_rocrate_uri(uri, silent=False) # check if the URI is a local directory if uri.is_local_directory(): - return ( + is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + crate: ROCrate = ( ROCrateBagitLocalFolder(uri, relative_root_path=relative_root_path) if is_bagit_crate else ROCrateLocalFolder(uri, relative_root_path=relative_root_path) ) + crate._package_type = PackageType.ATTACHED + crate._packaging_mode_explicit = mode != "auto" + return crate # check if the URI is a local zip file if uri.is_local_file(): suffix = uri.as_path().suffix.lower() if suffix == ".zip": - return ( + is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + crate = ( ROCrateBagitLocalZip(uri, relative_root_path=relative_root_path) if is_bagit_crate else ROCrateLocalZip(uri, relative_root_path=relative_root_path) ) - return ROCrateLocalMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.ATTACHED + crate._packaging_mode_explicit = mode != "auto" + return crate + if mode == "attached": + if uri.as_path().name != ROCrateMetadata.METADATA_FILE_DESCRIPTOR: + raise ValueError("Attached mode requires a canonical ro-crate-metadata.json or a package directory") + return ROCrate.new_instance(uri.as_path().parent, packaging_mode="attached") + crate = ROCrateLocalMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.DETACHED + return crate # check if the URI is a remote zip file if uri.is_remote_resource(): path_suffix = Path(uri.get_path()).suffix.lower() if path_suffix == ".zip": - return ( + is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + crate = ( ROCrateBagitRemoteZip(uri, relative_root_path=relative_root_path) if is_bagit_crate else ROCrateRemoteZip(uri, relative_root_path=relative_root_path) ) - return ROCrateRemoteMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.ATTACHED + crate._packaging_mode_explicit = mode != "auto" + return crate + if mode == "attached": + raise ValueError("Attached mode for a remote standalone document requires a package adapter") + crate = ROCrateRemoteMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.DETACHED + return crate # if the URI is not supported, raise an error raise ROCrateInvalidURIError(uri=uri, message="Unsupported RO-Crate URI") diff --git a/rocrate_validator/rocrate/entity.py b/rocrate_validator/rocrate/entity.py index 2c05bcef7..a4a41c6c3 100644 --- a/rocrate_validator/rocrate/entity.py +++ b/rocrate_validator/rocrate/entity.py @@ -143,7 +143,11 @@ def get_id_as_uri(cls, entity_id: str, ro_crate: ROCrate) -> URI: # Otherwise the `@id` is a relative path: if the RO-Crate itself is # remote, resolve it against the crate URI so the entity is still # classified as remote/web-based. - if ro_crate.uri.is_remote_resource() and entity_id.startswith("./"): + if ( + ro_crate.uri.is_remote_resource() + and not getattr(ro_crate, "is_in_memory", False) + and entity_id.startswith("./") + ): return URI(f"{ro_crate.uri}/{entity_id[2:]}") return URI(cls.get_id_as_path(entity_id, ro_crate)) diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index a12926d87..c9b708abd 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -38,7 +38,7 @@ class ROCrateMetadata: def __init__(self, ro_crate: ROCrate, metadata_dict: dict | None = None) -> None: self._ro_crate = ro_crate self._dict = metadata_dict - self._json: str | None = json.dumps(metadata_dict) if metadata_dict else None + self._json: str | None = json.dumps(metadata_dict) if metadata_dict is not None else None self._graph: Graph | None = None @property @@ -165,12 +165,12 @@ def as_bytes(self) -> bytes: return cast("bytes", self.__get_file_content__(binary_mode=True)) def as_json(self) -> str: - if not self._json: + if self._json is None: self._json = cast("str", self.__get_file_content__(binary_mode=False)) return self._json def as_dict(self) -> dict[Any, Any]: - if not self._dict: + if self._dict is None: # if the dictionary is not cached, load it self._dict = json.loads(self.as_json()) assert self._dict is not None, "Metadata dictionary should not be None after loading" diff --git a/rocrate_validator/rocrate/package_type.py b/rocrate_validator/rocrate/package_type.py new file mode 100644 index 000000000..c17c65ca8 --- /dev/null +++ b/rocrate_validator/rocrate/package_type.py @@ -0,0 +1,16 @@ +"""Processing context for an RO-Crate metadata document.""" + +from enum import Enum + + +class PackageType(str, Enum): + ATTACHED = "attached" + DETACHED = "detached" + UNSPECIFIED = "unspecified" + + +def parse_packaging_mode(value: str) -> str: + """Validate the public setting; ``auto`` is resolved by the source adapter.""" + if value not in {"auto", "attached", "detached"}: + raise ValueError("packaging_mode must be 'auto', 'attached', or 'detached'") + return value diff --git a/rocrate_validator/rocrate/plain.py b/rocrate_validator/rocrate/plain.py index 9be07f7ac..7e06f6daf 100644 --- a/rocrate_validator/rocrate/plain.py +++ b/rocrate_validator/rocrate/plain.py @@ -26,6 +26,7 @@ from .base import ROCrate from .metadata import ROCrateMetadata +from .package_type import PackageType if TYPE_CHECKING: from rocrate_validator.utils.uri import URI @@ -34,6 +35,39 @@ logger = logging.getLogger(__name__) +class ROCrateInMemory(ROCrate): + """Metadata without an implicit payload directory or working-directory root.""" + + def __init__(self, uri: str = "urn:rocrate-validator:in-memory/"): + super().__init__(uri) + + @property + def is_in_memory(self) -> bool: + return True + + @property + def size(self) -> int: + return self.metadata.size + + def list_files(self) -> list[Path]: + return [] + + def has_descriptor(self) -> bool: + return True + + def has_file(self, path: Path) -> bool: + return False + + def has_directory(self, path: Path) -> bool: + return False + + def get_file_size(self, path: Path) -> int: + raise NotImplementedError("In-memory metadata has no payload storage") + + def get_file_content(self, path: Path, binary_mode: bool = True) -> str | bytes: + raise NotImplementedError("In-memory metadata has no payload storage") + + class ROCrateLocalFolder(ROCrate): """ Class representing an RO-Crate stored in a local folder. @@ -59,12 +93,16 @@ def metadata_descriptor_id(self) -> str: if self._metadata_descriptor_id: return self._metadata_descriptor_id base_path = self.uri.as_path() + canonical = self.attached_descriptor_id + if (base_path / canonical).is_file(): + self._metadata_descriptor_id = canonical + return canonical candidates = sorted( (p for p in base_path.rglob(f"*{ROCrateMetadata.METADATA_FILE_DESCRIPTOR}") if p.is_file()), key=lambda p: (len(p.relative_to(base_path).parts), str(p)), ) - if not candidates: - self._metadata_descriptor_id = ROCrateMetadata.METADATA_FILE_DESCRIPTOR + if len(candidates) != 1: + self._metadata_descriptor_id = canonical return self._metadata_descriptor_id self._metadata_descriptor_id = str(candidates[0].relative_to(base_path)) return self._metadata_descriptor_id @@ -158,12 +196,16 @@ def has_descriptor(self) -> bool: def metadata_descriptor_id(self) -> str: if self._metadata_descriptor_id: return self._metadata_descriptor_id + canonical = self.attached_descriptor_id + if Path(canonical) in self.list_files(): + self._metadata_descriptor_id = canonical + return canonical candidates = sorted( (p for p in self.list_files() if str(p).endswith(ROCrateMetadata.METADATA_FILE_DESCRIPTOR)), key=lambda p: (len(p.parts), str(p)), ) - if not candidates: - self._metadata_descriptor_id = ROCrateMetadata.METADATA_FILE_DESCRIPTOR + if len(candidates) != 1: + self._metadata_descriptor_id = canonical return self._metadata_descriptor_id self._metadata_descriptor_id = str(candidates[0]) return self._metadata_descriptor_id @@ -228,8 +270,9 @@ def __init__(self, path: str | Path | URI, relative_root_path: Path | None = Non if suffix not in (".json", ".jsonld"): raise ROCrateInvalidURIError(uri=path, message="Unsupported metadata file format") - def is_detached(self) -> bool: - return True + @property + def package_type(self) -> PackageType: + return getattr(self, "_package_type", PackageType.DETACHED) @property def metadata_descriptor_id(self) -> str: @@ -266,8 +309,9 @@ def __init__(self, uri: str | Path | URI, relative_root_path: Path | None = None if not self.uri.is_remote_resource(): raise ROCrateInvalidURIError(uri=uri) - def is_detached(self) -> bool: - return True + @property + def package_type(self) -> PackageType: + return getattr(self, "_package_type", PackageType.DETACHED) @property def metadata_descriptor_id(self) -> str: diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index f5bc76e92..934b21059 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -13,6 +13,7 @@ # limitations under the License. +import copy import shutil import tempfile import zipfile @@ -94,18 +95,24 @@ def _extract_and_validate( settings: ValidationSettings, subscribers: list[Subscriber] | None, rocrate_path: Path ) -> Validator: """Extract a (local or downloaded) zipped RO-Crate to a temp dir and validate it.""" - original_data_path = settings.rocrate_uri - with tempfile.TemporaryDirectory() as tmp_dir: - try: - with zipfile.ZipFile(rocrate_path, "r") as zip_ref: - zip_ref.extractall(tmp_dir) - logger.debug("RO-Crate extracted to temporary directory: %s", tmp_dir) - settings.rocrate_uri = URI(str(tmp_dir)) - return _build_validator(settings, subscribers) - finally: - if original_data_path is not None: - settings.rocrate_uri = original_data_path - logger.debug("Original data path restored: %s", original_data_path) + # The extracted package must outlive this function and remain available + # through Validator.validate(); its owner is retained by the validator. + temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with + try: + with zipfile.ZipFile(rocrate_path, "r") as zip_ref: + zip_ref.extractall(temp_dir.name) + logger.debug("RO-Crate extracted to temporary directory: %s", temp_dir.name) + effective_settings = copy.copy(settings) + effective_settings.rocrate_uri = URI(temp_dir.name) + effective_settings._source_rocrate_uri = settings.rocrate_uri + validator = _build_validator(effective_settings, subscribers) + # ValidationContext is created by Validator.validate(), after this + # function returns. Keep the extracted package alive until then. + validator._extracted_crate_directory = temp_dir + return validator + except Exception: + temp_dir.cleanup() + raise def _download_remote_rocrate( @@ -113,7 +120,8 @@ def _download_remote_rocrate( ) -> Validator: """Download a remote (http/https/ftp) RO-Crate to a temp file, then extract and validate it.""" logger.debug("RO-Crate is a remote RO-Crate") - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + with tempfile.TemporaryDirectory() as download_dir: + download_path = Path(download_dir) / "download.zip" requester = HttpRequester() offline = bool(getattr(settings, "offline", False)) # In offline mode, the cache is the only source of truth. Otherwise, @@ -134,13 +142,13 @@ def _download_remote_rocrate( raise FileNotFoundError( f"Failed to download remote RO-Crate '{rocrate_path.uri}' (status {r.status_code})." ) - with Path(tmp_file.name).open("wb") as f: + with download_path.open("wb") as f: shutil.copyfileobj(r.raw, f) - logger.debug("RO-Crate downloaded to temporary file: %s", tmp_file.name) - return _extract_and_validate(settings, subscribers, Path(tmp_file.name)) + logger.debug("RO-Crate downloaded to temporary file: %s", download_path) + return _extract_and_validate(settings, subscribers, download_path) -def __initialise_validator__( +def __initialise_validator__( # noqa: C901, PLR0911 # pylint: disable=too-many-return-statements settings: dict | ValidationSettings, subscribers: list[Subscriber] | None = None ) -> Validator: """ @@ -149,11 +157,30 @@ def __initialise_validator__( # if settings is a dict, convert to ValidationSettings settings = ValidationSettings.parse(settings) + if settings.metadata_dict is not None: + if settings.packaging_mode == "attached" and not settings.metadata_only: + raise ValueError("Attached in-memory metadata requires metadata_only=True or a package directory/ZIP") + # A dictionary has no payload backend, even when called through the + # general validate() entry point rather than validate_metadata_as_dict(). + settings.metadata_only = True + return _build_validator(settings, subscribers) + # parse the rocrate path assert settings.rocrate_uri is not None, "RO-Crate URI is required" rocrate_path: URI = URI(str(settings.rocrate_uri)) logger.debug("Validating RO-Crate: %s", rocrate_path) + if ( + settings.packaging_mode == "attached" + and rocrate_path.is_remote_resource() + and (Path(rocrate_path.get_path()).suffix.lower() != ".zip") + ): + raise ValueError("Attached mode for a remote standalone document requires a package adapter") + if settings.packaging_mode == "detached" and ( + rocrate_path.is_local_directory() or Path(rocrate_path.get_path()).suffix.lower() == ".zip" + ): + raise ValueError("Detached mode requires a standalone metadata document, not a directory or ZIP") + # check if the RO-Crate exists if ( not getattr(settings, "metadata_only", False) @@ -171,6 +198,8 @@ def __initialise_validator__( # Resolve the RO-Crate source: remote URL, local ZIP, or local directory. # We support http/https/ftp protocols to download a remote RO-Crate. if rocrate_path.scheme in ("http", "https", "ftp"): + if Path(rocrate_path.get_path()).suffix.lower() != ".zip": + return _build_validator(settings, subscribers) return _download_remote_rocrate(settings, subscribers, rocrate_path) if rocrate_path.as_path().suffix == ".zip": logger.debug("RO-Crate is a local ZIP file") @@ -179,6 +208,8 @@ def __initialise_validator__( logger.debug("RO-Crate is a local directory") settings.rocrate_uri = URI(str(rocrate_path.as_path())) return _build_validator(settings, subscribers) + if rocrate_path.is_local_file() and rocrate_path.as_path().suffix.lower() in (".json", ".jsonld"): + return _build_validator(settings, subscribers) raise ValueError( f"Invalid RO-Crate URI: {rocrate_path}. It MUST be a local directory or a ZIP file (local or remote)." ) From 4dd34eff4de205c017b52e369e867de37804523e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:09:29 +0200 Subject: [PATCH 181/271] =?UTF-8?q?fix(profiles):=20=F0=9F=A7=AD=20use=20e?= =?UTF-8?q?xplicit=20package=20type=20in=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.2/must/0_file_descriptor_format.py | 11 ++++- .../ro-crate/1.2/must/1_ro_crate_preview.py | 4 +- .../1.2/must/2_root_data_entity_identifier.py | 4 +- .../1.2/must/4_data_entity_metadata.py | 48 +++++++------------ .../should/2_root_data_entity_identifier.py | 4 +- .../1.2/should/4_missing_file_local_path.py | 10 +--- 6 files changed, 33 insertions(+), 48 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 749d258a8..f9af4028d 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -15,6 +15,7 @@ import json import re from http import HTTPStatus +from pathlib import Path from typing import Any from urllib.parse import urljoin @@ -71,8 +72,14 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: logger.debug("Skipping file descriptor existence check in metadata-only mode") context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED - if not context.ro_crate.has_descriptor(): - message = f'file descriptor "{context.rel_fd_path}" is not present' + if context.ro_crate.is_attached(): + descriptor = context.ro_crate.attached_descriptor_id + exists = context.ro_crate.has_file(Path(descriptor)) + else: + descriptor = str(context.rel_fd_path) + exists = context.ro_crate.has_descriptor() + if not exists: + message = f'file descriptor "{descriptor}" is not present' context.result.add_issue(message, self) return False return True diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index f03f09ffc..d92b998b8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -32,8 +32,8 @@ class ROCrateWebsiteChecker(PyFunctionCheck): @check(name="RO-Crate Website HTML5 doctype") def check_preview_html(self, context: ValidationContext) -> CheckResultValue: try: - if context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is detached", "returned") + if not context.ro_crate.is_attached(): + context.record_skip(self, "attached package context is unavailable", "returned") return CheckResult.SKIPPED except ROCrateMetadataNotFoundError: logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py index 50f32b2e2..2fe90ec38 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py @@ -31,8 +31,8 @@ class RootDataEntityIdentifierChecker(PyFunctionCheck): @check(name="Root Data Entity: REQUIRED value") def check_identifier(self, context: ValidationContext) -> CheckResultValue: try: - if context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is detached", "returned") + if not context.ro_crate.is_attached(): + context.record_skip(self, "attached package context is unavailable", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if root_entity.id == "./": diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 7f6aed04d..c8ed3e2a8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -71,15 +71,9 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: # """ Check the presence of the Data Entity in the RO-Crate """ - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if is_detached: - logger.debug("Skipping data entity payload checks for detached RO-Crate") - context.record_skip(self, "RO-Crate is detached", "returned") + if not context.ro_crate.is_attached(): + logger.debug("Skipping data entity payload checks without an attached package context") + context.record_skip(self, "attached package context is unavailable", "returned") return CheckResult.SKIPPED # Skip the check in metadata-only mode if context.settings.metadata_only: @@ -144,14 +138,8 @@ class DetachedDataEntityChecker(PyFunctionCheck): @check(name="Detached RO-Crate: data entities MUST be web-based") def check_detached_entities(self, context: ValidationContext) -> CheckResultValue: - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if not is_detached: - context.record_skip(self, "RO-Crate is attached", "returned") + if not context.ro_crate.is_detached(): + context.record_skip(self, "detached package context is unavailable", "returned") return CheckResult.SKIPPED result = True root_entity_id = None @@ -166,6 +154,9 @@ def check_detached_entities(self, context: ValidationContext) -> CheckResultValu for entity in entities: if root_entity_id and entity.id == root_entity_id: continue + if entity.id.startswith("#"): + # Contextual entities do not identify Detached package payloads. + continue if not is_external_reference(entity.id): context.result.add_issue( f"Data Entity '{entity.id}' is not web-based, " @@ -189,13 +180,8 @@ class DataEntityIdentifierChecker(PyFunctionCheck): def check_identifiers(self, context: ValidationContext) -> CheckResultValue: result = True root_entity_id = None - root_entity_is_local = False with contextlib.suppress(ValueError): - root_data_entity = context.ro_crate.metadata.get_root_data_entity() - root_entity_id = root_data_entity.id - root_entity_is_local = ( - root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False - ) + root_entity_id = context.ro_crate.metadata.get_root_data_entity().id try: entities = context.ro_crate.metadata.get_data_entities() except ROCrateMetadataNotFoundError: @@ -205,7 +191,11 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: for entity in entities: if root_entity_id and entity.id == root_entity_id: continue - if not root_entity_is_local and not is_external_reference(entity.id): + if ( + context.ro_crate.is_detached() + and not entity.has_local_identifier() + and not is_external_reference(entity.id) + ): context.result.add_issue( f"Data Entity '{entity.id}' has a local identifier but the Root Data Entity " "does not have a local identifier", @@ -226,14 +216,8 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: @check(name="Data Entity: relative @id for payload files") def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if is_detached: - context.record_skip(self, "RO-Crate is detached", "returned") + if not context.ro_crate.is_attached(): + context.record_skip(self, "attached package context is unavailable", "returned") return CheckResult.SKIPPED result = True try: diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py index 45fef61cf..c17ee903a 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py @@ -35,7 +35,7 @@ def check_identifier(self, context: ValidationContext) -> CheckResultValue: # n """ try: if not context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is attached", "returned") + context.record_skip(self, "detached package context is unavailable", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): @@ -78,7 +78,7 @@ def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValu """ try: if not context.ro_crate.is_detached(): - context.record_skip(self, "RO-Crate is attached", "returned") + context.record_skip(self, "detached package context is unavailable", "returned") return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py index aaa88c00c..b1d9a8693 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py @@ -101,14 +101,8 @@ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) def check_missing_file_local_path(self, context: ValidationContext) -> CheckResultValue: - try: - is_detached = context.ro_crate.is_detached() - except ROCrateMetadataNotFoundError: - logger.debug("Skipping missing local file check: metadata descriptor is not available") - context.record_skip(self, "metadata descriptor is not available", "exception") - return CheckResult.SKIPPED - if is_detached or context.settings.metadata_only: - context.record_skip(self, "RO-Crate is detached or metadata-only mode is active", "configured") + if not context.ro_crate.is_attached() or context.settings.metadata_only: + context.record_skip(self, "attached payload context is unavailable", "configured") return CheckResult.SKIPPED root_entity_id = None with contextlib.suppress(ValueError): From 06c29319284ab982ab6fc6fe3ded716ec366aeb4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:26:18 +0200 Subject: [PATCH 182/271] =?UTF-8?q?feat(cli):=20=F0=9F=93=A6=20expose=20pa?= =?UTF-8?q?ckaging=20mode=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 084bb841f..7a7d22ef5 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -78,6 +78,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True, ) +@click.option( + "--packaging-mode", + type=click.Choice(["auto", "attached", "detached"], case_sensitive=False), + default="auto", + show_default=True, + help="Packaging context inferred from the input, or explicitly attached/detached", +) @click.option("-ff", "--fail-fast", is_flag=True, help="Fail fast validation mode", default=False, show_default=True) @click.option( "--creation-time", @@ -272,6 +279,7 @@ def validate( extra_profiles_path: Path | None = None, profile_identifier: tuple[str, ...] = (), metadata_only: bool = False, + packaging_mode: str = "auto", creation_time: bool = False, enforce_availability: bool = False, skip_availability_check: bool = False, @@ -350,6 +358,7 @@ def validate( "abort_on_first": fail_fast, "skip_checks": skip_checks_list, "metadata_only": metadata_only, + "packaging_mode": packaging_mode, "cache_max_age": cache_max_age if not no_cache else -1, "cache_path": cache_path, "offline": offline, From 93b1d2c40d2e9f401868aaf34eaf0a24d10e2396 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:27:18 +0200 Subject: [PATCH 183/271] =?UTF-8?q?test(cli):=20=F0=9F=A7=AA=20cover=20pac?= =?UTF-8?q?kaging=20mode=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 08eab49ce..87800be41 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -52,6 +52,24 @@ def test_version(cli_runner: CliRunner): assert get_version() in result.output +def test_validate_packaging_mode_option(cli_runner: CliRunner, tmp_path: Path): + help_result = cli_runner.invoke(cli, ["validate", "--help"]) + assert help_result.exit_code == 0 + assert "--packaging-mode" in help_result.output + + invalid_choice = cli_runner.invoke(cli, ["validate", "--packaging-mode", "unknown"]) + assert invalid_choice.exit_code != 0 + assert "auto" in invalid_choice.output + + (tmp_path / "ro-crate-metadata.json").write_text("{}") + incompatible = cli_runner.invoke( + cli, + ["validate", str(tmp_path), "--packaging-mode", "detached", "--no-auto-profile", "--no-paging"], + ) + assert incompatible.exit_code != 0 + assert "standalone metadata document" in incompatible.output + + def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): result = cli_runner.invoke( cli, From 595fcc8ef5f3504acd52014eac33de31717c82b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:28:45 +0200 Subject: [PATCH 184/271] =?UTF-8?q?feat(reports):=20=F0=9F=93=A6=20show=20?= =?UTF-8?q?package=20type=20and=20validation=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/result.py | 14 +++- rocrate_validator/models/skipped_check.py | 19 ++++- .../io_helpers/output/json/formatters.py | 2 + .../io_helpers/output/text/layout/report.py | 69 +++++++++++++------ 4 files changed, 78 insertions(+), 26 deletions(-) diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 03a4effc3..377be6741 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -32,7 +32,12 @@ RequirementLevel, Severity, ) -from rocrate_validator.models.skipped_check import SkipCategory, SkipCategoryInput, SkippedCheckDetail +from rocrate_validator.models.skipped_check import ( + SkipCategory, + SkipCategoryInput, + SkippedCheckDetail, + normalize_skip_category, +) if TYPE_CHECKING: from collections.abc import Collection @@ -56,6 +61,7 @@ def __init__( violatingProperty: str | None = None, violatingEntity: str | None = None, value: str | None = None, + *, context: ValidationContext | None = None, ): self._message = message @@ -278,7 +284,7 @@ def _record_check_result( self._check_results[check.identifier] = normalized_result if normalized_result is CheckResult.SKIPPED: - normalized_skip_category = SkipCategory(skip_category) + normalized_skip_category = normalize_skip_category(skip_category) self._executed_checks.discard(check) self._skipped_checks.add(check) self._executed_checks_results.pop(check.identifier, None) @@ -489,10 +495,14 @@ def to_dict(self) -> dict: "enable_profile_inheritance", "requirement_severity", "abort_on_first", + "metadata_only", + "packaging_mode", ] validation_settings = { key: value for key, value in self.validation_settings.to_dict().items() if key in allowed_properties } + validation_settings["package_type"] = self.context.ro_crate.package_type.value + validation_settings["packaging_mode_explicit"] = self.context.ro_crate.packaging_mode_explicit result: dict[str, Any] = { "meta": {"version": JSON_OUTPUT_FORMAT_VERSION}, "validation_settings": validation_settings, diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py index 8e6cb3ea5..ddb8e80f8 100644 --- a/rocrate_validator/models/skipped_check.py +++ b/rocrate_validator/models/skipped_check.py @@ -63,6 +63,21 @@ def description(self) -> str: ) +def normalize_skip_category(value: SkipCategoryInput) -> SkipCategory: + """Convert a public skip-category value to its enum member.""" + if isinstance(value, SkipCategory): + return value + return { + "returned": SkipCategory.RETURNED, + "configured": SkipCategory.CONFIGURED, + "exception": SkipCategory.EXCEPTION, + "deactivated": SkipCategory.DEACTIVATED, + "dependency": SkipCategory.DEPENDENCY, + "not_reached": SkipCategory.NOT_REACHED, + "inherited": SkipCategory.INHERITED, + }[value] + + class SkipRequirementCheck(Exception): """Signal that a requirement check should be skipped.""" @@ -74,7 +89,7 @@ def __init__( ): self.check = check self.message = message - self.category = SkipCategory(category) + self.category = normalize_skip_category(category) def __str__(self) -> str: return f"SkipRequirementCheck(check={self.check})" @@ -90,7 +105,7 @@ class SkippedCheckDetail: context: ValidationContext | None = field(default=None, compare=False, repr=False) def __post_init__(self) -> None: - object.__setattr__(self, "category", SkipCategory(self.category)) + object.__setattr__(self, "category", normalize_skip_category(self.category)) def to_dict(self) -> dict[str, str]: source_identifier = self.check.identifier diff --git a/rocrate_validator/utils/io_helpers/output/json/formatters.py b/rocrate_validator/utils/io_helpers/output/json/formatters.py index 842098e44..c8300cbae 100644 --- a/rocrate_validator/utils/io_helpers/output/json/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/json/formatters.py @@ -62,6 +62,8 @@ def format_validation_results( # Extract settings from the first result if available settings = results[0].validation_settings json_output["validation_settings"] = settings.to_dict() + json_output["validation_settings"]["package_type"] = results[0].context.ro_crate.package_type.value + json_output["validation_settings"]["packaging_mode_explicit"] = results[0].context.ro_crate.packaging_mode_explicit # Determine verbosity from settings verbose = settings.verbose if settings else False diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/report.py b/rocrate_validator/utils/io_helpers/output/text/layout/report.py index 9ed51e479..3c70b1d5c 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/report.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/report.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The output renderer depends on the crate entity model, whose concrete readers +# import shared report helpers; this existing package-level cycle is intentional. +# pylint: disable=cyclic-import + from __future__ import annotations import threading @@ -38,7 +42,6 @@ ) from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.colors import get_severity_color -from rocrate_validator.utils.uri import URI from rocrate_validator.utils.versioning import get_version from .dispatcher import EventDispatcher @@ -47,7 +50,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from rocrate_validator.events import Subscriber + from rocrate_validator.events import Event, Subscriber from rocrate_validator.utils.io_helpers.output.console import Console # set up logging @@ -72,6 +75,8 @@ def __init__( self._validation_checks_progress: Layout | None = None self._progress_monitor: ProgressMonitor | None = None self._subscriber: EventDispatcher = _ReportLayoutSubscriber(self) + self.base_info_layout: Layout | None = None + self._resolved_package_type: str | None = None self.requirement_checks_container_layout: Layout | None = None self.passed_checks: Layout | None = None self.failed_checks: Layout | None = None @@ -107,28 +112,17 @@ def live(self, update_callable: Callable) -> Any: return update_callable() def _init_layout(self): - - # Get the validation settings - settings = self.validation_settings - # Set the console height - self.console.height = 31 + self.console.height = 33 # Create the layout of the base info of the validation report - severity_color = get_severity_color(settings.requirement_severity) - base_info_layout = Layout( - Align( - f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(str(settings.rocrate_uri)).uri}[/bold]" - "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " - f"{settings.profile_identifier}[/bold magenta] " - f"{'[italic](autodetected)[/italic]' if self.profile_autodetected else ''}" - f"\n[bold cyan]Validation Severity:[/bold cyan] " - f"[bold {severity_color}]{settings.requirement_severity}[/bold {severity_color}]", - style="white", - align="left", - ), + result = self.result or (self.statistics.validation_result if self.statistics else None) + if result: + self._resolved_package_type = result.context.ro_crate.package_type.value + self.base_info_layout = Layout( + self._base_info(), name="Base Info", - size=5, + size=7, ) self.passed_checks = Layout(name="PASSED") self.failed_checks = Layout(name="FAILED") @@ -164,7 +158,7 @@ def _init_layout(self): # Create the layout of the report container report_container_layout = Layout(name="Report Container Layout") report_container_layout.split_column( - base_info_layout, + self.base_info_layout, Layout( Panel( requirement_checks_container_layout, @@ -202,11 +196,37 @@ def _init_layout(self): self.update_stats(self.statistics or ValidationStatistics(self.validation_settings)) # Extract the result if available - result = self.result or (self.statistics.validation_result) if self.statistics else None # Show the overall result if available if result: self.show_overall_result(result) + def _base_info(self) -> Align: + settings = self.validation_settings + severity_color = get_severity_color(settings.requirement_severity) + package_type = self._resolved_package_type or ( + settings.packaging_mode if settings.packaging_mode != "auto" else "resolving..." + ) + validation_scope = "Metadata Only" if settings.metadata_only else "Full Package" + return Align( + f"\n[bold cyan]RO-Crate:[/bold cyan] " + f"[bold]{getattr(settings, '_source_rocrate_uri', settings.rocrate_uri) or 'in-memory metadata'}[/bold]" + f"\n[bold cyan]RO-Crate Package Type:[/bold cyan] [bold yellow]{package_type.title()}[/bold yellow]" + "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " + f"{settings.profile_identifier}[/bold magenta] " + f"{'[italic](autodetected)[/italic]' if self.profile_autodetected else ''}" + f"\n[bold cyan]Validation Severity:[/bold cyan] " + f"[bold {severity_color}]{settings.requirement_severity}[/bold {severity_color}]" + f"\n[bold cyan]Validation Scope:[/bold cyan] [bold orange1]{validation_scope}[/bold orange1]", + style="white", + align="left", + ) + + def set_package_type(self, package_type: str) -> None: + """Show the resolved package type as soon as the validation context exists.""" + self._resolved_package_type = package_type + if self.base_info_layout is not None: + self.base_info_layout.update(self._base_info()) + def update_stats(self, profile_stats: ValidationStatistics | None = None): assert profile_stats, "Profile stats must be provided" assert self.passed_checks is not None and self.failed_checks is not None and self.skipped_checks is not None, ( @@ -283,6 +303,7 @@ def show_overall_result(self, result: ValidationResult | None): assert result, "Validation result must be provided" assert self.overall_result is not None, "Layout not initialized" self.result = result + self.set_package_type(result.context.ro_crate.package_type.value) if result.passed(): icon = "[OK]" if not self.console.interactive else "✅" self.overall_result.update( @@ -316,6 +337,10 @@ def __init__(self, layout: ValidationReportLayout): super().__init__("ValidationReportLayout") self._layout = layout + def _on_validation_start(self, event: Event, ctx: ValidationContext | None) -> None: + assert ctx is not None, "Validation context must be provided" + self._layout.set_package_type(ctx.ro_crate.package_type.value) + def _on_requirement_check_validation_end( self, event: RequirementCheckValidationEvent, ctx: ValidationContext | None ) -> None: From b55d6982cd50c2289ab714c156c9ad4f6cd70454 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:29:29 +0200 Subject: [PATCH 185/271] =?UTF-8?q?test(rocrate):=20=F0=9F=A7=AA=20cover?= =?UTF-8?q?=20package=20type=20detection=20and=20reporting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_package_mode.py | 314 ++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 tests/unit/test_package_mode.py diff --git a/tests/unit/test_package_mode.py b/tests/unit/test_package_mode.py new file mode 100644 index 000000000..8fa0b5669 --- /dev/null +++ b/tests/unit/test_package_mode.py @@ -0,0 +1,314 @@ +"""Packaging mode is selected from the source, independently of graph contents.""" + +import io +import json +import zipfile +from pathlib import Path + +import pytest + +from rocrate_validator.events import Event, EventType +from rocrate_validator.models import ValidationSettings +from rocrate_validator.rocrate import ROCrate +from rocrate_validator.rocrate.package_type import PackageType +from rocrate_validator.services import __initialise_validator__, validate +from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.io_helpers.output.console import Console +from rocrate_validator.utils.io_helpers.output.json.formatters import format_validation_results +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter +from rocrate_validator.utils.io_helpers.output.text.layout.report import ValidationReportLayout +from rocrate_validator.utils.uri import URI + + +@pytest.mark.parametrize("root_id", ["./", "https://example.org/crate", "urn:example:crate"]) +def test_directory_mode_does_not_depend_on_root_id_or_payload(tmp_path: Path, root_id: str): + descriptor = tmp_path / "ro-crate-metadata.json" + descriptor.write_text(json.dumps({"@graph": [{"@id": root_id, "@type": "Dataset"}]})) + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + assert crate.is_attached() and not crate.is_detached() + descriptor.write_text("not JSON") + assert ROCrate.new_instance(tmp_path).package_type is PackageType.ATTACHED + + +def test_missing_payload_does_not_change_attached_mode(tmp_path: Path): + (tmp_path / "ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + (tmp_path / "payload.txt").write_text("data") + assert ROCrate.new_instance(tmp_path).package_type is PackageType.ATTACHED + + +@pytest.mark.parametrize("root_id", ["./", "https://example.org/crate"]) +def test_standalone_document_is_detached_even_with_dot_root(tmp_path: Path, root_id: str): + descriptor = tmp_path / "crate-ro-crate-metadata.json" + descriptor.write_text(json.dumps({"@graph": [{"@id": root_id, "@type": "Dataset"}]})) + crate = ROCrate.new_instance(descriptor) + assert crate.package_type is PackageType.DETACHED + assert crate.is_detached() and not crate.is_attached() + + +def test_zip_and_explicit_mode_constraints(tmp_path: Path): + archive = tmp_path / "crate.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("ro-crate-metadata.json", "{}") + assert ROCrate.new_instance(archive).package_type is PackageType.ATTACHED + with pytest.raises(ValueError, match="standalone metadata document"): + ROCrate.new_instance(archive, packaging_mode="detached") + with pytest.raises(ValueError, match="standalone metadata document"): + ROCrate.new_instance(tmp_path, packaging_mode="detached") + assert ROCrate.new_instance(archive, relative_root_path=Path("nested")).attached_descriptor_id == ( + "nested/ro-crate-metadata.json" + ) + + +def test_bagit_package_type_and_effective_descriptor(tmp_path: Path): + (tmp_path / "bagit.txt").write_text("BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n") + (tmp_path / "data").mkdir() + (tmp_path / "data" / "ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + assert crate.attached_descriptor_id == "data/ro-crate-metadata.json" + + +def test_extracted_zip_keeps_attached_mode_and_source_alive(tmp_path: Path): + archive = tmp_path / "crate.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("ro-crate-metadata.json", json.dumps(_minimal_metadata())) + settings = ValidationSettings( + rocrate_uri=archive, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + offline=True, + disable_remote_crate_download=False, + ) + validator = __initialise_validator__(settings) + result = validator.validate() + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert result.context.ro_crate.has_descriptor() + assert result.context.ro_crate.metadata.as_dict() == _minimal_metadata() + assert result.validation_settings.to_dict()["rocrate_uri"] == str(archive) + assert settings.rocrate_uri.as_path() == archive + + +def test_downloaded_zip_preserves_attached_mode_and_remote_source(monkeypatch, tmp_path: Path): + archive = tmp_path / "crate.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("ro-crate-metadata.json", json.dumps(_minimal_metadata())) + + class Response: + status_code = 200 + + def __init__(self): + self.raw = io.BytesIO(archive.read_bytes()) + + def __enter__(self): + return self + + def __exit__(self, *_args): + self.raw.close() + + remote_uri = "https://example.org/crate.zip" + settings = ValidationSettings( + rocrate_uri=remote_uri, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + packaging_mode="attached", + disable_remote_crate_download=False, + offline=True, + ) + monkeypatch.setattr(URI, "is_available", lambda self: True) + monkeypatch.setattr(HttpRequester().session, "get", lambda *_args, **_kwargs: Response()) + result = __initialise_validator__(settings).validate() + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert result.context.ro_crate.packaging_mode_explicit + assert result.validation_settings.to_dict()["rocrate_uri"] == remote_uri + assert result.context.ro_crate.has_descriptor() + + +def test_explicit_attached_canonical_document_uses_parent(tmp_path: Path): + descriptor = tmp_path / "ro-crate-metadata.json" + descriptor.write_text("{}") + crate = ROCrate.new_instance(descriptor, packaging_mode="attached") + assert crate.package_type is PackageType.ATTACHED + assert crate.packaging_mode_explicit + assert crate.uri.as_path() == tmp_path + + +def test_in_memory_source_has_no_implicit_directory(monkeypatch, tmp_path: Path): + metadata = {"@graph": [{"@id": "./", "@type": "Dataset"}]} + first = ROCrate.from_metadata_dict(metadata) + monkeypatch.chdir(tmp_path) + second = ROCrate.from_metadata_dict(metadata) + assert first.package_type is second.package_type is PackageType.UNSPECIFIED + assert not first.is_attached() and not first.is_detached() + assert str(first.uri) == str(second.uri) + assert not first.has_file(Path("payload.txt")) + assert ROCrate.from_metadata_dict(metadata, "detached").is_detached() + assert ROCrate.from_metadata_dict(metadata, "attached").is_attached() + assert ROCrate.from_metadata_dict({}).metadata.as_json() == "{}" + + +def test_settings_and_service_reject_invalid_context(tmp_path: Path): + with pytest.raises(ValueError, match="packaging_mode"): + ValidationSettings(rocrate_uri=tmp_path, packaging_mode="unknown", offline=True) + settings = ValidationSettings( + rocrate_uri=tmp_path, metadata_dict={"@graph": []}, packaging_mode="attached", offline=True + ) + with pytest.raises(ValueError, match="metadata_only"): + __initialise_validator__(settings) + with pytest.raises(ValueError, match="remote standalone"): + ROCrate.new_instance("https://example.org/ro-crate-metadata.json", packaging_mode="attached") + in_memory = __initialise_validator__({"metadata_dict": {}, "offline": True}) + assert in_memory.validation_settings.metadata_only is True + + +def _minimal_metadata(*, root_id: str = "./", file_id: str | None = None) -> dict: + graph = [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": root_id}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.2"}, + }, + { + "@id": root_id, + "@type": "Dataset", + "name": "Test crate", + "description": "A minimal test crate", + "datePublished": "2026-01-01", + "license": {"@id": "https://spdx.org/licenses/CC0-1.0"}, + }, + ] + if file_id is not None: + graph.append({"@id": file_id, "@type": "File", "name": "Test file"}) + return {"@context": {"@vocab": "http://schema.org/"}, "@graph": graph} + + +def _validate_metadata(metadata: dict, mode: str): + return validate( + { + "metadata_dict": metadata, + "metadata_only": True, + "packaging_mode": mode, + "offline": True, + "requirement_severity": "REQUIRED", + "profile_identifier": "ro-crate-1.2", + } + ) + + +def test_metadata_mode_controls_detached_rule_and_reports_scope(monkeypatch, tmp_path: Path): + metadata = _minimal_metadata(file_id="missing.txt") + first = _validate_metadata(metadata, "auto") + (tmp_path / "missing.txt").write_text("irrelevant") + monkeypatch.chdir(tmp_path) + second = _validate_metadata(metadata, "auto") + assert first.passed() and second.passed() + assert first.skipped_checks_count == second.skipped_checks_count + assert any("package context is unavailable" in detail.message for detail in first.skipped_check_details) + assert first.context.publicID == second.context.publicID + assert first.to_dict()["validation_settings"]["package_type"] == "unspecified" + assert first.to_dict()["validation_settings"]["packaging_mode"] == "auto" + assert first.to_dict()["validation_settings"]["metadata_only"] is True + + attached = _validate_metadata(metadata, "attached") + detached = _validate_metadata(metadata, "detached") + assert attached.passed() + assert not detached.passed() + assert any("not web-based" in issue.message for issue in detached.issues) + assert detached.to_dict()["validation_settings"]["packaging_mode_explicit"] is True + + +def test_detached_contextual_file_is_not_treated_as_payload(): + metadata = _minimal_metadata(file_id="#contextual-file") + result = _validate_metadata(metadata, "detached") + assert not any("not web-based" in issue.message for issue in result.issues) + + +@pytest.mark.parametrize("mode,expected", [("auto", "Unspecified"), ("attached", "Attached"), ("detached", "Detached")]) +def test_reports_show_package_type_after_target_profile(mode: str, expected: str): + result = _validate_metadata(_minimal_metadata(), mode) + console = Console(record=True, width=120) + layout = ValidationReportLayout(console, result.validation_settings) + _ = layout.layout + assert layout.base_info_layout is not None + + layout.subscribers[0].update(Event(EventType.VALIDATION_START), result.context) + assert f"[bold yellow]{expected}[/bold yellow]" in layout.base_info_layout.renderable.renderable + assert "[bold orange1]Metadata Only[/bold orange1]" in layout.base_info_layout.renderable.renderable + console.print(layout.base_info_layout.renderable) + interactive_text = console.export_text() + assert "Validation Severity:" in interactive_text + assert f"RO-Crate Package Type: {expected}" in interactive_text + assert interactive_text.index("RO-Crate:") < interactive_text.index("RO-Crate Package Type:") + assert interactive_text.index("RO-Crate Package Type:") < interactive_text.index("Target Profile:") + assert "Validation Scope: Metadata Only" in interactive_text + + report = json.loads(format_validation_results({"ro-crate-1.2": result})) + assert report["validation_settings"]["package_type"] == expected.lower() + assert report["validation_settings"]["packaging_mode"] == mode + assert report["validation_settings"]["metadata_only"] is True + assert result.to_dict()["validation_settings"]["package_type"] == expected.lower() + assert result.to_dict()["validation_settings"]["packaging_mode"] == mode + assert result.to_dict()["validation_settings"]["metadata_only"] is True + + text_console = Console(record=True, width=120) + text_console.print(ValidationResultTextOutputFormatter(result)) + text_report = text_console.export_text() + assert "Packaging mode:" not in text_report + assert "validation scope:" not in text_report + + +def test_interactive_report_shows_full_package_scope(tmp_path: Path): + settings = ValidationSettings(rocrate_uri=tmp_path, metadata_only=False, offline=True) + console = Console(record=True, width=120) + layout = ValidationReportLayout(console, settings) + _ = layout.layout + assert layout.base_info_layout is not None + console.print(layout.base_info_layout.renderable) + assert "Validation Scope: Full Package" in console.export_text() + + +def test_attached_directory_missing_payload_stays_attached(tmp_path: Path): + (tmp_path / "ro-crate-metadata.json").write_text(json.dumps(_minimal_metadata(file_id="missing.txt"))) + settings = ValidationSettings( + rocrate_uri=tmp_path, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + offline=True, + ) + result = validate(settings) + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert any("missing.txt" in issue.message for issue in result.issues) + (tmp_path / "missing.txt").write_text("now available") + available_result = validate(settings) + assert available_result.context.ro_crate.package_type is PackageType.ATTACHED + assert not any("missing.txt" in issue.message for issue in available_result.issues) + + +def test_prefixed_descriptor_in_directory_does_not_become_detached(tmp_path: Path): + (tmp_path / "prefix-ro-crate-metadata.json").write_text(json.dumps(_minimal_metadata())) + settings = ValidationSettings( + rocrate_uri=tmp_path, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + offline=True, + ) + result = validate(settings) + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert any('file descriptor "ro-crate-metadata.json" is not present' in issue.message for issue in result.issues) + + +def test_ambiguous_attached_descriptors_are_not_selected_arbitrarily(tmp_path: Path): + (tmp_path / "first-ro-crate-metadata.json").write_text("{}") + (tmp_path / "second-ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + assert crate.metadata_descriptor_id == "ro-crate-metadata.json" + assert not crate.has_descriptor() + + (tmp_path / "ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.metadata_descriptor_id == "ro-crate-metadata.json" + assert crate.has_descriptor() From 47db9ac30a8cdf31f52a974c10ac16c901cca3ab Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:33:32 +0200 Subject: [PATCH 186/271] =?UTF-8?q?test(profiles):=20=F0=9F=A7=AA=20addres?= =?UTF-8?q?s=20detached=20RO-Crate=20inputs=20explicitly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...e-metadata.json => ro-crate-metadata.json} | 0 tests/ro_crates_v1_2.py | 49 ++++++++++++++++--- tests/ro_crates_v1_3.py | 49 ++++++++++++++++--- 3 files changed, 84 insertions(+), 14 deletions(-) rename tests/data/crates/valid/ro-crate-1.2-absolute-root/{prefix-ro-crate-metadata.json => ro-crate-metadata.json} (100%) diff --git a/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.2-absolute-root/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json rename to tests/data/crates/valid/ro-crate-1.2-absolute-root/ro-crate-metadata.json diff --git a/tests/ro_crates_v1_2.py b/tests/ro_crates_v1_2.py index 9273f368f..60ad396b9 100644 --- a/tests/ro_crates_v1_2.py +++ b/tests/ro_crates_v1_2.py @@ -150,15 +150,33 @@ def __remote_base_url__(self) -> str: @property def valid_local_descriptor_filename(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "valid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "valid" + / "basic-ro-crate-metadata.json" + ) @property def invalid_local_descriptor_filename(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "invalid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "invalid" + / "ro-crate-metadata.json" + ) @property def valid_root_data_entity_identifier_when_online_available(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "root-data-entity-identifier" / "online-available" / "valid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "root-data-entity-identifier" + / "online-available" + / "valid" + / "prefix-ro-crate-metadata.json" + ) @property def invalid_root_data_entity_identifier_when_online_available(self) -> Path: @@ -242,11 +260,21 @@ def invalid_required_downloadable_citeAs(self) -> Path: @property def valid_recommended_citeAs_for_resolvable_id(self) -> Path: - return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "valid" + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "valid" + / "citeas-ro-crate-metadata.json" + ) @property def invalid_recommended_citeAs_for_resolvable_id(self) -> Path: - return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "invalid" + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "invalid" + / "citeas-ro-crate-metadata.json" + ) @property def valid_additional_conformsTo_reference(self) -> Path: @@ -415,11 +443,18 @@ def invalid_local_entity_reference(self) -> Path: @property def valid_detached_rocrate_dataEntities(self) -> Path: - return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" + return ( + self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" / "basic-ro-crate-metadata.json" + ) @property def invalid_detached_rocrate_dataEntities(self) -> Path: - return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "invalid" + return ( + self.DATA_ENTITIES_CRATES_PATH + / "detached_rocrate_dataEntities" + / "invalid" + / "basic-ro-crate-metadata.json" + ) @property def valid_recommended_properties(self) -> Path: diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py index a893fe2ae..53744e2f6 100644 --- a/tests/ro_crates_v1_3.py +++ b/tests/ro_crates_v1_3.py @@ -150,15 +150,33 @@ def __remote_base_url__(self) -> str: @property def valid_local_descriptor_filename(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "valid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "valid" + / "basic-ro-crate-metadata.json" + ) @property def invalid_local_descriptor_filename(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "invalid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "invalid" + / "ro-crate-metadata.json" + ) @property def valid_root_data_entity_identifier_when_online_available(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "root-data-entity-identifier" / "online-available" / "valid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "root-data-entity-identifier" + / "online-available" + / "valid" + / "prefix-ro-crate-metadata.json" + ) @property def invalid_root_data_entity_identifier_when_online_available(self) -> Path: @@ -234,11 +252,21 @@ def invalid_required_downloadable_citeAs(self) -> Path: @property def valid_recommended_citeAs_for_resolvable_id(self) -> Path: - return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "valid" + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "valid" + / "citeas-ro-crate-metadata.json" + ) @property def invalid_recommended_citeAs_for_resolvable_id(self) -> Path: - return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "invalid" + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "invalid" + / "citeas-ro-crate-metadata.json" + ) @property def valid_additional_conformsTo_reference(self) -> Path: @@ -415,11 +443,18 @@ def invalid_local_entity_reference(self) -> Path: @property def valid_detached_rocrate_dataEntities(self) -> Path: - return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" + return ( + self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" / "basic-ro-crate-metadata.json" + ) @property def invalid_detached_rocrate_dataEntities(self) -> Path: - return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "invalid" + return ( + self.DATA_ENTITIES_CRATES_PATH + / "detached_rocrate_dataEntities" + / "invalid" + / "basic-ro-crate-metadata.json" + ) @property def valid_recommended_properties(self) -> Path: From 63e38d11e8647e4ba0f001033f71587838897798 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 16:35:10 +0200 Subject: [PATCH 187/271] =?UTF-8?q?chore(lint):=20=F0=9F=A7=B9=20resolve?= =?UTF-8?q?=20pre-commit=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 1 - pyproject.toml | 12 ++++++++++++ rocrate_validator/cli/commands/profiles.py | 3 ++- rocrate_validator/models/requirement.py | 6 ++++++ rocrate_validator/models/statistics.py | 1 + rocrate_validator/utils/versioning.py | 2 +- .../profiles/ro-crate-1.2/test_metadata_document.py | 7 ++----- .../test_metadata_contextualEntities_v1_3.py | 2 ++ .../ro-crate-1.3/test_metadata_dataEntities_v1_3.py | 2 ++ .../ro-crate-1.3/test_metadata_document_v1_3.py | 2 +- .../test_metadata_rootDataEntity_v1_3.py | 6 +++--- .../profiles/ro-crate/test_file_uri_data_entities.py | 7 +++---- tests/unit/requirements/test_dependencies.py | 2 +- tests/unit/test_check_result.py | 5 +++-- tests/unit/test_jsonld.py | 2 +- 15 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6968afc79..9e8e4f7f5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,3 @@ updates: weekly-batch: patterns: - "*" - diff --git a/pyproject.toml b/pyproject.toml index b10a677c6..a44f340ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -202,6 +202,10 @@ disable = [ # the underscore signals "internal API" within the package, not a hard # privacy boundary. Per-callsite suppressions would be noisy. "protected-access", # W0212 + # The crate readers and text report layout depend on each other's shared + # model helpers; Pylint reports this intentional package-level cycle at + # graph-build time, before file-level suppressions are considered. + "cyclic-import", # R0401 ] # Mypy configuration. @@ -212,6 +216,14 @@ disable = [ [tool.mypy] python_version = "3.10" # lowest supported version (see dependencies.python) files = ["rocrate_validator"] # the package; tests are not type-checked (yet) +exclude = [ + # Versioned profiles are loaded from directories rather than imported as + # Python packages, so repeated script basenames map to duplicate modules. + "rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py", + "rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py", + "rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py", + "rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py", +] # Surface configuration/annotation drift without being noisy. warn_unused_configs = true diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 21927335d..08de1b747 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -17,6 +17,7 @@ from pathlib import Path from typing import Any +from click.exceptions import Exit from rich.markdown import Markdown from rich.padding import Padding from rich.panel import Panel @@ -215,7 +216,7 @@ def check_profile(ctx, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, no_ except Exception as e: handle_error(e, console) if failed: - raise click.exceptions.Exit(1) + raise Exit(1) @profiles.command("describe") diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 9e6004314..5a3867c48 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -51,6 +51,10 @@ from rocrate_validator.models.profile import Profile from rocrate_validator.models.validation import ValidationContext +# This module contains the requirement/check model and its loader algorithms. +# Keep the related model code together; splitting it would create circular imports. +# pylint: disable=too-many-lines + @total_ordering class Requirement(ABC): @@ -758,6 +762,8 @@ def _topological_order( return [nodes[index] for index in ordered_indices] + # The topological ordering algorithm keeps its graph bookkeeping local. + # pylint: disable=too-many-locals @classmethod def order_by_dependencies( cls, diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index 7900e0c27..caca14b80 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -794,6 +794,7 @@ def __compute_averall_stats__(self): raw_stats = self.__aggregate_raw_stats__(self._statistics_list) return self.__build_sorted_stats_dict__(raw_stats) + # pylint: disable=too-many-locals @classmethod def __aggregate_raw_stats__( cls, diff --git a/rocrate_validator/utils/versioning.py b/rocrate_validator/utils/versioning.py index 4356ea4bf..aecfee3c2 100644 --- a/rocrate_validator/utils/versioning.py +++ b/rocrate_validator/utils/versioning.py @@ -111,7 +111,7 @@ def _is_source_checkout(package_root: Path) -> bool: return False git_root = run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=package_root) - return bool(git_root) and Path(git_root).resolve() == package_root.resolve() + return bool(git_root) and Path(git_root or "").resolve() == package_root.resolve() def get_version() -> str: diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index e7ce584f5..c84b39e19 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -72,7 +72,7 @@ def track_descriptor_reads(crate, path, binary_mode=True): metadata_dict=metadata_dict, metadata_only=True, ) - assert descriptor_reads == [] + assert not descriptor_reads def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_path): @@ -449,10 +449,7 @@ def test_https_entity_id_is_not_recommended(): metadata_dict=metadata_dict, ) ) - assert not any( - issue.check.requirement.name == "Schema.org @id compatibility" - for issue in result.get_issues() - ) + assert not any(issue.check.requirement.name == "Schema.org @id compatibility" for issue in result.get_issues()) def test_https_nested_schema_org_id_is_recommended_for_compatibility(): diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py index 3fa58964a..9cce3460a 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=invalid-name + import logging from rocrate_validator import models diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py index 333e468f3..40c586aed 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=invalid-name + import logging from rocrate_validator import models diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py index 10cf57fe8..378f615d1 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py @@ -72,7 +72,7 @@ def track_descriptor_reads(crate, path, binary_mode=True): metadata_dict=metadata_dict, metadata_only=True, ) - assert descriptor_reads == [] + assert not descriptor_reads def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_path): diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py index 855556104..7d53b6f33 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=invalid-name + import logging from pathlib import Path @@ -44,9 +46,7 @@ def test_root_data_entity_type_target_ignores_preview_about_literal(): ) for profile_version, descriptor_name in descriptor_cases: - profile_path = ( - Path(__file__).resolve().parents[4] / "rocrate_validator/profiles/ro-crate" / profile_version - ) + profile_path = Path(__file__).resolve().parents[4] / "rocrate_validator/profiles/ro-crate" / profile_version shapes = Graph() shapes.parse(profile_path / "prefixes.ttl", format="turtle") shapes.parse(profile_path / "must/2_root_data_entity_metadata.ttl", format="turtle") diff --git a/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py b/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py index 7fb72ae62..1e2b08180 100644 --- a/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py +++ b/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py @@ -62,10 +62,9 @@ def test_identifier_path_syntax(tmp_path, version, entity_id, issue, detached): def _validate(crate, version, entity_id, valid, detached=False, expected_issue=None): - source = Path( - f"tests/data/crates/rocrate-{version}/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json" - ) - metadata = json.loads(source.read_text()) + relative_path = "8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json" + source = Path(f"tests/data/crates/rocrate-{version}") / relative_path + metadata = json.loads(source.read_text(encoding="utf-8")) graph = metadata["@graph"] root = next(e for e in graph if e["@id"] == "./") files = [e for e in graph if e.get("@type") == "File"] diff --git a/tests/unit/requirements/test_dependencies.py b/tests/unit/requirements/test_dependencies.py index cd3a83824..84f15ce5f 100644 --- a/tests/unit/requirements/test_dependencies.py +++ b/tests/unit/requirements/test_dependencies.py @@ -93,7 +93,7 @@ def test_order_by_dependencies_reorders_requirements(): dependent_requirement, ] assert dependent.depends_on == ("base",) - assert base.depends_on == () + assert not base.depends_on def test_inherited_dependency_is_resolved_from_effective_profile() -> None: diff --git a/tests/unit/test_check_result.py b/tests/unit/test_check_result.py index 029809b41..bdd8f8fa8 100644 --- a/tests/unit/test_check_result.py +++ b/tests/unit/test_check_result.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, cast +from typing import Any, cast # pylint: disable=unused-import import pytest @@ -46,7 +46,8 @@ def check_function(check_instance, context): check_function.__annotations__["return"] = result_type decorated = check()(check_function) - assert decorated.check + check_attribute = "check" + assert getattr(decorated, check_attribute) def test_check_decorator_rejects_invalid_return_annotation(): diff --git a/tests/unit/test_jsonld.py b/tests/unit/test_jsonld.py index 4c5830a0d..65ae696e2 100644 --- a/tests/unit/test_jsonld.py +++ b/tests/unit/test_jsonld.py @@ -46,4 +46,4 @@ def test_find_singleton_property_arrays_preserves_jsonld_semantics(): def test_find_singleton_property_arrays_ignores_invalid_graph_shape(): - assert find_singleton_property_arrays({"@context": [], "@graph": {}}) == [] + assert not find_singleton_property_arrays({"@context": [], "@graph": {}}) From 858a0d3c462b0cd6c0c15c7993772579651577b2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 19:41:54 +0200 Subject: [PATCH 188/271] =?UTF-8?q?fix(validation):=20=F0=9F=93=A6=20allow?= =?UTF-8?q?=20attached=20metadata-only=20document=20inputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 1 + rocrate_validator/rocrate/base.py | 27 ++++++++++++++++++-------- rocrate_validator/services.py | 1 + 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 641e6534c..1a51948ca 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -559,6 +559,7 @@ def __init__(self, validator: Validator, settings: ValidationSettings): rocrate_uri, relative_root_path=settings.rocrate_relative_root_path, packaging_mode=settings.packaging_mode, + metadata_only=settings.metadata_only, ) assert isinstance(self._rocrate, ROCrate), "Invalid RO-Crate instance" diff --git a/rocrate_validator/rocrate/base.py b/rocrate_validator/rocrate/base.py index 781ed129c..88ea6e66a 100644 --- a/rocrate_validator/rocrate/base.py +++ b/rocrate_validator/rocrate/base.py @@ -119,7 +119,7 @@ def metadata(self) -> ROCrateMetadata: @property def package_type(self) -> PackageType: - """Resolved RO-Crate package type, based on the input source.""" + """Package context selected explicitly or assumed from the input source.""" return getattr(self, "_package_type", PackageType.ATTACHED) @property @@ -382,7 +382,10 @@ def from_metadata_dict(metadata_dict: dict, packaging_mode: str = "auto") -> ROC @staticmethod def new_instance( # noqa: C901 # pylint: disable=too-many-locals - uri: str | Path | URI, relative_root_path: Path | None = None, packaging_mode: str = "auto" + uri: str | Path | URI, + relative_root_path: Path | None = None, + packaging_mode: str = "auto", + metadata_only: bool = False, ) -> ROCrate: """ Create a new instance of the RO-Crate based on the URI. @@ -390,6 +393,9 @@ def new_instance( # noqa: C901 # pylint: disable=too-many-locals :param uri: the URI of the RO-Crate :type uri: Union[str, Path, URI] + :param packaging_mode: Explicit package context or an input-based default. + :param metadata_only: Allow Attached document input without a payload backend. + :return: a new instance of the RO-Crate :rtype: ROCrate @@ -415,7 +421,12 @@ def new_instance( # noqa: C901 # pylint: disable=too-many-locals # Reject incompatible source/mode combinations before network access. if not isinstance(uri, URI): uri = URI(uri) - if mode == "attached" and uri.is_remote_resource() and Path(uri.get_path()).suffix.lower() != ".zip": + if ( + mode == "attached" + and not metadata_only + and uri.is_remote_resource() + and Path(uri.get_path()).suffix.lower() != ".zip" + ): raise ValueError("Attached mode for a remote standalone document requires a package adapter") if mode == "detached" and (uri.is_local_directory() or Path(uri.get_path()).suffix.lower() == ".zip"): raise ValueError("Detached mode requires a standalone metadata document, not a directory or ZIP") @@ -446,12 +457,13 @@ def new_instance( # noqa: C901 # pylint: disable=too-many-locals crate._package_type = PackageType.ATTACHED crate._packaging_mode_explicit = mode != "auto" return crate - if mode == "attached": + if mode == "attached" and not metadata_only: if uri.as_path().name != ROCrateMetadata.METADATA_FILE_DESCRIPTOR: raise ValueError("Attached mode requires a canonical ro-crate-metadata.json or a package directory") return ROCrate.new_instance(uri.as_path().parent, packaging_mode="attached") crate = ROCrateLocalMetadataFile(uri, relative_root_path=relative_root_path) - crate._package_type = PackageType.DETACHED + crate._package_type = PackageType.ATTACHED if mode == "attached" else PackageType.DETACHED + crate._packaging_mode_explicit = mode != "auto" return crate # check if the URI is a remote zip file if uri.is_remote_resource(): @@ -466,10 +478,9 @@ def new_instance( # noqa: C901 # pylint: disable=too-many-locals crate._package_type = PackageType.ATTACHED crate._packaging_mode_explicit = mode != "auto" return crate - if mode == "attached": - raise ValueError("Attached mode for a remote standalone document requires a package adapter") crate = ROCrateRemoteMetadataFile(uri, relative_root_path=relative_root_path) - crate._package_type = PackageType.DETACHED + crate._package_type = PackageType.ATTACHED if mode == "attached" else PackageType.DETACHED + crate._packaging_mode_explicit = mode != "auto" return crate # if the URI is not supported, raise an error raise ROCrateInvalidURIError(uri=uri, message="Unsupported RO-Crate URI") diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 934b21059..bbf968044 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -172,6 +172,7 @@ def __initialise_validator__( # noqa: C901, PLR0911 # pylint: disable=too-many if ( settings.packaging_mode == "attached" + and not settings.metadata_only and rocrate_path.is_remote_resource() and (Path(rocrate_path.get_path()).suffix.lower() != ".zip") ): From 38790db14049e459c2147ef7c1488e207883c539 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 19:42:34 +0200 Subject: [PATCH 189/271] =?UTF-8?q?fix(profiles):=20=F0=9F=A7=AD=20rely=20?= =?UTF-8?q?on=20detached=20data=20entity=20requirement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/1.2/must/4_data_entity_metadata.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index c8ed3e2a8..74ec0cb43 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -191,19 +191,6 @@ def check_identifiers(self, context: ValidationContext) -> CheckResultValue: for entity in entities: if root_entity_id and entity.id == root_entity_id: continue - if ( - context.ro_crate.is_detached() - and not entity.has_local_identifier() - and not is_external_reference(entity.id) - ): - context.result.add_issue( - f"Data Entity '{entity.id}' has a local identifier but the Root Data Entity " - "does not have a local identifier", - self, - ) - result = False - if context.fail_fast: - return False if entity.has_local_identifier(): continue path_error = _data_entity_path_error(entity.id) From 884f0754eaefa7f98a4082f2707cda48f7df7e3c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 19:43:15 +0200 Subject: [PATCH 190/271] =?UTF-8?q?test(profiles):=20=F0=9F=A7=AA=20expect?= =?UTF-8?q?=20detached=20web-based=20identifier=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/ro-crate-1.2/test_metadata_dataEntities.py | 4 ++-- .../profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py index 56066bc2a..0e5c96263 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py @@ -72,8 +72,8 @@ def test_invalid_detached_rocrate_dataEntities(): models.Severity.REQUIRED, False, profile_identifier="ro-crate-1.2", - expected_triggered_requirements=["Data Entity: identifier requirements"], - expected_triggered_issues=["has a local identifier but the Root Data Entity does not have a local identifier"], + expected_triggered_requirements=["Detached RO-Crate: data entities MUST be web-based"], + expected_triggered_issues=["MUST have an absolute URL as @id"], ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py index 40c586aed..63b542fde 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py @@ -74,8 +74,8 @@ def test_invalid_detached_rocrate_dataEntities(): models.Severity.REQUIRED, False, profile_identifier="ro-crate-1.3", - expected_triggered_requirements=["Data Entity: identifier requirements"], - expected_triggered_issues=["has a local identifier but the Root Data Entity does not have a local identifier"], + expected_triggered_requirements=["Detached RO-Crate: data entities MUST be web-based"], + expected_triggered_issues=["MUST have an absolute URL as @id"], ) From 66da2b9b8710a5459fa8a33f4fa1df75b4c01d34 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 19:43:48 +0200 Subject: [PATCH 191/271] =?UTF-8?q?fix(cli):=20=F0=9F=92=AC=20describe=20p?= =?UTF-8?q?ackage=20mode=20as=20assumed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7a7d22ef5..f7e2c01ba 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -83,7 +83,7 @@ def validate_uri(ctx, param, value): type=click.Choice(["auto", "attached", "detached"], case_sensitive=False), default="auto", show_default=True, - help="Packaging context inferred from the input, or explicitly attached/detached", + help="Packaging context assumed from the input, or explicitly attached/detached", ) @click.option("-ff", "--fail-fast", is_flag=True, help="Fail fast validation mode", default=False, show_default=True) @click.option( From 3b1a66e95779d460b30388e905c76330128800ea Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Oct 2026 19:44:38 +0200 Subject: [PATCH 192/271] =?UTF-8?q?docs(package-modes):=20=F0=9F=93=9A=20e?= =?UTF-8?q?xplain=20modes=20and=20validation=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + docs/0_toc.rst | 1 + docs/11_writing_a_profile.rst | 20 +++ docs/2_usage_cli.rst | 2 + docs/3_usage_api.rst | 6 + docs/6_package_modes.rst | 294 ++++++++++++++++++++++++++++++++++ 6 files changed, 326 insertions(+) create mode 100644 docs/6_package_modes.rst diff --git a/README.md b/README.md index 1188d3299..334a48bca 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,9 @@ where `` is the path to the RO-Crate you want to validate. Type `rocrate-validator --help` for more information. +For package modes, validation scope, and supported input combinations, see the +[package modes guide](https://rocrate-validator.readthedocs.io/en/latest/6_package_modes.html). + ## Programmatic Validation You can also integrate the package programmatically in your Python code. diff --git a/docs/0_toc.rst b/docs/0_toc.rst index 7aa8efa1f..52d470bd7 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -26,6 +26,7 @@ :caption: Using the Validator 4_how_it_works + 6_package_modes 12_validation_profiles 5_offline_mode 10_api diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 5e956f8db..c805eb794 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -136,6 +136,26 @@ For example, this shape requires every ``schema:Dataset`` to have a ``name``: Python checks ^^^^^^^^^^^^^ +Package mode selects Attached or Detached validation rules. Validation scope +selects whether to check metadata and data files, or metadata only. See +:ref:`package_modes` for definitions, defaults, and supported input combinations. + +For a check that applies only to one package mode, use +``context.ro_crate.is_attached()`` or ``context.ro_crate.is_detached()``. Both +return ``False`` when the mode is unspecified, as with dictionary input in +automatic mode. Record a skip if your check requires a mode that was not selected. +Do not choose the mode yourself from Root identifiers, the presence of data +files, or the resources described in the metadata. + +With ``metadata_only=True``, keep metadata checks active and skip checks that +need access to the data files (the payload). Attached rules can apply to metadata +supplied as a file, URL, or dictionary even when those data files are unavailable. +Detached identifier rules also remain active in metadata-only validation. + +The dedicated Detached Data Entity check enforces its identifier restriction. +General identifier checks should check syntax without reporting the same +restriction again. + Python checks are declared with the ``@check`` decorator in a requirement module. The decorator's ``name`` and severity identify the check in the same way as ``sh:name`` and severity identify a SHACL check. The function receives diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 8891fa290..ce33748ef 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -43,3 +43,5 @@ CLI Validation * To select additional validation profiles, see :doc:`Validation profiles <12_validation_profiles>`; to create a new profile, see :doc:`Writing a profile <11_writing_a_profile>`. + * For choosing Attached/Detached rules, checking only metadata, and + supported inputs, see :ref:`package_modes`. diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 4fa929d08..ea65cb895 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -39,6 +39,8 @@ Programmatic Validation * To select or write validation profiles, see :doc:`Validation profiles <12_validation_profiles>` and :doc:`Writing a profile <11_writing_a_profile>`. + * For package modes, scope, and accepted input combinations, see + :ref:`package_modes`. Metadata-only Validation @@ -49,6 +51,10 @@ the library also supports metadata-only validation. This is useful when you want to ensure that the metadata conforms to the expected schema without checking the actual data files. +You can check only metadata using either Attached or Detached rules. See +:ref:`package_modes` for the meaning of these modes, their defaults, and examples +using a metadata file or URL. + To perform metadata-only validation, you can use the `validate_metadata_as_dict` from the `rocrate_validator.services` module. This function takes a dictionary representing the metadata and validates it against a given validation profile. diff --git a/docs/6_package_modes.rst b/docs/6_package_modes.rst new file mode 100644 index 000000000..33626eec2 --- /dev/null +++ b/docs/6_package_modes.rst @@ -0,0 +1,294 @@ +.. _package_modes: + +Package modes and validation scope +================================== + +RO-Crate metadata describes data files and their context. It can be supplied +as part of a package or as a separate metadata document. The distinction +between Attached and Detached RO-Crates determines which validation rules apply. + +An **Attached RO-Crate** has a root directory containing +``ro-crate-metadata.json`` and, optionally, data files. These data files are +called the **payload**. An Attached crate can also refer to resources on the web. + +A **Detached RO-Crate** is a metadata document used without a package root +directory. Its Data Entities (files and directories described in the metadata, +excluding the Root Data Entity, which describes the crate as a whole) must be +web-based resources with absolute identifiers. An absolute identifier includes +a URI scheme, as in ``https://example.org/data.csv``; ``data.csv`` is relative. + +Package mode and validation scope answer two separate questions: + +* **Which rules should be applied?** Use ``--packaging-mode`` to select + Attached or Detached rules, or let ``auto`` choose from the input. +* **Should data files be checked?** Add ``--metadata-only`` to check only the + metadata. This keeps the selected mode's metadata rules. + +For example, if you have metadata from an Attached crate but cannot share its +data files, use ``--packaging-mode attached --metadata-only``. + +In this guide, a **metadata document** means JSON/JSON-LD supplied as a local +file or through a URL. A **dictionary input** means metadata already loaded +into a Python dictionary and supplied through the API. + +Modes and automatic assumptions +------------------------------- + +The `RO-Crate specification +`_ +allows software to assume a package mode from its input or process metadata +without selecting a package mode. The validator uses the defaults described +below. These assumptions select the validation rules; they do not establish +the original package type. + +The CLI option ``--packaging-mode`` and API setting +``ValidationSettings.packaging_mode`` accept the following values: + +.. list-table:: Available modes + :header-rows: 1 + :class: package-mode-table + :widths: 20 80 + + * - Mode + - Behaviour + * - ``auto`` (default) + - * **Use when** → the input-based assumptions below suit your case. + * **Effect** → select Attached or Detached rules from the input format; + leave the type *Unspecified* for a Python dictionary. + * - ``attached`` + - * **Use when** → you are validating an Attached crate, including a copy + of its metadata supplied without the data files. + * **Effect** → apply Attached rules. Data Entities can use relative + payload paths or absolute identifiers for web-based resources. + * - ``detached`` + - * **Use when** → you are validating metadata as a Detached crate. + * **Effect** → apply Detached rules. Data Entities must have absolute + identifiers and refer to web-based resources. + +With ``auto``, the validator makes these assumptions: + +* **Directory or ZIP** → *Attached*. +* **Metadata document**, supplied as a local file or URL → *Detached*. +* **Python dictionary** → *Unspecified*. Common metadata checks run, while + checks requiring Attached or Detached rules are recorded as skipped. + +Here, *Unspecified* means that no package type was selected. It is a reported +value, not a fourth option for ``--packaging-mode``. Select ``attached`` or +``detached`` explicitly to enable the corresponding checks for dictionary input. + +.. note:: + + **A metadata document is not necessarily a Detached crate.** It may be a + copy of the metadata from an Attached crate. In that case, select + ``--packaging-mode attached --metadata-only`` explicitly. Using + ``--metadata-only`` alone does not change the automatic choice of Detached. + +The automatic choice uses the input format. It does not examine the Root Data +Entity's identifier or determine whether the described data files exist or are +on the web. An Attached package may contain no data files. + +What metadata-only changes +-------------------------- + +``--metadata-only`` (``metadata_only=True`` in the API) skips data-file checks. +It does **not** change the selected package type or relax its metadata rules. +For example, Detached mode still requires absolute Data Entity identifiers: +``data.csv`` does not meet that requirement, even if a base URL could resolve it +to an absolute URL. See the `File Data Entity requirements +`_. + +A successful metadata-only result says nothing about whether the data files +are present or valid. For *Unspecified* input, checks specific to Attached or +Detached crates are also skipped. The report lists the skipped checks. + +Input compatibility +------------------- + +The tables below show which inputs each mode accepts, for each validation +scope. A cell containing Attached, Detached, or Unspecified means validation can +start using that type; it does not mean the crate passes validation. +**Rejected** means you must change the input or settings before validation can +start. Supported BagIt packages (a packaging format that wraps files and +metadata) follow the directory or ZIP rows, depending on how they are supplied. + +Full validation requested +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This table applies to CLI invocations without ``--metadata-only`` and calls to +``services.validate()`` with ``metadata_only=False``. + +.. list-table:: Mode used when metadata and data-file checks are requested + :header-rows: 1 + :class: package-mode-table + :widths: 28 24 24 24 + + * - Input + - ``auto`` + - ``attached`` + - ``detached`` + * - Local directory + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local or remote ZIP + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local ``ro-crate-metadata.json`` + - Detached + - Attached; parent directory becomes the package root + - Detached + * - Other local JSON/JSON-LD document + - Detached + - Rejected: supply the package directory/ZIP or its ``ro-crate-metadata.json`` + - Detached + * - Metadata document supplied through a URL + - Detached + - Rejected: supply a package directory/ZIP or use metadata-only validation + - Detached + * - In-memory ``metadata_dict`` (API) + - Unspecified; only metadata is checked + - Rejected: set ``metadata_only=True`` or supply a package directory/ZIP + - Detached; only metadata is checked + +A dictionary supplies metadata, not the package's data files. For ``auto`` and +``detached`` dictionary inputs, the API automatically sets ``metadata_only=True``. +For ``attached`` dictionary input, you must set it explicitly. + +For full Attached validation, supply the package directory or supported archive. +You can also select the local ``ro-crate-metadata.json`` file with +``--packaging-mode attached``; the validator then uses the directory containing +that file as the package root. + +Metadata-only validation requested +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This table applies with ``--metadata-only`` or ``metadata_only=True``. Every +accepted combination checks metadata without checking its data files. + +.. list-table:: Mode used when only metadata is checked + :header-rows: 1 + :class: package-mode-table + :widths: 28 24 24 24 + + * - Input + - ``auto`` + - ``attached`` + - ``detached`` + * - Local directory + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local or remote ZIP + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local ``ro-crate-metadata.json`` + - Detached + - Attached; data files need not be accessible + - Detached + * - Other local JSON/JSON-LD document + - Detached + - Attached; the input file can have any name + - Detached + * - Metadata document supplied through a URL + - Detached + - Attached; data files need not be accessible + - Detached + * - In-memory ``metadata_dict`` (API) + - Unspecified + - Attached + - Detached + +The name of the input file can differ from the metadata document's identifier +inside the JSON. Allowing any input filename does not relax the rules for that +identifier or for the rest of the metadata. + + +Examples +-------- + +Command line +~~~~~~~~~~~~ + +Validate an Attached package or a Detached document explicitly: + +.. code-block:: console + + rocrate-validator validate ./crate --packaging-mode attached + rocrate-validator validate ./dataset-ro-crate-metadata.json --packaging-mode detached + +Validate metadata from an Attached crate without exposing its payload: + +.. code-block:: console + + rocrate-validator validate ./metadata.json --packaging-mode attached --metadata-only + rocrate-validator validate https://example.org/ro-crate-metadata.json --packaging-mode attached --metadata-only + +Python API +~~~~~~~~~~ + +Select Attached rules when submitting metadata from an Attached crate: + +.. code-block:: python + + from rocrate_validator import services + + result = services.validate({ + "rocrate_uri": "https://example.org/ro-crate-metadata.json", + "profile_identifier": "ro-crate-1.3", + "packaging_mode": "attached", + "metadata_only": True, + }) + +For dictionary input, ``validate_metadata_as_dict()`` enables metadata-only +validation for you. Select the intended mode in its settings: + +.. code-block:: python + + import json + from rocrate_validator import services + + with open("metadata.json", encoding="utf-8") as source: + metadata = json.load(source) + + result = services.validate_metadata_as_dict(metadata, settings={ + "profile_identifier": "ro-crate-1.3", + "packaging_mode": "attached", + }) + +Omitting ``packaging_mode`` in this dictionary example leaves the mode +unspecified. By contrast, calling ``services.validate()`` directly with Attached +``metadata_dict`` input requires ``metadata_only=True`` explicitly. + +Reading the reports +------------------- + +The reported **RO-Crate package type** tells you which rules were selected: +Attached, Detached, or Unspecified. It does not certify the original package's +type. **Validation scope** tells you whether metadata-only or full validation +was requested. In Python, read the selected type through +``result.context.ro_crate.package_type``. + +JSON results include these fields under ``validation_settings``: + +.. list-table:: Fields describing the selected mode and scope + :header-rows: 1 + :class: package-mode-table + :widths: 35 65 + + * - Field + - Meaning + * - ``packaging_mode`` + - Requested mode: ``auto``, ``attached``, or ``detached``. + * - ``package_type`` + - Mode actually used: ``attached``, ``detached``, or ``unspecified``. + * - ``packaging_mode_explicit`` + - ``true`` if you selected ``attached`` or ``detached``; ``false`` for ``auto``. + * - ``metadata_only`` + - ``true`` if only metadata is checked, including when the API enables this + automatically for dictionary input. + +The result's ``skipped_check_details`` explains why checks were skipped. Read +those details alongside the issues and validation scope to understand the limits +of the result. From b6e1265c3d5418d17b415a062b9cbb0ab47c671d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 10:01:30 +0200 Subject: [PATCH 193/271] =?UTF-8?q?fix(models):=20=F0=9F=9B=A0=EF=B8=8F=20?= =?UTF-8?q?fall=20back=20to=20rocrate=5Furi=20when=20serializing=20setting?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index 918c3435c..75aae78fe 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -170,7 +170,9 @@ def to_dict(self): Convert the ValidationSettings to a dictionary """ result = asdict(self) - source_uri = getattr(self, "_source_rocrate_uri", self.rocrate_uri) + source_uri = getattr(self, "_source_rocrate_uri", None) + if source_uri is None: + source_uri = self.rocrate_uri result["rocrate_uri"] = str(source_uri) if source_uri is not None else None result.pop("metadata_dict", None) # exclude metadata_dict from the dict representation # Remove disable_crate_download from the dict representation From ce5e1f98cde6cf74e448a9e2d0c90ce437064e0a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 10:02:04 +0200 Subject: [PATCH 194/271] =?UTF-8?q?test(crate-type):=20=F0=9F=A7=AA=20use?= =?UTF-8?q?=20the=20canonical=20metadata=20filename=20for=20attached=20cra?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{prefix-ro-crate-metadata.json => ro-crate-metadata.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/data/crates/valid/ro-crate-1.3-absolute-root/{prefix-ro-crate-metadata.json => ro-crate-metadata.json} (100%) diff --git a/tests/data/crates/valid/ro-crate-1.3-absolute-root/prefix-ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.3-absolute-root/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/valid/ro-crate-1.3-absolute-root/prefix-ro-crate-metadata.json rename to tests/data/crates/valid/ro-crate-1.3-absolute-root/ro-crate-metadata.json From ed56162f52f0414928eef85f3e6a1a5fe4180b84 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 12:16:11 +0200 Subject: [PATCH 195/271] =?UTF-8?q?feat(models):=20=F0=9F=93=A6=20add=20pa?= =?UTF-8?q?ckage=20root=20and=20metadata=20source=20to=20validation=20sett?= =?UTF-8?q?ings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/result.py | 6 ++++++ rocrate_validator/models/settings.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 377be6741..49fe49405 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -497,12 +497,18 @@ def to_dict(self) -> dict: "abort_on_first", "metadata_only", "packaging_mode", + "package_root", ] validation_settings = { key: value for key, value in self.validation_settings.to_dict().items() if key in allowed_properties } validation_settings["package_type"] = self.context.ro_crate.package_type.value validation_settings["packaging_mode_explicit"] = self.context.ro_crate.packaging_mode_explicit + validation_settings["metadata_source"] = ( + "dictionary" if self.validation_settings.metadata_dict is not None else "package descriptor" + ) + if self.validation_settings.package_root is not None: + validation_settings["package_root"] = str(self.validation_settings.package_root) result: dict[str, Any] = { "meta": {"version": JSON_OUTPUT_FORMAT_VERSION}, "validation_settings": validation_settings, diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index 75aae78fe..41f4cea51 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -96,6 +96,8 @@ class ValidationSettings: packaging_mode: str = "auto" #: RO-Crate metadata as dictionary metadata_dict: dict | None = None + #: Local package directory used as the payload source for dictionary metadata + package_root: Path | None = None #: Verbose output verbose: bool = False #: Cache max age in seconds (negative values mean "never expire") @@ -115,6 +117,8 @@ class ValidationSettings: def __post_init__(self): self.packaging_mode = parse_packaging_mode(self.packaging_mode) + if self.package_root is not None and not isinstance(self.package_root, Path): + self.package_root = Path(self.package_root) # if requirement_severity is a str, convert to Severity if isinstance(self.requirement_severity, str): self.requirement_severity = Severity[self.requirement_severity] From 86a84ea536b50ef2f3e60b1aa4a07540b7152420 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 12:16:40 +0200 Subject: [PATCH 196/271] =?UTF-8?q?feat(validation):=20=F0=9F=93=A6=20supp?= =?UTF-8?q?ort=20dictionary=20metadata=20with=20a=20local=20package=20root?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/validation.py | 7 +++++- rocrate_validator/rocrate/base.py | 32 ++++++++++++++++++-------- rocrate_validator/services.py | 28 +++++++++++++++++----- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 1a51948ca..fd03d59d8 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -551,7 +551,12 @@ def __init__(self, validator: Validator, settings: ValidationSettings): # initialize the ROCrate object if settings.metadata_dict is not None: - self._rocrate = ROCrate.from_metadata_dict(settings.metadata_dict, packaging_mode=settings.packaging_mode) + self._rocrate = ROCrate.from_metadata_dict( + settings.metadata_dict, + packaging_mode=settings.packaging_mode, + package_root=settings.package_root, + metadata_only=settings.metadata_only, + ) else: rocrate_uri = settings.rocrate_uri assert rocrate_uri is not None, "RO-Crate URI is required when metadata_dict is not provided" diff --git a/rocrate_validator/rocrate/base.py b/rocrate_validator/rocrate/base.py index 88ea6e66a..eed7131e6 100644 --- a/rocrate_validator/rocrate/base.py +++ b/rocrate_validator/rocrate/base.py @@ -360,23 +360,37 @@ def get_external_file_size(uri: str) -> int | None: return int(content_length) @staticmethod - def from_metadata_dict(metadata_dict: dict, packaging_mode: str = "auto") -> ROCrate: + def from_metadata_dict( + metadata_dict: dict, + packaging_mode: str = "auto", + package_root: str | Path | URI | None = None, + metadata_only: bool = True, + ) -> ROCrate: """ - Create a new instance of the RO-Crate based on the metadata dictionary. + Create a crate from a metadata dictionary and an optional local package directory. :param metadata_dict: the metadata dictionary - :type metadata_dict: dict - - :raises ROCrateInvalidURIError: if the URI is invalid + :param packaging_mode: Explicit package context or an input-based default. + :param package_root: Local package directory that provides payload files. + :param metadata_only: Whether to skip checks that require package files. """ from .plain import ROCrateInMemory # noqa: PLC0415 mode = parse_packaging_mode(packaging_mode) - ro_crate = ROCrateInMemory("urn:rocrate-validator:in-memory/") - ro_crate._package_type = PackageType.UNSPECIFIED if mode == "auto" else PackageType(mode) - ro_crate._packaging_mode_explicit = mode != "auto" + if package_root is not None: + if mode == "detached": + raise ValueError("A package_root cannot be used with detached packaging_mode") + ro_crate = ROCrate.new_instance( + package_root, + packaging_mode=mode, + metadata_only=metadata_only, + ) + else: + ro_crate = ROCrateInMemory("urn:rocrate-validator:in-memory/") + ro_crate._package_type = PackageType.UNSPECIFIED if mode == "auto" else PackageType(mode) + ro_crate._packaging_mode_explicit = mode != "auto" - # override the metadata with the provided dictionary + # Use the supplied graph while retaining the package's payload backend. ro_crate._metadata = ROCrateMetadata(ro_crate, metadata_dict=metadata_dict) return ro_crate diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index bbf968044..7542d47d3 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -148,6 +148,24 @@ def _download_remote_rocrate( return _extract_and_validate(settings, subscribers, download_path) +def _initialise_dict_metadata_validator( + settings: ValidationSettings, subscribers: list[Subscriber] | None +) -> Validator: + """Build a validator for dictionary metadata, optionally using a package directory.""" + if settings.package_root is not None: + if settings.packaging_mode == "detached": + raise ValueError("package_root cannot be combined with detached packaging_mode") + if not settings.package_root.is_dir(): + raise ValueError(f"package_root is not a local directory: {settings.package_root}") + else: + if settings.packaging_mode == "attached" and not settings.metadata_only: + raise ValueError("Attached in-memory metadata requires metadata_only=True or package_root") + # A standalone dictionary has no payload backend, even when called + # through validate() rather than validate_metadata_as_dict(). + settings.metadata_only = True + return _build_validator(settings, subscribers) + + def __initialise_validator__( # noqa: C901, PLR0911 # pylint: disable=too-many-return-statements settings: dict | ValidationSettings, subscribers: list[Subscriber] | None = None ) -> Validator: @@ -157,13 +175,11 @@ def __initialise_validator__( # noqa: C901, PLR0911 # pylint: disable=too-many # if settings is a dict, convert to ValidationSettings settings = ValidationSettings.parse(settings) + if settings.package_root is not None and settings.metadata_dict is None: + raise ValueError("package_root requires metadata_dict") + if settings.metadata_dict is not None: - if settings.packaging_mode == "attached" and not settings.metadata_only: - raise ValueError("Attached in-memory metadata requires metadata_only=True or a package directory/ZIP") - # A dictionary has no payload backend, even when called through the - # general validate() entry point rather than validate_metadata_as_dict(). - settings.metadata_only = True - return _build_validator(settings, subscribers) + return _initialise_dict_metadata_validator(settings, subscribers) # parse the rocrate path assert settings.rocrate_uri is not None, "RO-Crate URI is required" From 0c9967c02a42af5a8df443634c064449fe7c75e7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 14:39:23 +0200 Subject: [PATCH 197/271] =?UTF-8?q?fix(profiles):=20=F0=9F=93=84=20use=20d?= =?UTF-8?q?ictionary=20input=20as=20the=20metadata=20descriptor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/1.1/must/0_file_descriptor_format.py | 9 +++++++-- .../ro-crate/1.2/must/0_file_descriptor_format.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index 0f683c082..b39a51207 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -43,6 +43,9 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ + # Dictionary input supplies the descriptor independently of payload storage. + if context.settings.metadata_dict is not None: + return True if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") context.record_skip(self, "metadata-only mode", "configured") @@ -58,11 +61,13 @@ def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ - if context.settings.metadata_only: + if context.settings.metadata_only and context.settings.metadata_dict is None: logger.debug("Skipping file descriptor existence check in metadata-only mode") context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED - if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: + if ( + context.settings.metadata_dict is not None or context.ro_crate.has_descriptor() + ) and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index f9af4028d..6272c8752 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -68,6 +68,9 @@ def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ + # Dictionary input supplies the descriptor independently of payload storage. + if context.settings.metadata_dict is not None: + return True if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") context.record_skip(self, "metadata-only mode", "configured") @@ -89,11 +92,13 @@ def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ - if context.settings.metadata_only: + if context.settings.metadata_only and context.settings.metadata_dict is None: logger.debug("Skipping file descriptor existence check in metadata-only mode") context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED - if context.ro_crate.has_descriptor() and context.ro_crate.metadata.size == 0: + if ( + context.settings.metadata_dict is not None or context.ro_crate.has_descriptor() + ) and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -110,7 +115,7 @@ def check(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is UTF-8 encoded """ - if context.settings.metadata_only: + if context.settings.metadata_only and context.settings.metadata_dict is None: logger.debug("Skipping file descriptor encoding check in metadata-only mode") context.record_skip(self, "metadata-only mode", "configured") return CheckResult.SKIPPED From 545e744825b4106b58aa549c6c8331c74630ae46 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 14:39:54 +0200 Subject: [PATCH 198/271] =?UTF-8?q?fix(cli):=20=F0=9F=93=8D=20show=20the?= =?UTF-8?q?=20package=20root=20in=20the=20validation=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/io_helpers/output/text/layout/report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/report.py b/rocrate_validator/utils/io_helpers/output/text/layout/report.py index 3c70b1d5c..9b3ed48dd 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/report.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/report.py @@ -207,9 +207,10 @@ def _base_info(self) -> Align: settings.packaging_mode if settings.packaging_mode != "auto" else "resolving..." ) validation_scope = "Metadata Only" if settings.metadata_only else "Full Package" + crate_source = settings.package_root or getattr(settings, "_source_rocrate_uri", None) or settings.rocrate_uri return Align( f"\n[bold cyan]RO-Crate:[/bold cyan] " - f"[bold]{getattr(settings, '_source_rocrate_uri', settings.rocrate_uri) or 'in-memory metadata'}[/bold]" + f"[bold]{crate_source or 'in-memory metadata'}[/bold]" f"\n[bold cyan]RO-Crate Package Type:[/bold cyan] [bold yellow]{package_type.title()}[/bold yellow]" "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " f"{settings.profile_identifier}[/bold magenta] " From 51eda48b1b1a3c58ca92f36fff910583db9ab4ee Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 14:42:34 +0200 Subject: [PATCH 199/271] =?UTF-8?q?test(cli):=20=F0=9F=A7=AA=20cover=20cra?= =?UTF-8?q?te=20source=20labels=20in=20validation=20reports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_package_mode.py | 36 +++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_package_mode.py b/tests/unit/test_package_mode.py index 8fa0b5669..4ec3b679e 100644 --- a/tests/unit/test_package_mode.py +++ b/tests/unit/test_package_mode.py @@ -260,14 +260,46 @@ def test_reports_show_package_type_after_target_profile(mode: str, expected: str assert "validation scope:" not in text_report +@pytest.mark.parametrize("metadata_only", [True, False]) +def test_interactive_report_shows_package_root_for_dictionary_input(tmp_path: Path, metadata_only: bool): + settings = ValidationSettings( + rocrate_uri=None, + metadata_dict=_minimal_metadata(), + package_root=tmp_path, + packaging_mode="attached", + metadata_only=metadata_only, + offline=True, + ) + console = Console(record=True, width=300) + layout = ValidationReportLayout(console, settings) + _ = layout.layout + assert layout.base_info_layout is not None + console.print(layout.base_info_layout.renderable) + report = console.export_text() + assert f"RO-Crate: {tmp_path}" in report + assert "in-memory metadata" not in report + + +def test_interactive_report_shows_in_memory_source_for_dictionary_only(): + settings = ValidationSettings(rocrate_uri=None, metadata_dict=_minimal_metadata(), metadata_only=True, offline=True) + console = Console(record=True, width=120) + layout = ValidationReportLayout(console, settings) + _ = layout.layout + assert layout.base_info_layout is not None + console.print(layout.base_info_layout.renderable) + assert "RO-Crate: in-memory metadata" in console.export_text() + + def test_interactive_report_shows_full_package_scope(tmp_path: Path): settings = ValidationSettings(rocrate_uri=tmp_path, metadata_only=False, offline=True) - console = Console(record=True, width=120) + console = Console(record=True, width=300) layout = ValidationReportLayout(console, settings) _ = layout.layout assert layout.base_info_layout is not None console.print(layout.base_info_layout.renderable) - assert "Validation Scope: Full Package" in console.export_text() + report = console.export_text() + assert "Validation Scope: Full Package" in report + assert f"RO-Crate: {tmp_path}" in report def test_attached_directory_missing_payload_stays_attached(tmp_path: Path): From f9f779bd725cca3d47c17db8793ab6836cca684a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 14:44:37 +0200 Subject: [PATCH 200/271] =?UTF-8?q?build(docs):=20=F0=9F=93=93=20provide?= =?UTF-8?q?=20Pandoc=20for=20notebook=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 7 +++++++ docs/requirements.txt | 1 + poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f7a3bb85f..5a15af99f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,8 +28,15 @@ import sys from pathlib import Path +from pypandoc import get_pandoc_path + from rocrate_validator import __version__ +# nbconvert invokes Pandoc by name when rendering notebook Markdown. +# Make the executable bundled with the docs dependencies available on PATH. +pandoc_bin_dir = str(Path(get_pandoc_path()).resolve().parent) +os.environ["PATH"] = os.pathsep.join((os.environ.get("PATH", ""), pandoc_bin_dir)) + # update PYTHONPATH sys.path.insert(0, str(Path.cwd())) sys.path.insert(0, str(Path("..").resolve())) diff --git a/docs/requirements.txt b/docs/requirements.txt index d9838fe79..b46a0c9ac 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,7 @@ # Required packages for building the documentation with Sphinx sphinx==8.1.3 nbsphinx==0.9.5 +pypandoc-binary==1.17 enum-tools==0.12.0 sphinx-toolbox==3.8.1 myst-parser==4.0.0 diff --git a/poetry.lock b/poetry.lock index 85f1cf755..2bff7eb26 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2531,6 +2531,23 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pypandoc-binary" +version = "1.17" +description = "Thin wrapper for pandoc." +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "pypandoc_binary-1.17-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:734726dc618ef276343e272e1a6b4567e59c2ef9ef41d5533042deac3b0531f1"}, + {file = "pypandoc_binary-1.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fcfd28f347ed998dda28823fc6bc24f9310e7fdf3ddceaf925bf0563a100ab5b"}, + {file = "pypandoc_binary-1.17-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b620b21c9374e3e48aabd518492bf0776b148442ee28816f6aaf52da3d4387"}, + {file = "pypandoc_binary-1.17-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ada156cb980cd54fd6534231788e668c00dbb591cbd24f0be0bd86812eb8788"}, + {file = "pypandoc_binary-1.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2f439dcd211183bb3460253ca4511101df6e1acf4a01f45f5617e1fa2ad24279"}, + {file = "pypandoc_binary-1.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6e6d3e4cfafbe23189a08db3d41f8def260bacd6e7e382bceadab7ba1f17da6"}, + {file = "pypandoc_binary-1.17-py3-none-win_amd64.whl", hash = "sha256:76fae066cd2d7e78fb97f0ec8e9e36f437b07187b689b0b415ca18216f8f898a"}, +] + [[package]] name = "pyparsing" version = "3.3.3" @@ -4437,4 +4454,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "125fe78f66565d99ddbeca5593513fc401b989b4a743fea564d0cf860e19fd08" +content-hash = "9d1199e9fdd35ffb8c9be707c6d0e59393fa99a9f037ecfd3addb5229cebd882" diff --git a/pyproject.toml b/pyproject.toml index a44f340ec..7408374e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ pytest-xdist = "^3.6.1" [tool.poetry.group.docs.dependencies] sphinx = "^8.1.3" nbsphinx = "^0.9.5" +pypandoc-binary = "^1.17" sphinx-toolbox = "^3.8.1" myst-parser = "^4.0.0" shibuya = "==2026.7.12" From 03c649d4f811e8d4a1b0e7e9686071f4f9c42352 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 14:45:53 +0200 Subject: [PATCH 201/271] =?UTF-8?q?docs(validation):=20=F0=9F=93=93=20docu?= =?UTF-8?q?ment=20dictionary=20input=20with=20a=20package=20root?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/0_toc.rst | 1 + docs/3_usage_api.rst | 28 ++ docs/6_package_modes.rst | 92 ++++-- docs/examples/index.rst | 33 +++ ...validate_metadata_dict_package_modes.ipynb | 261 ++++++++++++++++++ 5 files changed, 396 insertions(+), 19 deletions(-) create mode 100644 docs/examples/index.rst create mode 100644 docs/examples/validate_metadata_dict_package_modes.ipynb diff --git a/docs/0_toc.rst b/docs/0_toc.rst index 52d470bd7..1c2da5dc7 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -30,6 +30,7 @@ 12_validation_profiles 5_offline_mode 10_api + examples/index .. toctree:: :maxdepth: 5 diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index ea65cb895..67288b78c 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -78,6 +78,34 @@ representing the metadata and validates it against a given validation profile. # process the validation result as needed ... +To check an edited metadata dictionary against payload files in an Attached +crate directory, pass ``package_root`` to ``services.validate()``: + +.. code-block:: python + + from pathlib import Path + from rocrate_validator import services + from rocrate_validator.models.settings import ValidationSettings + + settings = ValidationSettings( + rocrate_uri=None, + metadata_dict=rocrate_metadata, + package_root=Path("./my-crate"), + profile_identifier="ro-crate-1.3", + packaging_mode="attached", + metadata_only=False, + ) + result = services.validate(settings) + +The dictionary supplies the metadata being validated, replacing the physical +metadata descriptor. The directory supplies only the payload files: +``ro-crate-metadata.json`` is not required on disk and is ignored if present. +The report records +``metadata_source="dictionary"`` and the selected ``package_root``. + +For a runnable notebook comparing package modes, validation scopes, and report +output with dictionary input, see :ref:`validation_examples`. + Warm validation with a reusable Validator ----------------------------------------- diff --git a/docs/6_package_modes.rst b/docs/6_package_modes.rst index 33626eec2..f17beb585 100644 --- a/docs/6_package_modes.rst +++ b/docs/6_package_modes.rst @@ -29,7 +29,13 @@ data files, use ``--packaging-mode attached --metadata-only``. In this guide, a **metadata document** means JSON/JSON-LD supplied as a local file or through a URL. A **dictionary input** means metadata already loaded -into a Python dictionary and supplied through the API. +into a Python dictionary and supplied through the API. For a dictionary input, +``package_root`` optionally names the local package directory that provides its +payload files. + +To try these options with your own metadata dictionary, see the +:ref:`example notebook `. It compares all three modes +and demonstrates both validation scopes with a local package directory. Modes and automatic assumptions ------------------------------- @@ -53,8 +59,9 @@ The CLI option ``--packaging-mode`` and API setting - Behaviour * - ``auto`` (default) - * **Use when** → the input-based assumptions below suit your case. - * **Effect** → select Attached or Detached rules from the input format; - leave the type *Unspecified* for a Python dictionary. + * **Effect** → select rules from the input context; a dictionary without + ``package_root`` is *Unspecified*, while a dictionary with it is + *Attached*. * - ``attached`` - * **Use when** → you are validating an Attached crate, including a copy of its metadata supplied without the data files. @@ -69,12 +76,16 @@ With ``auto``, the validator makes these assumptions: * **Directory or ZIP** → *Attached*. * **Metadata document**, supplied as a local file or URL → *Detached*. -* **Python dictionary** → *Unspecified*. Common metadata checks run, while - checks requiring Attached or Detached rules are recorded as skipped. +* **Python dictionary without a package directory** → *Unspecified*. Common + metadata checks run, while checks requiring Attached or Detached rules are + recorded as skipped. +* **Python dictionary with ``package_root``** → *Attached*. The dictionary + supplies the metadata graph and the local directory supplies the payload. Here, *Unspecified* means that no package type was selected. It is a reported -value, not a fourth option for ``--packaging-mode``. Select ``attached`` or -``detached`` explicitly to enable the corresponding checks for dictionary input. +value, not a fourth option for ``--packaging-mode``. For a dictionary without +``package_root``, select ``attached`` or ``detached`` explicitly to enable those +mode-specific checks. .. note:: @@ -146,14 +157,26 @@ This table applies to CLI invocations without ``--metadata-only`` and calls to - Detached - Rejected: supply a package directory/ZIP or use metadata-only validation - Detached - * - In-memory ``metadata_dict`` (API) + * - ``metadata_dict`` without ``package_root`` (API) - Unspecified; only metadata is checked - - Rejected: set ``metadata_only=True`` or supply a package directory/ZIP + - Rejected for full validation; set ``metadata_only=True`` - Detached; only metadata is checked + * - ``metadata_dict`` with local ``package_root`` (API) + - Attached + - Attached + - Rejected: a package directory cannot be Detached -A dictionary supplies metadata, not the package's data files. For ``auto`` and -``detached`` dictionary inputs, the API automatically sets ``metadata_only=True``. -For ``attached`` dictionary input, you must set it explicitly. +Without ``package_root``, a dictionary has no payload backend, so ``auto`` and +``detached`` inputs are automatically limited to metadata-only validation. For +full Attached validation, set ``package_root`` to a local directory containing +the payload files. The dictionary replaces the metadata descriptor: a physical +``ro-crate-metadata.json`` is not required and, if present, is not read. +Metadata checks use the supplied dictionary, including in metadata-only mode. +The report identifies the dictionary as the metadata source and includes the +package root. + +When ``metadata_only=True``, ``package_root`` still selects Attached rules, but +checks that read payload files are skipped. For full Attached validation, supply the package directory or supported archive. You can also select the local ``ro-crate-metadata.json`` file with @@ -195,10 +218,14 @@ accepted combination checks metadata without checking its data files. - Detached - Attached; data files need not be accessible - Detached - * - In-memory ``metadata_dict`` (API) + * - ``metadata_dict`` without ``package_root`` (API) - Unspecified - Attached - Detached + * - ``metadata_dict`` with local ``package_root`` (API) + - Attached + - Attached + - Rejected: a package directory cannot be Detached The name of the input file can differ from the metadata document's identifier inside the JSON. Allowing any input filename does not relax the rules for that @@ -241,8 +268,30 @@ Select Attached rules when submitting metadata from an Attached crate: "metadata_only": True, }) -For dictionary input, ``validate_metadata_as_dict()`` enables metadata-only -validation for you. Select the intended mode in its settings: +For dictionary-only input, ``validate_metadata_as_dict()`` enables +metadata-only validation for you. To validate a modified dictionary against a +local Attached package's payload, use ``services.validate()`` with +``metadata_dict``, ``package_root``, and ``metadata_only=False``: + +.. code-block:: python + + from pathlib import Path + from rocrate_validator import services + from rocrate_validator.models.settings import ValidationSettings + + settings = ValidationSettings( + rocrate_uri=None, + metadata_dict=metadata, + package_root=Path("./my-crate"), + profile_identifier="ro-crate-1.3", + packaging_mode="attached", + metadata_only=False, + ) + result = services.validate(settings) + +The directory supplies payload files; ``metadata_dict`` supplies the metadata +being validated. No metadata descriptor is required on disk. If the directory +contains one, it is ignored for this validation. .. code-block:: python @@ -257,9 +306,9 @@ validation for you. Select the intended mode in its settings: "packaging_mode": "attached", }) -Omitting ``packaging_mode`` in this dictionary example leaves the mode -unspecified. By contrast, calling ``services.validate()`` directly with Attached -``metadata_dict`` input requires ``metadata_only=True`` explicitly. +Omitting ``packaging_mode`` for a dictionary without ``package_root`` leaves +the mode unspecified. Calling ``services.validate()`` with Attached dictionary +input and no ``package_root`` requires ``metadata_only=True`` explicitly. Reading the reports ------------------- @@ -287,7 +336,12 @@ JSON results include these fields under ``validation_settings``: - ``true`` if you selected ``attached`` or ``detached``; ``false`` for ``auto``. * - ``metadata_only`` - ``true`` if only metadata is checked, including when the API enables this - automatically for dictionary input. + automatically for dictionary-only input. + * - ``metadata_source`` + - ``dictionary`` when checks use the supplied metadata dictionary; otherwise + ``package descriptor``. + * - ``package_root`` + - Local package directory used to access payload files for dictionary input. The result's ``skipped_check_details`` explains why checks were skipped. Read those details alongside the issues and validation scope to understand the limits diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 000000000..6521b6a75 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,33 @@ +.. _validation_examples: + +Validation examples +=================== + +Validate a metadata dictionary +------------------------------ + +The notebook below lets you compare ``auto``, ``attached``, and ``detached`` +using the same metadata dictionary. It also demonstrates metadata-only and +full validation with a local package directory, and displays the report header +and JSON summary for each run. + +Read it online or :download:`download the notebook +` to run it locally. + +To run the examples: + +1. Open the notebook in a notebook editor, such as VS Code or Jupyter, with + the repository's ``.venv`` Python environment selected as the kernel. +2. Set ``INPUT_PATH`` to your metadata JSON file. For validation with payload + files, set ``ATTACHED_CRATE_DIR`` to your local crate directory. +3. Choose the target profile and ``OFFLINE`` setting, then run the cells in order. + External resources may be fetched when ``OFFLINE=False``. + +The documentation build does not execute the notebook. See +:doc:`../3_usage_api` for API usage and :ref:`package_modes` for the assumptions +and accepted input combinations. + +.. toctree:: + :maxdepth: 1 + + validate_metadata_dict_package_modes diff --git a/docs/examples/validate_metadata_dict_package_modes.ipynb b/docs/examples/validate_metadata_dict_package_modes.ipynb new file mode 100644 index 000000000..7b8cf203b --- /dev/null +++ b/docs/examples/validate_metadata_dict_package_modes.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "df4ae258", + "metadata": {}, + "source": [ + "# Try RO-Crate metadata validation with a Python dictionary\n", + "\n", + "Set `INPUT_PATH` in the first setup cell to the JSON file you want to inspect. The notebook loads it into `INPUT` and validates that same dictionary with `packaging_mode` set to `auto`, `attached`, and `detached`.\n", + "\n", + "The first setup cell also contains the profile, offline-mode, and optional Attached crate directory parameters. Run this notebook from the repository checkout with its `.venv` kernel selected. Dictionary-only input has no payload backend, so validation is metadata-only. Full Attached validation is available when you also set `ATTACHED_CRATE_DIR` as the `package_root`.\n", + "\n", + "This notebook is also published in the documentation. Download it to run the examples locally; building the documentation does not execute validations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0cd4479", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "from rocrate_validator.models.settings import ValidationSettings\n", + "from rocrate_validator.services import validate\n", + "from rocrate_validator.utils.io_helpers.output.console import Console\n", + "from rocrate_validator.utils.io_helpers.output.text.layout.report import ValidationReportLayout\n", + "\n", + "# Parameters: edit these values to try another metadata file or profile.\n", + "INPUT_PATH = Path(\"/tmp/mymetadata.json\")\n", + "PROFILE_IDENTIFIER = \"ro-crate-1.3\"\n", + "OFFLINE = False\n", + "ATTACHED_CRATE_DIR = Path(\"/tmp/profile-crate-process-run-crate/\") # Set this to a local crate directory\n", + "\n", + "with INPUT_PATH.open(encoding=\"utf-8\") as stream:\n", + " INPUT = json.load(stream)\n", + "\n", + "if not isinstance(INPUT, dict):\n", + " raise TypeError(f\"Expected a JSON object, got {type(INPUT).__name__}\")\n", + "\n", + "print(f\"Loaded {INPUT_PATH}: {len(INPUT.get('@graph', []))} graph entities\")\n", + "print(\"Top-level keys:\", list(INPUT))\n" + ] + }, + { + "cell_type": "markdown", + "id": "c48fa27f", + "metadata": {}, + "source": [ + "## Run one validation\n", + "\n", + "Use the parameters in the first setup cell to change the input file, target profile, or offline behavior. `OFFLINE=False` allows uncached JSON-LD contexts or external resources to be fetched; set it to `True` to use the validator cache only.\n", + "\n", + "Each run displays the same report header used by the CLI, followed by a JSON summary. With `package_root`, the `RO-Crate` row shows the supplied directory; dictionary-only input shows `in-memory metadata`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7210d90", + "metadata": {}, + "outputs": [], + "source": [ + "def run_validation(package_mode=\"auto\", metadata_only=True, package_root=None):\n", + " settings = ValidationSettings(\n", + " rocrate_uri=None, # Not needed for dictionary input\n", + " metadata_dict=INPUT,\n", + " package_root=package_root,\n", + " profile_identifier=PROFILE_IDENTIFIER,\n", + " packaging_mode=package_mode,\n", + " metadata_only=metadata_only,\n", + " offline=OFFLINE,\n", + " )\n", + " result = validate(settings)\n", + " report = result.to_dict()\n", + " report_settings = report[\"validation_settings\"]\n", + " summary = {\n", + " \"requested_mode\": package_mode,\n", + " \"resolved_package_type\": report_settings[\"package_type\"],\n", + " \"metadata_source\": report_settings[\"metadata_source\"],\n", + " \"metadata_only\": report_settings[\"metadata_only\"],\n", + " \"package_root\": report_settings.get(\"package_root\"),\n", + " \"passed\": report[\"passed\"],\n", + " \"issues\": len(report[\"issues\"]),\n", + " \"skipped_checks\": report[\"skipped_checks\"],\n", + " }\n", + " console = Console(force_jupyter=True)\n", + " layout = ValidationReportLayout(console, result.validation_settings)\n", + " layout.result = result\n", + " _ = layout.layout\n", + " if layout.base_info_layout is not None:\n", + " console.print(layout.base_info_layout.renderable)\n", + " print(json.dumps(summary, indent=2))\n", + " return result, report\n", + "\n", + "result, report = run_validation(package_mode=\"auto\", metadata_only=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "0b48fb9b", + "metadata": {}, + "source": [ + "## Compare package modes\n", + "\n", + "Each run uses the same `INPUT` dictionary. `auto` resolves dictionary input to `UNSPECIFIED`; `attached` and `detached` select their respective profile rules. The report includes the resolved package type, validation scope, issues, and skipped checks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28c7884f", + "metadata": {}, + "outputs": [], + "source": [ + "mode_results = {}\n", + "for mode in (\"auto\", \"attached\", \"detached\"):\n", + " print(f\"\\n=== packaging_mode={mode} ===\")\n", + " mode_results[mode] = run_validation(package_mode=mode, metadata_only=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e20d3f53", + "metadata": {}, + "source": [ + "## Explore the `metadata_only` option\n", + "\n", + "A dictionary has no associated payload files. For `auto` and `detached`, the validator therefore makes the dictionary validation metadata-only even if `metadata_only=False` was requested. For explicit `attached`, `metadata_only=False` is rejected: provide `package_root` pointing to a local crate directory to perform full Attached validation with the dictionary.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42278547", + "metadata": {}, + "outputs": [], + "source": [ + "scope_results = {}\n", + "for mode in (\"auto\", \"detached\"):\n", + " print(f\"\\n=== packaging_mode={mode}, metadata_only=False ===\")\n", + " scope_results[(mode, False)] = run_validation(package_mode=mode, metadata_only=False)\n", + "\n", + "try:\n", + " run_validation(package_mode=\"attached\", metadata_only=False)\n", + "except ValueError as exc:\n", + " print(\"Expected configuration error for Attached dictionary input:\")\n", + " print(exc)\n" + ] + }, + { + "cell_type": "markdown", + "id": "216d377b", + "metadata": {}, + "source": [ + "## Dictionary plus attached directory with full validation\n", + "\n", + "This combination is supported: the dictionary supplies the metadata graph and `ATTACHED_CRATE_DIR` supplies the payload files. The dictionary replaces the metadata descriptor: `ro-crate-metadata.json` is not required on disk and is ignored if present. Metadata checks use `INPUT` in both validation scopes. The report records `metadata_source=\"dictionary\"` and the package root.\n", + "\n", + "Set `ATTACHED_CRATE_DIR` in the first setup cell to an existing local RO-Crate directory before running this section. `auto` resolves this input to Attached; the cell runs both `auto` and explicit `attached`, each with metadata-only and full validation. In every case, the `RO-Crate` row should show `ATTACHED_CRATE_DIR`. Results are saved in `package_results`, indexed by `(mode, metadata_only)`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "742f3d55", + "metadata": {}, + "outputs": [], + "source": [ + "package_results = {}\n", + "if not ATTACHED_CRATE_DIR.is_dir():\n", + " print(f\"Set ATTACHED_CRATE_DIR to an existing crate directory first: {ATTACHED_CRATE_DIR}\")\n", + "else:\n", + " for mode in (\"auto\", \"attached\"):\n", + " for metadata_only in (True, False):\n", + " print(f\"\\n=== dictionary + package_root, packaging_mode={mode}, metadata_only={metadata_only} ===\")\n", + " package_results[(mode, metadata_only)] = run_validation(\n", + " package_mode=mode,\n", + " metadata_only=metadata_only,\n", + " package_root=ATTACHED_CRATE_DIR,\n", + " )\n", + "\n", + " print(\"\\nDetached mode with package_root is an invalid combination:\")\n", + " try:\n", + " run_validation(\n", + " package_mode=\"detached\",\n", + " metadata_only=False,\n", + " package_root=ATTACHED_CRATE_DIR,\n", + " )\n", + " except ValueError as exc:\n", + " print(exc)\n" + ] + }, + { + "cell_type": "markdown", + "id": "846ede7b", + "metadata": {}, + "source": [ + "## Inspect findings\n", + "\n", + "Print issue summaries for one mode. Select `auto`, `attached`, or `detached` below.\n", + "\n", + "Set `INSPECT_PACKAGE_ROOT=True` to inspect a dictionary-plus-directory result from the preceding section; `SCOPE_TO_INSPECT` selects metadata-only (`True`) or full validation (`False`).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a890ec63", + "metadata": {}, + "outputs": [], + "source": [ + "MODE_TO_INSPECT = \"attached\"\n", + "INSPECT_PACKAGE_ROOT = False\n", + "SCOPE_TO_INSPECT = False\n", + "if INSPECT_PACKAGE_ROOT:\n", + " key = (MODE_TO_INSPECT, SCOPE_TO_INSPECT)\n", + " if key not in package_results:\n", + " raise ValueError(\"Run the dictionary-plus-directory section first; select auto or attached.\")\n", + " selected_result, selected_report = package_results[key]\n", + "else:\n", + " selected_result, selected_report = mode_results[MODE_TO_INSPECT]\n", + "print(f\"\\n=== inspecting mode: {MODE_TO_INSPECT} ===\\n\")\n", + "for issue in selected_report[\"issues\"]:\n", + " check = issue.get(\"check\", {})\n", + " print(f\"[{issue['severity']}] {check.get('identifier', '(unknown check)')}: {issue['message']}\")\n", + "\n", + "print(f\"\\nSkipped checks: {selected_report['skipped_checks']}\")\n", + "for skipped in selected_report.get(\"skipped_check_details\", []):\n", + " print(skipped)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From c0212aaa79c91b720d2376752f2d466dfadb9504 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 14:58:08 +0200 Subject: [PATCH 202/271] =?UTF-8?q?feat(docs):=20=F0=9F=8E=A8=20adopt=20Sh?= =?UTF-8?q?ibuya=20with=20custom=20colors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_static/custom.css | 244 ++++++++++++++++++++++++++++++++++++++-- docs/conf.py | 19 +++- docs/requirements.txt | 2 +- poetry.lock | 68 ++++++----- pyproject.toml | 2 +- 5 files changed, 281 insertions(+), 54 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index ab81ff48f..bc6458b84 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,24 +1,248 @@ -/* Keep the composition summary within the documentation content column. */ -.rst-content .profile-composition-table { - width: 100%; +/* Teal palette with darker text accents for contrast in light mode. */ +html.light[data-accent-color="teal"] { + --accent-2: #f2f9f9; + --accent-3: #e5f3f3; + --accent-4: #d3ebec; + --accent-8: #39989f; + --accent-9: #175a5f; + --accent-10: #094b4b; + --accent-11: #175a5f; + --accent-12: #003333; + --accent-a2: rgba(57, 152, 159, 0.06); + --accent-a3: rgba(57, 152, 159, 0.13); + --accent-a4: rgba(57, 152, 159, 0.2); + --accent-a9: #175a5f; + --accent-a11: #175a5f; + --accent-contrast: #fff; + --color-surface-accent: #c2e5e6; + --sy-c-heading: #133233; + --sy-c-bold: #094b4b; + --sy-c-link: #175a5f; + --sy-c-link-hover: #094b4b; + --sy-c-foot-background: #edf6f6; + --yue-c-link-1: #175a5f; + --yue-c-link-2: #094b4b; +} + +html.dark[data-accent-color="teal"] { + --accent-2: #0d292c; + --accent-3: #12383c; + --accent-4: #164247; + --accent-8: #39989f; + --accent-9: #81d8d5; + --accent-10: #a3e4e2; + --accent-11: #81d8d5; + --accent-12: #d5f3f2; + --accent-a2: rgba(129, 216, 213, 0.06); + --accent-a3: rgba(129, 216, 213, 0.13); + --accent-a4: rgba(129, 216, 213, 0.2); + --accent-a9: #81d8d5; + --accent-a11: #81d8d5; + --accent-contrast: #003333; + --color-surface-accent: #164247; + --sy-c-background: #071f23; + --sy-c-background-dropback: rgba(7, 31, 35, 0.96); + --sy-c-surface: #123035; + --sy-c-text: #e0efef; + --sy-c-light: #a8c2c4; + --sy-c-heading: #c2e5e6; + --sy-c-bold: #edfafa; + --sy-c-border: #285052; + --sy-c-divider: #234549; + --sy-c-link: #81d8d5; + --sy-c-link-hover: #a3e4e2; + --sy-c-foot-background: #003333; + --yue-c-link-1: #81d8d5; + --yue-c-link-2: #a3e4e2; +} + +/* Callouts use teal for information, orange/yellow for warnings and attention, + * brown for important details, and red for errors. Override each semantic type: + * the theme's callout backgrounds otherwise remain too light in dark mode. + */ +.admonition, +.admonition:is(.note, .tip, .hint, .seealso) { + --color-1: var(--accent-2); + --color-2: var(--accent-3); + --color-3: var(--accent-9); + --color-4: var(--accent-11); + color: var(--sy-c-text); + --yue-c-link-1: var(--color-4); + --yue-c-link-2: var(--color-4); + --yue-c-link-border: var(--color-3); + --yue-c-code-text: var(--color-4); + --yue-c-code-background: var(--color-2); +} + +/* Keep the icon badges above the title strip, which extends into their area. */ +.admonition::before { + z-index: 2; +} + +.admonition::after { + z-index: 3; +} + +.admonition > .admonition-title { + z-index: 1; + color: var(--color-4); + background-color: var(--color-2); +} + +.admonition:is(.warning, .caution, .admonition-todo) { + --color-1: #fff7ed; + --color-2: #ffedd5; + --color-3: #b45309; + --color-4: #7c2d12; +} + +.admonition.attention { + --color-1: #fffbeb; + --color-2: #fef3c7; + --color-3: #a16207; + --color-4: #713f12; +} + +.admonition.important { + --color-1: #faf4ef; + --color-2: #efdfcf; + --color-3: #8b5e3c; + --color-4: #65432a; +} + +.admonition:is(.danger, .error) { + --color-1: #fff1f2; + --color-2: #ffe4e6; + --color-3: #be123c; + --color-4: #881337; +} + +html.dark .admonition, +html.dark .admonition:is(.note, .tip, .hint, .seealso) { + --color-1: #123035; + --color-2: #164247; + --color-3: #39989f; + --color-4: #81d8d5; + color: #e0efef; +} + +html.dark .admonition:is(.warning, .caution, .admonition-todo) { + --color-1: #382817; + --color-2: #51371d; + --color-3: #fb923c; + --color-4: #fed7aa; + color: #e0efef; +} + +html.dark .admonition.attention { + --color-1: #332c14; + --color-2: #49401c; + --color-3: #fbbf24; + --color-4: #fde68a; + color: #e0efef; +} + +html.dark .admonition.important { + --color-1: #30251f; + --color-2: #47352a; + --color-3: #c89b72; + --color-4: #eed0b5; + color: #e0efef; } -.rst-content .profile-composition-table .wy-table-responsive { - overflow-x: visible; +html.dark .admonition:is(.danger, .error) { + --color-1: #381e28; + --color-2: #512635; + --color-3: #fb7185; + --color-4: #fecdd3; + color: #e0efef; } -.rst-content .profile-composition-table table.profile-composition-mechanisms { +html.dark .admonition::after { + background-color: #071f23; +} + +html.dark .admonition > .admonition-title { + color: var(--color-4); + background-color: var(--color-2); +} + +/* Deep petrol header in both colour modes. Local variables also + * keep the search field, navigation icons, and mobile menu readable. + */ +.sy-head { + color: #f5f5f5; + --sy-c-text: #f5f5f5; + --sy-c-light: #c2e5e6; + --sy-c-bold: #fff; + --sy-c-background: #003333; + --sy-c-background-dropback: rgba(0, 51, 51, 0.97); + --sy-c-surface: #0b4b4b; + --sy-c-border: #39989f; + --sy-c-divider: #21605f; + --sy-c-link: #81d8d5; + --sy-c-link-hover: #c2e5e6; +} + +.sy-head .searchbox input::placeholder { + color: #c2e5e6; + opacity: 1; +} + +.sy-head .searchbox kbd { + color: #c2e5e6; + background-color: #0b4b4b; + border-color: #39989f; +} + +/* Keep the composition summary within the documentation content column. */ +.profile-composition-table { + width: 100%; +} + +.profile-composition-table table.profile-composition-mechanisms { table-layout: fixed; width: 100%; } -.rst-content .profile-composition-table th, -.rst-content .profile-composition-table td { +.profile-composition-table th, +.profile-composition-table td { overflow-wrap: break-word; white-space: normal; } -.rst-content .profile-composition-table th:first-child, -.rst-content .profile-composition-table td:first-child { +.profile-composition-table th:first-child, +.profile-composition-table td:first-child { text-align: center; } + +/* Fit package-mode tables to the content column, including long API names. */ +table.docutils.package-mode-table { + table-layout: fixed; + width: 100%; + min-width: 0; +} + +table.docutils.package-mode-table th, +table.docutils.package-mode-table td, +table.docutils.package-mode-table caption { + white-space: normal; + overflow-wrap: anywhere; +} + +table.docutils.package-mode-table code { + white-space: normal; + overflow-wrap: anywhere; +} + +table.docutils.package-mode-table th, +table.docutils.package-mode-table td { + vertical-align: top; +} + +@media (max-width: 600px) { + table.docutils.package-mode-table th, + table.docutils.package-mode-table td { + padding: 0.4rem; + } +} diff --git a/docs/conf.py b/docs/conf.py index d04c02732..21ed46164 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,7 +114,8 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "shibuya" +html_title = "RO-Crate Validator" html_static_path = ["_static"] html_css_files = ["custom.css"] @@ -134,10 +135,16 @@ branch = os.popen("git rev-parse --abbrev-ref HEAD").read().strip() # -- Options for HTML output ------------------------------------------------- +html_theme_options = { + "accent_color": "teal", + "nav_socials": [ + {"name": "GitHub", "url": github_url, "icon": "simple-icons:github"}, + ], +} html_context = { - "display_github": True, # Integrate GitHub - "github_user": "crs4", # Username - "github_repo": "rocrate-validator", # Repo name - "github_version": branch, # Version - "conf_py_path": "/docs/", # Path in the checkout to the docs root + "source_type": "github", + "source_user": "crs4", + "source_repo": "rocrate-validator", + "source_version": branch, + "source_docs_path": "/docs/", } diff --git a/docs/requirements.txt b/docs/requirements.txt index 8d92b0be1..47ac130fb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,5 +4,5 @@ nbsphinx==0.9.5 enum-tools==0.12.0 sphinx-toolbox==3.8.1 myst-parser==4.0.0 -sphinx_rtd_theme==3 +shibuya==2026.7.12 sphinx-copybutton==0.5.2 diff --git a/poetry.lock b/poetry.lock index 59ad20b46..de7db8d5a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2454,6 +2454,21 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pygments-styles" +version = "0.3.0" +description = "A curated collection of Pygments styles based on VS Code themes." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "pygments_styles-0.3.0-py3-none-any.whl", hash = "sha256:c6c45e9939eb7590345bc9084113bac46c45f12b009d13422be02e80e84a034c"}, + {file = "pygments_styles-0.3.0.tar.gz", hash = "sha256:67746b8fc6ff72c1179ca4d9a8bc89c7f54c196c2ff9d087f07392cd6fde3ecf"}, +] + +[package.dependencies] +pygments = "*" + [[package]] name = "pylint" version = "3.3.9" @@ -3264,6 +3279,22 @@ files = [ {file = "ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809"}, ] +[[package]] +name = "shibuya" +version = "2026.7.12" +description = "A clean, responsive, and customizable Sphinx documentation theme with light/dark mode." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "shibuya-2026.7.12-py3-none-any.whl", hash = "sha256:0a2c0c109e7cc47fc650b01feb671d267c0ffe2e39c98f3529e32d723a9c9d9b"}, + {file = "shibuya-2026.7.12.tar.gz", hash = "sha256:9496606f5b95595511ad86b773d74b4b8d694a4dcc2f848e6ec34e2ad71d9060"}, +] + +[package.dependencies] +pygments-styles = "*" +Sphinx = "*" + [[package]] name = "six" version = "1.17.0" @@ -3494,26 +3525,6 @@ requests = ">=2.32.4" Sphinx = "*" urllib3 = "*" -[[package]] -name = "sphinx-rtd-theme" -version = "3.1.0" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89"}, - {file = "sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c"}, -] - -[package.dependencies] -docutils = ">0.18,<0.23" -sphinx = ">=6,<10" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "transifex-client", "twine", "wheel"] - [[package]] name = "sphinx-tabs" version = "3.4.5" @@ -3621,21 +3632,6 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -groups = ["docs"] -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -4022,4 +4018,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "907371566c23fe8e3a8301b5b635461851b491d2d67683d2b6b72cb9dfee6987" +content-hash = "8e37efc6b5b5fadb6e6765e82068ee1032ca64f2bf85340c89a8bd804aeedd9f" diff --git a/pyproject.toml b/pyproject.toml index adf0cf2fb..bf708349c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ sphinx = "^8.1.3" nbsphinx = "^0.9.5" sphinx-toolbox = "^3.8.1" myst-parser = "^4.0.0" -sphinx-rtd-theme = "^3.0.2" +shibuya = "==2026.7.12" sphinx-copybutton = "^0.5.2" [build-system] From d955005f381ce62cd7db973cb8ea504b85c8083c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 15:01:29 +0200 Subject: [PATCH 203/271] =?UTF-8?q?feat(docs):=20=F0=9F=94=84=20add=20live?= =?UTF-8?q?=20preview=20with=20automatic=20rebuilds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Makefile | 12 +- docs/requirements.txt | 1 + poetry.lock | 427 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 436 insertions(+), 5 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9..6b539a908 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,10 +3,20 @@ # You can set these variables from the command line, and also # from the environment for the first two. +.DEFAULT_GOAL := help + SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build +# Prefer the checkout's documentation dependencies; allow an explicit override. +SPHINXBUILD ?= $(if $(wildcard ../.venv/bin/sphinx-build),../.venv/bin/sphinx-build,sphinx-build) SOURCEDIR = . BUILDDIR = _build +SPHINXAUTO ?= $(if $(wildcard ../.venv/bin/sphinx-autobuild),../.venv/bin/sphinx-autobuild,sphinx-autobuild) +SERVE_HOST ?= 0.0.0.0 +SERVE_PORT ?= 8000 + +.PHONY: serve +serve: + @$(SPHINXAUTO) --host $(SERVE_HOST) --port $(SERVE_PORT) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/requirements.txt b/docs/requirements.txt index 47ac130fb..d9838fe79 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,3 +6,4 @@ sphinx-toolbox==3.8.1 myst-parser==4.0.0 shibuya==2026.7.12 sphinx-copybutton==0.5.2 +sphinx-autobuild==2024.10.3 diff --git a/poetry.lock b/poetry.lock index de7db8d5a..85f1cf755 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,26 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "anyio" +version = "4.15.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + [[package]] name = "apeye" version = "1.4.1" @@ -621,7 +641,7 @@ version = "8.5.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "docs"] files = [ {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, @@ -638,7 +658,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\"", docs = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""} [[package]] name = "colorlog" @@ -977,7 +997,7 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["main", "dev", "test"] +groups = ["main", "dev", "docs", "test"] markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, @@ -1047,6 +1067,18 @@ files = [ {file = "filelock-4.0.7.tar.gz", hash = "sha256:da5915714a70b55d167fdc7e251ad91302b0a36816fb574dfafae8f4f2c9bb21"}, ] +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + [[package]] name = "html5lib" version = "1.1" @@ -3405,6 +3437,29 @@ docs = ["sphinxcontrib-websupport"] lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] +[[package]] +name = "sphinx-autobuild" +version = "2024.10.3" +description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, + {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, +] + +[package.dependencies] +colorama = ">=0.4.6" +sphinx = "*" +starlette = ">=0.35" +uvicorn = ">=0.25" +watchfiles = ">=0.20" +websockets = ">=11" + +[package.extras] +test = ["httpx", "pytest (>=6)"] + [[package]] name = "sphinx-autodoc-typehints" version = "3.0.1" @@ -3714,6 +3769,25 @@ files = [ {file = "standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52"}, ] +[[package]] +name = "starlette" +version = "1.7.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e"}, + {file = "starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d"}, +] + +[package.dependencies] +anyio = ">=4.0.0,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "tabulate" version = "0.10.0" @@ -3927,6 +4001,26 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +[[package]] +name = "uvicorn" +version = "0.54.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf"}, + {file = "uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=13.0)"] + [[package]] name = "virtualenv" version = "21.14.1" @@ -3947,6 +4041,52 @@ platformdirs = ">=3.9.1,<5" python-discovery = ">=1.6" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} +[[package]] +name = "watchfiles" +version = "1.3.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441"}, + {file = "watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935"}, + {file = "watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3"}, + {file = "watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509"}, + {file = "watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661"}, + {file = "watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61"}, + {file = "watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc"}, + {file = "watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6"}, + {file = "watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4"}, + {file = "watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a"}, + {file = "watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f"}, + {file = "watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + [[package]] name = "wcwidth" version = "0.9.1" @@ -3994,6 +4134,285 @@ files = [ doc = ["furo", "sphinx"] test = ["pytest", "ruff"] +[[package]] +name = "websockets" +version = "16.1.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +markers = "python_version == \"3.10\"" +files = [ + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a"}, + {file = "websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9"}, + {file = "websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81"}, + {file = "websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57"}, + {file = "websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3"}, + {file = "websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747"}, + {file = "websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87"}, + {file = "websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea"}, + {file = "websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9"}, + {file = "websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328"}, + {file = "websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737"}, + {file = "websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7"}, + {file = "websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5"}, + {file = "websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3"}, + {file = "websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57"}, +] + +[[package]] +name = "websockets" +version = "17.2" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.11" +groups = ["docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "websockets-17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:569ed5db651e420b13279f9333443bb5b84a436cc66b599cbc535697ae4434a0"}, + {file = "websockets-17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3892d76754b5f36fb40619f3ef09c68e5c3091f1ab8840964518ae5a41f30952"}, + {file = "websockets-17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5436ffea003adb50e283ca0684a3fcaa1396104f841736c3322ee6582bd09e98"}, + {file = "websockets-17.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9df9d048def11365d170b375b6ffc8b23a7f188c3560acd4418ba088ca2e2705"}, + {file = "websockets-17.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:376a693697ddb695ea282ead76060f4847f90e564b12b4389f2c7589e6fadb9e"}, + {file = "websockets-17.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd63d0c7ed0d3d719c91b5a3861f0f0b3cec9bf223033ddf69d17aaac74bb6d"}, + {file = "websockets-17.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:48997ed4431d8006988788ef4b62e1fd3f053c7463b4fa793aa6c4f9e96a3bb7"}, + {file = "websockets-17.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e312e07557a5ad348f4e83d3419773527f6e790c7f97928b1911d767b6ea1c7"}, + {file = "websockets-17.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:902ce8cafca2dc14cef9558a6fc3b45dbf7f121d1404bf2ad18a1c894555e48c"}, + {file = "websockets-17.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e53d950e16d4bb672a5ff41fe3131e65a4e5d688d694e1c7074c8c9990bb3ceb"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:946ac2164d646e733004946ae39536b5af473853183d81da5962e29d36e3ad35"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:660aa158127035e741d4b1835dbe79ae18a1fbb21ecd236655f31d60110e68d5"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4733fc2d99fe888261417b7e29995403a72d9ffa78629902882325ea141177f2"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c2ec7e51157a3fa0e9cfdb1a8969bab38d1c22ad1ace7c6cea006383b43a1ad4"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ada04d0262ab06527054a2a497f384d102698ff39b3865dc566a7d24b6f4058c"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c393a202df08e96ed619310f0cd78be700e532a57d9a6ceee5f80b4e35bef14"}, + {file = "websockets-17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:af4c565b923bb5975401b8e4cedc2e17b2fdbf33b905737ee12384e6a6fd9507"}, + {file = "websockets-17.2-cp311-cp311-win32.whl", hash = "sha256:c81d6cdbacccda7e0eef3b076a457fd14c3835cdbc5993d2881580c2fb1f5f26"}, + {file = "websockets-17.2-cp311-cp311-win_amd64.whl", hash = "sha256:55c5b9eab079540bfb639b40b07b7b467e5c5a7ecf97a65cc8665781381c9856"}, + {file = "websockets-17.2-cp311-cp311-win_arm64.whl", hash = "sha256:55f9a808a0e072473337c240c939849818276e288e2374b832255b5b791b0851"}, + {file = "websockets-17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:916ebdfd82e7fc68041d36b2b5f60361b9abce1e087454da15f8bd004839e090"}, + {file = "websockets-17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3621f3686397708b8eeabfd0a9d75267c1f29a7537d2fe31e65d099e71587fa4"}, + {file = "websockets-17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a81e19710d48da88653473b6b9c366d47e99fe4f58e37ce415be47966748f31f"}, + {file = "websockets-17.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2731f9067976c8c4127212c0d2f2ada42d497d935e470419e029802365b12bb"}, + {file = "websockets-17.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6627b913b8586b1c06db9516b31dd0dfbc621de3bb9312616d92a7e44f268a5b"}, + {file = "websockets-17.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0198c4ec6a3406a2f7557c032967de426474c2c995c81076585e09d29a9f407b"}, + {file = "websockets-17.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:88c6a42c2632ff469e84155e44f6ed92cb15ccb047bf5fcb59225ae5a12fd33d"}, + {file = "websockets-17.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb0023e6cdb4b8ece0b33875188dd16104ad8c335361d396a98394f99e30ff7a"}, + {file = "websockets-17.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1c09d5d4646eb96bda2cfb97493bcea21a0956a981de116e6b1f4a9de07f3fd"}, + {file = "websockets-17.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0360c4dc13ac569cc245e0efa2f4d4b1e4733d24c47b8ab3f3747227b1356348"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76693a16dead737946b651375ee3109d7db7ad9569a1c55c60aaed3ef85cfcc6"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:77a42cc507993ec5471b5283f7eef869239173b6000031543e3938a86d1af0fd"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3bbc5543e39ee025d524077c5c15c2d67bc11c9f6676afe5b531839e24d701f6"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8da58558bfb0ca6ccac2419773521f1111e40654038b1afabdfc69c02cb82614"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:01420cb1cb47433e8e7075d32cb8017ad3ffed0654bd1e48c0251b865920dec3"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c49c9edd47d0e44d360299e2d8865e2950d2fcf1b4098782c9d7dcd070919e5a"}, + {file = "websockets-17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96f6c8d0fe21930d1f982bfce2382789d2e8d005d2ab63d21280660f95ef8fe1"}, + {file = "websockets-17.2-cp312-cp312-win32.whl", hash = "sha256:b25659ab2d655d742701487d5591e3f98e8f8b329fc999e05e3d59691ab344a1"}, + {file = "websockets-17.2-cp312-cp312-win_amd64.whl", hash = "sha256:faa763b677e96f1beccc6b4d7e8c079dfeed2f249f57a19debc321b519ee64ec"}, + {file = "websockets-17.2-cp312-cp312-win_arm64.whl", hash = "sha256:63499fc49efe48bccc2fca40723bc7adb198866cbe159093dd979905316994b6"}, + {file = "websockets-17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b24b83fbb34b2d8de06cf0f0d4bd7737344ef854482a614826d4356c0c3f0c12"}, + {file = "websockets-17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a829db795e3f87053904493d184b185c8eb1f497c852f434168ec856aa6f997"}, + {file = "websockets-17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf8811d285acc91216368df7fb55cc8c9bf6fcd90eea42429c7186c7385a12b9"}, + {file = "websockets-17.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89c4898da776193577279173dcf9860487590611d7320d379435a145881b048d"}, + {file = "websockets-17.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d87091c4347daadbcc0833b65812ff38d7350c67339625d4e4a512cf38e3e8ef"}, + {file = "websockets-17.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1110fbfd530c447380e6e6db88b7e43ffe33d54178f5b0ff0aaa5a280301e668"}, + {file = "websockets-17.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:83abd8beab056aa77a116364811f8fc262dffbcc7abea48de0c85ccbfc6f1428"}, + {file = "websockets-17.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:876da8ca5520d65b5d0f2ca6b4e7a00d35bb90ccda35cb2ce3cda4b6c711e84a"}, + {file = "websockets-17.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8462395df8f224d2daa3d80db3ae4450d9d4b7243c8483ac79a82862f1599dd6"}, + {file = "websockets-17.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e9a04e69456015e6ae5e0d486d995137fd435794442122b00ce5f9526ea3ba8"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a2321bcb73758c44c8076509024d02c15ee484fe77ce04edea4bf4d257492cc"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8be4a87b3baca380ec3c7b1643b2dd268ac9d42c5097c0e8dc9a49342faf4774"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:eb7b737ce8d18c8a08beb68f751572b7bf6a18093ecd1406ca1256b50592552e"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d6605630c2808b33f362d6d08582e79821f77ed2bd3f49f9d467ea70defea06d"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd9252828073fd0d69e7667af4275a1b17c18d0833b1ab7f59db272f194a6b9a"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:06c7386128a9d85de4e1960114604f3031c084d2f4eee8db382637f1634cbab1"}, + {file = "websockets-17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98f2d03df74977fd252831c997c388cd6c3f691a8a9d022b266d3cbd9849838f"}, + {file = "websockets-17.2-cp313-cp313-win32.whl", hash = "sha256:5b43a1f7e4853ce08c3f6d3bf69799ee5b46548bfb71792a8158f7e45d66b547"}, + {file = "websockets-17.2-cp313-cp313-win_amd64.whl", hash = "sha256:27c7a59b5352a8f741b422820adfe89dfe47c8f2d84fb32111e76111edaa0e83"}, + {file = "websockets-17.2-cp313-cp313-win_arm64.whl", hash = "sha256:533b7c82bb1eafbeb921dfe131c9f88e55451ddc328d84bde1c9340ba72d2808"}, + {file = "websockets-17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ecb748910e9ba4624ebe2057791df51dcbffb48c37108ab94a3c593472023c9e"}, + {file = "websockets-17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ab9af5cb7265899e659f079eb71691375a1025b6d5fbd3caa495dd08f70833a"}, + {file = "websockets-17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:06e46da092bca3a52e98f0458c66b247993ce501a07cd09c858be3296511ab7d"}, + {file = "websockets-17.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fcce735ffd72ac4056db05325d9f0232382b74826f0196eb6a15ca903abdaa0f"}, + {file = "websockets-17.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:42cbca10f82a8b2fb1536e8a0830ca6ceeb6bb3d8d64b766e0795369135654a8"}, + {file = "websockets-17.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63ff5a21f26bd0e6a8464b53fadbe174825c8718ac14180df45665eaacdb6af"}, + {file = "websockets-17.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63f543463601c1558b755f8dd7618b6ec3dd0934dda051d3b7030d8c76e54de2"}, + {file = "websockets-17.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4c32eb565ad9ce8a6444248e5b7a19dbb86a81c811fe5fcc2fba7a735aed5163"}, + {file = "websockets-17.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d459bbb6c22f26dcebea56924a362aba50d453b9867912862c970434fcf0d94"}, + {file = "websockets-17.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f19ca1a21871f024e38faf4107b433047df27558dff1b72a1dac31481e2c1fe5"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c76b4bcbf0f713194591673fc86a42820e14da6bbd1bb445d3d002cc4d1e4521"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:30201a7f69833b015556c72feb69ea501b645986fd0b90dab13f589e995ff428"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0c8600aec354cc259f1691b0b42816f04a9886a953f82cb227246df76057f97a"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:307fc22ea496be8542d67b82ae8c867a978dfd19ac35573d4f15943fd9277dfe"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c88697fa943bd4ef67cc919a17d81de6581846f52bfa8c6f64a916098986556"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7eac84d4969da82166d5e90d9c38d2f416fe24f9708a7013569b193745b9a31"}, + {file = "websockets-17.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:313f6703023d53baabab6d6c5c37cf637b2c4fee255acf2ed5e92ad69e28f1b7"}, + {file = "websockets-17.2-cp314-cp314-win32.whl", hash = "sha256:08d90cf344bdb971ba3a826b78d4da9bfd56cc6a97a604d9b88cbd40bfa6c735"}, + {file = "websockets-17.2-cp314-cp314-win_amd64.whl", hash = "sha256:dac93bf7a9beb215be3282b8441173cd50806c41c007b8be9bb24e03c60ad563"}, + {file = "websockets-17.2-cp314-cp314-win_arm64.whl", hash = "sha256:2ab742249f953d148a9ba696c8b9944361e8cb92e8bc61ba2dd53a178403afd3"}, + {file = "websockets-17.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a69ce25be5f1330ee1c74eb6fabbbceaa96b384beedd2627cecded7546490c40"}, + {file = "websockets-17.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8e24b878cf54843a63985d90480f163ca7f692689fbcbe9cdbd8165521083a8b"}, + {file = "websockets-17.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f33c7908a6885dcae9f462a4a8347b637053b4ff2b96beb4c23fba1cf7818e5f"}, + {file = "websockets-17.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c796a1bb3e4015249639849f30e8e680df8a431b45d417ba8acf843d2451d95f"}, + {file = "websockets-17.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:983bcdc898662f6ba9d6a025c30d29946ff0986d9ad60d400af0da3671f7cbf3"}, + {file = "websockets-17.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0f088ddfd9d9bc5019e27ff3767411779e92b59db5bb1507f2731a5b61158"}, + {file = "websockets-17.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:19e2511412ad3393191de652513bc7a0ca3c93af143b32d96d46e59fbbddf1d4"}, + {file = "websockets-17.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb5e2bf969ac99a6ae3c71208a5eb05cfde973192540ffa6e1068b57fb78c4f8"}, + {file = "websockets-17.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:691780fca2be3dec512cb603cb91060271968cb4af86b51d07c57445c5754a37"}, + {file = "websockets-17.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d39c19b1ba6a6791050383fd69efdd3b63533e2254693d0263879cd5f5921ba"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e48ac2b302986c6f55cf61e8e36b4dd97d0132c5078a713a697a940934ba422e"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e136197f1262620ef2e507afc3ea759c1ae7d221886da20eec5f4c9f2618c2aa"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3eb44019a2b0b3b91bac95998f1e4e5589730421170e060fe654a2b7be727dc7"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e5855e574804398859c5fbaf4fc7882b96278b7f6572a3d889627e6eb6cfca59"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5dc29815520c329f5662f6eb3ebadecf0d4f8c82dfa416d4d6efbf8f39245559"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d1a4f9462da6496b6cb79bbb09c60d17f7e63e8a1df136797b3afabec9560e4d"}, + {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9496bff5541086478264678bac73c0a75b2fde94fdf6568893bca1f7c6d50d18"}, + {file = "websockets-17.2-cp314-cp314t-win32.whl", hash = "sha256:e1e3bc8090a7eae79fdf634b63bdbfa3c93999991023c37c6fd3b469fc8ff5dc"}, + {file = "websockets-17.2-cp314-cp314t-win_amd64.whl", hash = "sha256:65a89a5bde227bfe908016f35b5bd347970cd1e5b0360f389502eba1c7fde6e0"}, + {file = "websockets-17.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1c27339934109dfaca83f18ab2c23db06714e9d5deca2c8e37e8f492ab90d20b"}, + {file = "websockets-17.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:a7c4bb26de6ef496d24822aee4f6a305d97cd33d21a2b85f290292d69ba1c25e"}, + {file = "websockets-17.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c08da1f15040bd1e1a6074bd4518a6ef20e67b1594ecfb0aa75e5b45f87e6d6d"}, + {file = "websockets-17.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3117abfd32b183bdb6194df9317766d32c6517f3d1c0aa8c62d5c6ccfda0b4a8"}, + {file = "websockets-17.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a046227daa7f191e843d26b911c1146233e9a33d249e0c954dcb3ac7c398710e"}, + {file = "websockets-17.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2901bdf24f20bc884124b3e88c61f7ece260c20c81e610f2196007395264a4aa"}, + {file = "websockets-17.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60e39adfecf998488166aca8ff24ab1ac406c9ecbecbcf9b3bcfc43cb1ec9a1"}, + {file = "websockets-17.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d4df62fd8448a85c752bbea1803cb3a2785e6fc8352009ab64ad7447af079b3c"}, + {file = "websockets-17.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8eea55fdfa9ba65c6981eea38bd20c800bce2f092a2803d82de764ecf0f071a"}, + {file = "websockets-17.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3f0def1279644acaa9bc861d4234af3f82ea9cee7e460dffac5cb63e691501e9"}, + {file = "websockets-17.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb78fb4158c12f77a934a003006784108a27a6553cfc0c6f10483c9c02e94f48"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f8969ad228115ad8869b5fed801f899e52ab8ad376fdb165ba4760a277c8258a"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:4a49ca342efc0800e6ae94ed5c9cbdcb319308f75e73c21181e4c24d6710e8dd"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:06fa3ce9c3154826c33d4395b225b2994aa64f1f3bcd8be8ed932019175d9268"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:50644d8715be7e0ec0682f9d7744b63008e199c5e1618a48fa153756a332235f"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:60deca33e584c09e91f70f8b55a0b1de7d671d6a63f051d154920f48bed717c7"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b5f79366a8d8dbb981d53ba800bb54a95454595ab8a4548c2b95501b32a08326"}, + {file = "websockets-17.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f2bbf3f28d0b63157577c8b774b9136f076afa6797e1a52a2ecd477f23cad3a8"}, + {file = "websockets-17.2-cp315-cp315-win32.whl", hash = "sha256:74836317b7010b579522bb52426f1e225608b042c9e78cbe2493522bebb8a318"}, + {file = "websockets-17.2-cp315-cp315-win_amd64.whl", hash = "sha256:aaead3d926e9ab4124ada727d20cd62d396649917822df4f771d1f07f1079b40"}, + {file = "websockets-17.2-cp315-cp315-win_arm64.whl", hash = "sha256:40960554e60eb60c3eec4ff9e42a80f84f8cd3ca9bc80a5481a61f1e64d807c9"}, + {file = "websockets-17.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9a2a60a7f0ea5f239efb6391d2b28630a640d82dad63e3bee47cf2c623c4495d"}, + {file = "websockets-17.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:cca2fcb72c007103740fa4fc3df19fdb1a318c641c69f3b0cc47ed63a889336e"}, + {file = "websockets-17.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b789356bc4e2e6c20ba52817f92c3fed74e24657654237ecd536c54843b80c6c"}, + {file = "websockets-17.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:222fb626fa15701a850eccc778be17312142b2f6a0e16aea80770b7459adb784"}, + {file = "websockets-17.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4497e87c34a2d21cbec1227858fec3af8e514dd70c47625557a122fcebc081dc"}, + {file = "websockets-17.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6281c171557ce0e408e19d9a223f22d915117ac38a5a7f32ed83809e7492316c"}, + {file = "websockets-17.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:08d97098644728bd1895caa7ecf3090b8e563d70809870d2adb33a107bd061d0"}, + {file = "websockets-17.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1fdb8d5a1660307dc6d36d0b7fc725213cbd7f80800904dc4896aa3208b89121"}, + {file = "websockets-17.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18b0a46e5e9b315e2b54ce8c3bafdeef0e1388ca363114fa868e6aab2dc58512"}, + {file = "websockets-17.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f115d5d804a2163dd89245710049078b0e726a58c1f44a1f86c2c6e79055d76"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1d829946a2e7630f92f9d7b45b62f3abe9f393cc2dea6a35edb3988f865e75f2"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:6c274fc1572edf7c197094a0eb1887d45fdc95254bc80597dc7599550486c06a"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:4173a4b8a025ae44313d9d9b4ecf31e886c7b7faf45386d51a8ca4ff2dcf3f2a"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:d8cfe9522ad69b6abb26b413ed1deca43cb915cefc588433d557cb3ae1c783e2"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:908d81d88bb16141613a6275059b5114656d5c2f0b5400b421d54fe6f1943507"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c6590e1eb624ff6b15b872421bc9a10bc6d2057635d69c6cd244ac3f928f85c6"}, + {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:61040f6f7da5a279d2f77496c69d51132aba75f701c52bded400d4c639277b18"}, + {file = "websockets-17.2-cp315-cp315t-win32.whl", hash = "sha256:f90bad2839c185a1edf8ee22a257cfc8a39e0e337a0490ab185dfa76ef04d1bd"}, + {file = "websockets-17.2-cp315-cp315t-win_amd64.whl", hash = "sha256:315551f4ccedbbf9fd4f7e8bf037a5948c976ade0e919ba5d8f581d465f6f725"}, + {file = "websockets-17.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0a6220bdf8d5f11af71251a599092d89ac1d6bfac691c7f5951c5b07953947a0"}, + {file = "websockets-17.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2de1ccf298f5c9e0f27113836d742edb95f015eee3148f004ac386f7ba9a05b1"}, + {file = "websockets-17.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:761cde41439f0be761aa460e1451a31e2e14baf4a46db6fe4913e5a06a90df66"}, + {file = "websockets-17.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:15a7101b660a9f15fac34108c92cefc9848f6753a50acef8869e3cd94148fdb7"}, + {file = "websockets-17.2-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:214da56dba368f61b3d745c77630b2d03c61c02da7b42fe80ef6efba079d3077"}, + {file = "websockets-17.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80cbc645af23ac5c12096545c161626960114a1bc10f864760558d3b3e82ba18"}, + {file = "websockets-17.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:063508ce9e0db745f30ab52fc652f4e59efc79c2b74934b3837d5cdb974da620"}, + {file = "websockets-17.2-py3-none-any.whl", hash = "sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae"}, + {file = "websockets-17.2.tar.gz", hash = "sha256:36c2fb94c990cc2545143b12690e2de6c16300f9dbe5b4f33fa300cf57dc8792"}, +] + [[package]] name = "zipp" version = "4.1.0" @@ -4018,4 +4437,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "8e37efc6b5b5fadb6e6765e82068ee1032ca64f2bf85340c89a8bd804aeedd9f" +content-hash = "125fe78f66565d99ddbeca5593513fc401b989b4a743fea564d0cf860e19fd08" diff --git a/pyproject.toml b/pyproject.toml index bf708349c..b10a677c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,7 @@ sphinx-toolbox = "^3.8.1" myst-parser = "^4.0.0" shibuya = "==2026.7.12" sphinx-copybutton = "^0.5.2" +sphinx-autobuild = "==2024.10.3" [build-system] requires = ["poetry-core"] From 2ddbded2e6a5e36afbae80578f6dd60d47db98ef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 15:03:10 +0200 Subject: [PATCH 204/271] =?UTF-8?q?docs(preview):=20=F0=9F=93=96=20explain?= =?UTF-8?q?=20documentation=20builds=20and=20live=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/0_toc.rst | 1 + docs/15_building_documentation.rst | 46 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 docs/15_building_documentation.rst diff --git a/docs/0_toc.rst b/docs/0_toc.rst index ffbaa0d22..df85dcd47 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -31,6 +31,7 @@ 11_writing_a_profile 13_optimizing_shacl_target_selection 14_graph_transformers + 15_building_documentation 10_api genindex diff --git a/docs/15_building_documentation.rst b/docs/15_building_documentation.rst new file mode 100644 index 000000000..f6b243799 --- /dev/null +++ b/docs/15_building_documentation.rst @@ -0,0 +1,46 @@ +Building and previewing the documentation +========================================= + +Install the documentation dependencies +-------------------------------------- + +From the repository root, install the documentation dependency group into the +project's virtual environment: + +.. code-block:: console + + poetry install --with docs + +The documentation uses the Shibuya theme, with a custom palette and styles for +both light and dark mode. The ``docs/Makefile`` uses Sphinx from ``.venv`` when +available, otherwise it uses the executable on ``PATH``. + +Build HTML +---------- + +.. code-block:: console + + make -C docs html + +Open ``docs/_build/html/index.html`` to read the generated documentation. + +Live preview +------------ + +.. code-block:: console + + make -C docs serve + +Open ``http://localhost:8000`` in your browser. The server listens on +``0.0.0.0``, so it is also accessible through the machine's network address. +Changes to documentation sources, configuration, and static files trigger an +automatic rebuild and reload the browser page. Stop the server with ``Ctrl+C``. + +To choose a different address or port: + +.. code-block:: console + + make -C docs serve SERVE_HOST=127.0.0.1 SERVE_PORT=8080 + +The preview server uses ``sphinx-autobuild``. You can override its executable +with ``SPHINXAUTO``, and the regular build executable with ``SPHINXBUILD``. From f9b8822d59ebe54ebf3f44169adfd11d91f64e63 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 16:22:49 +0200 Subject: [PATCH 205/271] fix(docs): improve status note contrast in dark mode --- README.md | 4 ++-- docs/_static/custom.css | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0d0502fe..bef4feaaa 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ against different profiles, including the base RO-Crate profile and various exte - Supports [CLI-based validation](#cli-based-validation) as well as [programmatic validation](#programmatic-validation) (so it can easily be used by Python code). - Extensible framework: new RO-Crate profiles can be added, implementing profile requirements as SHACL shapes and/or Python code. -
-Note: this software is still work in progress. Feel free to try it out, +
+Note: this software is still work in progress. Feel free to try it out, report positive and negative feedback. We also welcome contributions, but we suggest you send us a note (e.g., by opening an Issue) before starting to develop any code. The implementation of validation code for additional RO-Crate profiles would be particularly welcome.
diff --git a/docs/_static/custom.css b/docs/_static/custom.css index bc6458b84..98609b928 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -56,6 +56,15 @@ html.dark[data-accent-color="teal"] { --yue-c-link-2: #a3e4e2; } +/* The README's status callout has a light fallback for GitHub; adapt it to + * the documentation theme's dark palette when dark mode is selected. + */ +html.dark .project-status-note { + background-color: #123035 !important; + border-left-color: #39989f !important; + color: #e0efef !important; +} + /* Callouts use teal for information, orange/yellow for warnings and attention, * brown for important details, and red for errors. Override each semantic type: * the theme's callout backgrounds otherwise remain too light in dark mode. From 83a84c4a02b0a66d8e476c6d6dfd075edfa2028c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 17:11:14 +0200 Subject: [PATCH 206/271] =?UTF-8?q?feat(docs):=20=F0=9F=A7=AD=20improve=20?= =?UTF-8?q?navigation=20and=20index=20hierarchy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_static/custom.css | 196 ++++++++++++++++++++++ docs/_templates/components/nav-links.html | 62 +++++++ docs/conf.py | 9 + docs/index.rst | 8 +- 4 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 docs/_templates/components/nav-links.html diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 98609b928..6d1696136 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,5 +1,21 @@ /* Teal palette with darker text accents for contrast in light mode. */ +:root { + --font-mono: "JetBrainsMono Nerd Font", "JetBrains Mono", "Fira Code", + "Cascadia Code", "SFMono-Regular", Menlo, Monaco, Consolas, + "Liberation Mono", monospace; +} + +code, +kbd, +pre, +samp, +.sy-head-brand strong, +.product-name { + font-family: var(--font-mono); +} + html.light[data-accent-color="teal"] { + --toc-emphasis: #4b5358; --accent-2: #f2f9f9; --accent-3: #e5f3f3; --accent-4: #d3ebec; @@ -25,6 +41,7 @@ html.light[data-accent-color="teal"] { } html.dark[data-accent-color="teal"] { + --toc-emphasis: #c2cbce; --accent-2: #0d292c; --accent-3: #12383c; --accent-4: #164247; @@ -204,6 +221,185 @@ html.dark .admonition > .admonition-title { border-color: #39989f; } +/* Use the logo's blue sparingly to distinguish the quick links in the header. */ +.sy-head-links .nav-link-icon { + color: #37a5da; + margin-right: 0.4rem; +} + +.sy-head-links a:focus-visible, +.sy-head-links button:focus-visible { + outline: 2px solid #37a5da; + outline-offset: 3px; + border-radius: 4px; +} + +/* Use visible, theme-colored bullets and progressive indentation in the TOCs. */ +.yue .toctree-wrapper > ul { + padding-left: 1.25rem; +} + +.yue .toctree-wrapper > ul > li::marker { + color: var(--accent-9); + font-size: 1.05em; +} + +.yue .toctree-wrapper > p.caption { + color: var(--toc-emphasis); + font-weight: 700; +} + +.yue .toctree-wrapper > ul > li > a { + color: var(--toc-emphasis); + font-weight: 600; +} + +.yue .toctree-wrapper ul ul:not(:empty) { + list-style-type: circle; + margin-left: 0.3rem; + padding-left: 1rem; + border-left: 1px solid var(--sy-c-divider); +} + +.yue .toctree-wrapper ul ul a { + font-weight: 400; + font-style: italic; +} + +.globaltoc > ul { + padding-left: 1.25rem; + list-style-type: disc; +} + +.globaltoc li.toctree-l1 > a { + display: block; + padding: 0.18rem 0.4rem; + border-radius: 4px; + color: var(--toc-emphasis); + font-weight: 600; +} + +.globaltoc li.toctree-l2 > a, +.globaltoc li.toctree-l3 > a { + font-weight: 400; + font-style: italic; +} + +.globaltoc > ul > li::marker { + color: var(--accent-9); +} + +.globaltoc > ul > li.current > a { + font-weight: 700; +} + +.globaltoc li.current > a.current { + border-left: 3px solid var(--accent-9); + background-color: var(--accent-a2); + color: var(--toc-emphasis); +} + +/* Hide toggles for empty toctree placeholders; keep useful expand controls. */ +.globaltoc li:has(> ul.simple) > button { + display: none; +} + +.globaltoc ul ul { + padding-left: 1rem; + list-style-type: circle; +} + +.globaltoc ul ul ul { + list-style-type: square; +} + +.localtoc ul { + margin-left: 0.15rem; + padding-left: 0.9rem; + border-left: 1px solid var(--sy-c-divider); + list-style: none; +} + +.localtoc > ul { + margin-left: 0; +} + +.localtoc li { + position: relative; + padding-left: 0.55rem; +} + +.localtoc > ul > li > a { + color: var(--toc-emphasis); + font-weight: 600; +} + +.localtoc ul ul > li > a { + font-size: 0.96em; + font-weight: 400; + font-style: italic; +} + +.localtoc ul ul ul > li > a { + font-size: 0.92em; + font-style: normal; +} + +.localtoc ul ul ul ul > li > a { + font-size: 0.88em; + color: var(--sy-c-light); +} + +.localtoc li::before { + position: absolute; + top: 0.5em; + left: -1rem; + font-family: system-ui, sans-serif; + font-size: 0.55rem; + font-weight: 700; + line-height: 1; + color: var(--accent-8); + content: "●"; +} + +.localtoc ul ul { + margin-top: 0.25rem; + margin-bottom: 0.25rem; + padding-left: 0.75rem; +} + +.localtoc ul ul li::before { + top: 0.5em; + left: -0.82rem; + font-size: 0.48rem; + color: var(--sy-c-light); + content: "○"; +} + +.localtoc ul ul ul li::before { + font-size: 0.42rem; + font-weight: 700; + color: var(--accent-8); + content: "▪"; +} + +.localtoc ul ul ul ul li::before { + font-size: 0.45rem; + font-weight: 400; + color: var(--sy-c-light); + content: "–"; +} + +.localtoc li.active > a { + color: var(--toc-emphasis); + font-weight: 700; + font-style: normal; +} + +.localtoc li.active::before { + color: var(--accent-9); +} + /* Keep the composition summary within the documentation content column. */ .profile-composition-table { width: 100%; diff --git a/docs/_templates/components/nav-links.html b/docs/_templates/components/nav-links.html new file mode 100644 index 000000000..6eadbced3 --- /dev/null +++ b/docs/_templates/components/nav-links.html @@ -0,0 +1,62 @@ +{%- macro expandURL(url, resource=False) -%} +{%- if url.startswith(('https://', 'http://')) -%} +{{ url }} +{%- else -%} +{{ pathto(url, resource=resource) }} +{%- endif -%} +{%- endmacro -%} + + diff --git a/docs/conf.py b/docs/conf.py index 21ed46164..f7a3bb85f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -137,6 +137,15 @@ # -- Options for HTML output ------------------------------------------------- html_theme_options = { "accent_color": "teal", + "nav_links": [ + {"title": "CLI", "url": "2_usage_cli", "icon": "lucide:terminal"}, + {"title": "API", "url": "3_usage_api", "icon": "lucide:braces"}, + { + "title": "Profiles", + "url": "12_validation_profiles", + "icon": "lucide:boxes", + }, + ], "nav_socials": [ {"name": "GitHub", "url": github_url, "icon": "simple-icons:github"}, ], diff --git a/docs/index.rst b/docs/index.rst index 88f26c486..0a9bd839b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,9 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. -============================================= -Welcome to rocrate-validator's documentation! -============================================= +.. role:: product-name + +============================================================= +Welcome to :product-name:`rocrate-validator`'s documentation! +============================================================= .. _index: From 7a0b2aaae906d51951e6fe019d46a14dc10c9cb1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 17:11:56 +0200 Subject: [PATCH 207/271] =?UTF-8?q?docs(navigation):=20=F0=9F=94=97=20cros?= =?UTF-8?q?s-link=20CLI,=20API,=20and=20profile=20guides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/12_validation_profiles.rst | 9 +++++++++ docs/2_usage_cli.rst | 10 +++++++--- docs/3_usage_api.rst | 10 +++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/12_validation_profiles.rst b/docs/12_validation_profiles.rst index 9d093382a..260641e18 100644 --- a/docs/12_validation_profiles.rst +++ b/docs/12_validation_profiles.rst @@ -60,3 +60,12 @@ This mechanism allows you to: of a predefined profile with the same URI. It is different from a :ref:`profile rule overlay `, which composes a target profile from an inherited source profile and only redefines selected checks. + +.. seealso:: + + * To create a new profile or implement its checks, see + :doc:`Writing a profile <11_writing_a_profile>`. + * To load additional profiles from the command line or Python, see the + :doc:`CLI guide <2_usage_cli>` and the :doc:`Python API guide <3_usage_api>`. + * For profile-related services and models, see the + :doc:`API reference <10_api>`. diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 4169efce3..8891fa290 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -36,6 +36,10 @@ CLI Validation .. seealso:: - To validate without network access and manage the HTTP cache from the - command line (the ``--offline`` and ``--no-cache`` flags and the ``cache`` - subcommand), see :ref:`offline_mode`. + * To validate without network access and manage the HTTP cache from the + command line, see :ref:`offline_mode`. + * For the equivalent Python workflow, see the :doc:`Python API guide + <3_usage_api>` and the :doc:`API reference <10_api>`. + * To select additional validation profiles, see + :doc:`Validation profiles <12_validation_profiles>`; to create a new + profile, see :doc:`Writing a profile <11_writing_a_profile>`. diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 673f6ec27..08e531fb9 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -32,9 +32,13 @@ Programmatic Validation .. seealso:: - To resolve resources from a local cache or run validation without network - access (the ``offline`` / ``no_cache`` settings of ``ValidationSettings``), - see :ref:`offline_mode`. + * To configure the HTTP cache or run validation without network access, + see :ref:`offline_mode`. + * For command-line validation, see the :doc:`CLI guide <2_usage_cli>`. + * For the complete package API, see the :doc:`API reference <10_api>`. + * To select or write validation profiles, see + :doc:`Validation profiles <12_validation_profiles>` and + :doc:`Writing a profile <11_writing_a_profile>`. Metadata-only Validation From 428338942ab0028f6894e4b2dabe4fd79b33bf3b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 18:54:24 +0200 Subject: [PATCH 208/271] =?UTF-8?q?docs(navigation):=20=F0=9F=A7=AD=20grou?= =?UTF-8?q?p=20usage=20and=20development=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/0_toc.rst | 12 ++++++++---- docs/10_api.rst | 9 +-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/0_toc.rst b/docs/0_toc.rst index df85dcd47..7aa8efa1f 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -20,20 +20,24 @@ 1_installation 2_usage_cli 3_usage_api + +.. toctree:: + :maxdepth: 5 + :caption: Using the Validator + 4_how_it_works + 12_validation_profiles 5_offline_mode + 10_api .. toctree:: :maxdepth: 5 - :caption: Resources + :caption: Profile Development - 12_validation_profiles 11_writing_a_profile 13_optimizing_shacl_target_selection 14_graph_transformers 15_building_documentation - 10_api - genindex .. toctree:: :maxdepth: 1 diff --git a/docs/10_api.rst b/docs/10_api.rst index 9fc33b470..b3d2618b6 100644 --- a/docs/10_api.rst +++ b/docs/10_api.rst @@ -15,14 +15,6 @@ .. _api: -.. toctree:: - :maxdepth: 5 - :caption: Getting Started - -.. toctree:: - :maxdepth: 5 - :caption: Resources - ################################################### API documentation ################################################### @@ -32,6 +24,7 @@ This section documents the ``rocrate_validator`` package's API. The for using them to validate RO-Crate metadata, and for interpreting the results. Below, you will find descriptions and usage examples for the core services, models, and other components provided by the package. +See the :ref:`Index ` for an alphabetical list of API entries. The Python API is structured in three main parts. From a1fbf92e555d540f9322b1b2e6d7b16767c7ab46 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Oct 2026 19:33:45 +0200 Subject: [PATCH 209/271] =?UTF-8?q?test(package-mode):=20=F0=9F=A7=AA=20as?= =?UTF-8?q?sert=20detached-rule=20behavior=20independently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_package_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_package_mode.py b/tests/unit/test_package_mode.py index 4ec3b679e..e9f7dc26f 100644 --- a/tests/unit/test_package_mode.py +++ b/tests/unit/test_package_mode.py @@ -204,7 +204,7 @@ def test_metadata_mode_controls_detached_rule_and_reports_scope(monkeypatch, tmp (tmp_path / "missing.txt").write_text("irrelevant") monkeypatch.chdir(tmp_path) second = _validate_metadata(metadata, "auto") - assert first.passed() and second.passed() + assert not any("not web-based" in issue.message for result in (first, second) for issue in result.issues) assert first.skipped_checks_count == second.skipped_checks_count assert any("package context is unavailable" in detail.message for detail in first.skipped_check_details) assert first.context.publicID == second.context.publicID @@ -214,7 +214,7 @@ def test_metadata_mode_controls_detached_rule_and_reports_scope(monkeypatch, tmp attached = _validate_metadata(metadata, "attached") detached = _validate_metadata(metadata, "detached") - assert attached.passed() + assert not any("not web-based" in issue.message for issue in attached.issues) assert not detached.passed() assert any("not web-based" in issue.message for issue in detached.issues) assert detached.to_dict()["validation_settings"]["packaging_mode_explicit"] is True From 7849d0567a50a321e26e375fa064298a122a853d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 09:50:42 +0200 Subject: [PATCH 210/271] =?UTF-8?q?feat(models):=20=F0=9F=A9=BA=20add=20di?= =?UTF-8?q?agnostic=20check=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/__init__.py | 3 +- rocrate_validator/models/requirement.py | 19 +++++-- rocrate_validator/models/result.py | 75 +++++++++++++++++++++++-- rocrate_validator/models/settings.py | 2 + rocrate_validator/models/statistics.py | 65 ++++++++++++++++++++- rocrate_validator/models/validation.py | 8 +++ 6 files changed, 161 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index fb64fc324..7efbba62a 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -34,7 +34,7 @@ RequirementLoader, SourceSnippet, ) -from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationResult +from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationDiagnostic, ValidationResult from rocrate_validator.models.settings import ( DEFAULT_PROFILES_PATH, BaseTypes, @@ -87,6 +87,7 @@ "SkippedCheckDetail", "SourceSnippet", "ValidationContext", + "ValidationDiagnostic", "ValidationEvent", "ValidationResult", "ValidationSettings", diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 37401c627..14dda9b9a 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -218,10 +218,11 @@ def _do_validate_(self, context: ValidationContext) -> bool: checks_to_perform = [check for check in self._checks if not context.is_check_skipped(check)] configured_skips = [check for check in self._checks if check not in checks_to_perform] for check in configured_skips: + message = context.check_skip_reason(check) context.result._record_check_result( check, CheckResult.SKIPPED, - "Check was skipped by validation settings", + message, SkipCategory.CONFIGURED, ) processed_checks: set[RequirementCheck] = set() @@ -441,7 +442,7 @@ def __execute_check__( # Python check twice. recorded_result = context.result.get_check_result(check) if recorded_result is not None: - failed = recorded_result is CheckResult.FAILED + failed = recorded_result is CheckResult.FAILED and not check.diagnostic return all_passed and not failed, failed and context.fail_fast execution_check = self.__resolve_execution_check__(check, context) @@ -496,10 +497,12 @@ def __execute_check__( check.identifier, normalized_result, ) - new_all_passed = all_passed and normalized_result is not CheckResult.FAILED + new_all_passed = all_passed and (check.diagnostic or normalized_result is not CheckResult.FAILED) # A batched backend can also report failures for checks other than the # one driving this execution. - should_break = context.fail_fast and (normalized_result is CheckResult.FAILED or context.result.has_issues()) + should_break = not check.diagnostic and context.fail_fast and ( + normalized_result is CheckResult.FAILED or context.result.has_issues() + ) return new_all_passed and not should_break, should_break def __eq__(self, other: object) -> bool: @@ -900,6 +903,7 @@ def __init__( *, deactivated: bool = False, depends_on: tuple[str, ...] | None = None, + diagnostic: bool = False, ): self._requirement: Requirement = requirement self._order_number = 0 @@ -909,6 +913,7 @@ def __init__( self._hidden = hidden self._deactivated = deactivated self._depends_on = tuple(depends_on or ()) + self._diagnostic = diagnostic @property def order_number(self) -> int: @@ -952,6 +957,11 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.level.severity + @property + def diagnostic(self) -> bool: + """Whether this check reports advisory diagnostics instead of conformance issues.""" + return self._diagnostic + @property def overridden_by(self) -> list[RequirementCheck]: overridden_by = [] @@ -1009,6 +1019,7 @@ def to_dict(self, with_requirement: bool = True, with_profile: bool = True) -> d "name": self.name, "description": self.description, "severity": self.severity.name, + "diagnostic": self.diagnostic, "depends_on": list(self.depends_on), } if with_requirement: diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 03a4effc3..3554df48d 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -194,6 +194,43 @@ def to_json( ) +class ValidationDiagnostic: + """A non-blocking diagnostic produced by an optional heuristic check.""" + + def __init__( + self, + check: RequirementCheck, + message: str, + context: ValidationContext, + *, + violating_entity: str | None = None, + violating_property: str | None = None, + value: str | None = None, + ): + self.check = check + self.message = message + self.identifier = context.effective_check_identifier(check) + self.violating_entity = violating_entity + self.violating_property = violating_property + self.value = value + + def to_dict(self) -> dict[str, str | None]: + return { + "check": self.identifier, + "message": self.message, + "violatingEntity": self.violating_entity, + "violatingProperty": self.violating_property, + "violatingPropertyValue": self.value, + } + + def __repr__(self) -> str: + return f"ValidationDiagnostic(check={self.identifier!r}, message={self.message!r})" + + def __eq__(self, other: object) -> bool: + return isinstance(other, ValidationDiagnostic) and self.to_dict() == other.to_dict() + + __hash__ = None # type: ignore[assignment] + class ValidationResult: """ Represents the result of a validation. @@ -219,6 +256,7 @@ def __init__(self, context: ValidationContext): self._validation_settings: ValidationSettings = context.settings # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] + self._diagnostics: list[ValidationDiagnostic] = [] # keep track of the checks that have been executed self._executed_checks: set[RequirementCheck] = set() self._executed_checks_results: dict[str, bool] = {} @@ -373,6 +411,30 @@ def issues(self) -> list[CheckIssue]: """ return self._issues.copy() + @property + def diagnostics(self) -> list[ValidationDiagnostic]: + """Non-blocking heuristic findings, separate from conformance issues.""" + return self._diagnostics.copy() + + def add_diagnostic( + self, + check: RequirementCheck, + message: str, + violating_entity: str | None = None, + violating_property: str | None = None, + value: str | None = None, + ) -> ValidationDiagnostic: + diagnostic = ValidationDiagnostic( + check, + message, + self.context, + violating_entity=violating_entity, + violating_property=violating_property, + value=value, + ) + self._diagnostics.append(diagnostic) + return diagnostic + def get_issues(self, min_severity: Severity | None = None) -> list[CheckIssue]: """ Get the issues found during the validation with a severity greater than or equal to `min_severity` @@ -416,10 +478,12 @@ def add_issue( Parameters: message(str): The message of the issue check(RequirementCheck): The check that generated the issue - violatingEntity(Optional[str]): The entity that caused the issue (if any) - violatingProperty(Optional[str]): The property that caused the issue (if any) - violatingPropertyValue(Optional[str]): The value of the violatingProperty (if any) + violatingEntity(Optional[str]): The entity that caused the issue (if any) + violatingProperty(Optional[str]): The property that caused the issue (if any) + violatingPropertyValue(Optional[str]): The value of the violatingProperty (if any) """ + if check.diagnostic: + raise ValueError(f"Diagnostic check '{check.name}' cannot add a conformance issue") c = CheckIssue( check, message, @@ -474,9 +538,9 @@ def __repr__(self): def __eq__(self, other: object) -> bool: if not isinstance(other, ValidationResult): raise TypeError(f"Cannot compare ValidationResult with {type(other)}") - return self._issues == other._issues + return self._issues == other._issues and self._diagnostics == other._diagnostics - # Equality is based on the mutable list of issues, so instances are + # Equality is based on mutable issue and diagnostic lists, so instances are # intentionally unhashable (a content-based hash would be unstable). __hash__ = None # type: ignore[assignment] @@ -498,6 +562,7 @@ def to_dict(self) -> dict: "validation_settings": validation_settings, "passed": self.passed(cast("Severity", self.context.settings.requirement_severity)), "issues": [issue.to_dict() for issue in self.issues], + "diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics], "skipped_checks": self.skipped_checks_count, "skipped_check_details": [detail.to_dict() for detail in self.skipped_check_details], } diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index 43be3a660..127ae656d 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -88,6 +88,8 @@ class ValidationSettings: disable_check_for_duplicates: bool = False #: Checks to skip skip_checks: list[str] | None = None + #: Run checks classified as diagnostic, without affecting conformance + include_diagnostic_checks: bool = False #: Flag to validate only the metadata of the RO-Crate metadata_only: bool = False #: RO-Crate metadata as dictionary diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index 281b567cd..bd79f6733 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -211,6 +211,25 @@ def total_checks(self) -> int: """ return len(self._stats.get("checks", [])) + @property + def diagnostic_checks(self) -> list[RequirementCheck]: + """Get all diagnostic checks available to this validation.""" + return self._stats.get("diagnostic_checks", []) + + @property + def executed_diagnostic_checks(self) -> list[RequirementCheck]: + """Get diagnostic checks that ran, regardless of their findings.""" + return self._stats.get("executed_diagnostic_checks", []) + + @property + def skipped_diagnostic_checks(self) -> list[RequirementCheck]: + """Get diagnostic checks that were disabled or skipped.""" + return self._stats.get("skipped_diagnostic_checks", []) + + @property + def total_diagnostic_checks(self) -> int: + return len(self.diagnostic_checks) + @property def validated_profiles(self) -> list[Profile]: """ @@ -289,7 +308,8 @@ def __collect_requirement_checks__( requirement_checks = [ _ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) - if not _.overridden or _.requirement.profile.identifier == target_profile_identifier + if not _.diagnostic + and (not _.overridden or _.requirement.profile.identifier == target_profile_identifier) ] num_checks = len(requirement_checks) requirement_checks_count += num_checks @@ -332,6 +352,7 @@ def __initialise__(cls, validation_settings: ValidationSettings): # Initialize the counters checks_by_severity: dict[Severity, set[RequirementCheck]] = {} checks: set[RequirementCheck] = set() + diagnostic_checks: set[RequirementCheck] = set() requirements: set[Requirement] = set() # Initialize the counters @@ -354,6 +375,13 @@ def __initialise__(cls, validation_settings: ValidationSettings): if requirement.hidden: continue + diagnostic_checks.update( + check + for check in requirement.get_checks() + if check.diagnostic + and (not check.overridden or check.requirement.profile.identifier == target_profile_identifier) + ) + requirement_checks_count = cls.__collect_requirement_checks__( requirement, severity_validation=severity_validation, @@ -365,6 +393,8 @@ def __initialise__(cls, validation_settings: ValidationSettings): # count the requirements and checks if requirement_checks_count == 0: logger.debug(f"No checks for requirement: {requirement}") + elif requirement.overridden: + logger.debug(f"Requirement is fully overridden and is not counted: {requirement}") else: # Only if there are checks for the requirement count it logger.debug(f"Requirement: {requirement} checks count: {requirement_checks_count}") @@ -385,6 +415,7 @@ def __initialise__(cls, validation_settings: ValidationSettings): "profiles": profiles, "requirements": requirements, "checks": checks, + "diagnostic_checks": diagnostic_checks, "severity": severity_validation, "checks_by_severity": checks_by_severity, "failed_requirements": [], @@ -397,6 +428,8 @@ def __initialise__(cls, validation_settings: ValidationSettings): "validated_profiles": [], "validated_requirements": [], "validated_checks": [], + "executed_diagnostic_checks": [], + "skipped_diagnostic_checks": [], } logger.debug(result) return result @@ -444,6 +477,19 @@ def __record_check_status__(self, check: RequirementCheck, result: CheckResult) def __handle_requirement_check_validation_end__(self, event: Event, ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementCheckValidationEvent) assert ctx is not None + if event.requirement_check in self.diagnostic_checks: + if event.validation_result is not None: + for key in ("executed_diagnostic_checks", "skipped_diagnostic_checks"): + self._stats[key] = [ + check + for check in self._stats[key] + if check.identifier != event.requirement_check.identifier + ] + result = normalize_check_result(event.validation_result) + status = "skipped_diagnostic_checks" if result is CheckResult.SKIPPED else "executed_diagnostic_checks" + self._stats[status].append(event.requirement_check) + self.notify_listeners() + return if event.requirement_check in self.checks: if event.validation_result is not None: result = normalize_check_result(event.validation_result) @@ -521,6 +567,13 @@ def to_dict(self) -> dict: "total_failed_checks": len(self.failed_checks), "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, + "diagnostic_checks": { + "count": self.total_diagnostic_checks, + "executed": len(self.executed_diagnostic_checks), + "skipped": len(self.skipped_diagnostic_checks), + "findings": len(self.validation_result.diagnostics) if self.validation_result else 0, + "identifiers": sorted(check.identifier for check in self.diagnostic_checks), + }, # Requirements involved "requirements": { "count": self.total_requirements, @@ -619,6 +672,16 @@ def to_dict(self) -> dict: "total_failed_checks": len(self.failed_checks), "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, + "diagnostic_checks": { + "count": sum(stats.total_diagnostic_checks for stats in self._statistics_list), + "executed": sum(len(stats.executed_diagnostic_checks) for stats in self._statistics_list), + "skipped": sum(len(stats.skipped_diagnostic_checks) for stats in self._statistics_list), + "findings": sum( + len(stats.validation_result.diagnostics) + for stats in self._statistics_list + if stats.validation_result is not None + ), + }, # Requirements involved "requirements": { "count": self.total_requirements, diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index 4a50b1bca..b7f4c2873 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -744,12 +744,20 @@ def is_check_skipped(self, check: RequirementCheck) -> bool: while exposing a target-local effective identifier. Accepting either identity lets callers address the check without mutating its provenance. """ + if check.diagnostic and not self.settings.include_diagnostic_checks: + return True skipped_identifiers = self.settings.skip_checks if not skipped_identifiers: return False check_identifiers = {check.identifier, self.effective_check_identifier(check)} return bool(check_identifiers.intersection(skipped_identifiers)) + def check_skip_reason(self, check: RequirementCheck) -> str: + """Describe why validation settings exclude a check.""" + if check.diagnostic and not self.settings.include_diagnostic_checks: + return "diagnostic checks are disabled; use --include-diagnostic-checks to enable them" + return "Check was skipped by validation settings" + def effective_check_replacement(self, check: RequirementCheck) -> RequirementCheck | None: """ Return the target-local check replacing ``check``, if any. From e3ef940b1a07ce95c9fa86358114d619f0b67aa5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 09:53:39 +0200 Subject: [PATCH 211/271] =?UTF-8?q?feat(validation):=20=F0=9F=A9=BA=20expo?= =?UTF-8?q?se=20opt-in=20diagnostic=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 18 ++++++ .../1.2/must/0_file_descriptor_format.py | 24 +++++--- .../requirements/python/__init__.py | 8 +++ .../io_helpers/output/json/formatters.py | 2 + .../io_helpers/output/text/formatters.py | 55 +++++++++++++++++++ 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 084bb841f..001d45ec8 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -36,6 +36,7 @@ from rocrate_validator.utils.io_helpers.output.console import Console from rocrate_validator.utils.io_helpers.output.json import JSONOutputFormatter from rocrate_validator.utils.io_helpers.output.text import TextOutputFormatter +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationDiagnosticsTextOutputFormatter from rocrate_validator.utils.io_helpers.output.text.layout.report import LiveTextProgressLayout, get_app_header_rule from rocrate_validator.utils.paths import get_profiles_path from rocrate_validator.utils.uri import validate_rocrate_uri @@ -100,6 +101,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True, ) +@click.option( + "--include-diagnostic-checks", + is_flag=True, + help="Run all checks marked as diagnostic; findings do not affect conformance", + default=False, + show_default=True, +) @click.option( "--profiles-path", type=click.Path(exists=True), @@ -275,6 +283,7 @@ def validate( creation_time: bool = False, enforce_availability: bool = False, skip_availability_check: bool = False, + include_diagnostic_checks: bool = False, no_auto_profile: bool = False, disable_profile_inheritance: bool = False, no_profile_checks: bool = False, @@ -360,6 +369,7 @@ def validate( "creation_time": creation_time, "enforce_availability": enforce_availability, "skip_availability_check": skip_availability_check, + "include_diagnostic_checks": include_diagnostic_checks, "validate_profile_checks": not no_profile_checks, } @@ -613,11 +623,17 @@ def _render_console_result( ) if verbose_choice == "y" or verbose: command_view.display_validation_result(result) + elif result.diagnostics: + console.print(ValidationDiagnosticsTextOutputFormatter(result)) + elif result.diagnostics: + console.print(ValidationDiagnosticsTextOutputFormatter(result)) return result result = services.validate(validation_settings) console.register_formatter(TextOutputFormatter()) console.print(result.statistics) + if result.diagnostics: + console.print(ValidationDiagnosticsTextOutputFormatter(result)) if not result.passed() and verbose: out = Console(no_color=console.no_color, width=console.width, height=console.height) out.register_formatter(TextOutputFormatter()) @@ -660,6 +676,8 @@ def _render_file_or_collected_result( out = Console(color_system=None, width=output_line_width, height=31, file=f) out.register_formatter(TextOutputFormatter()) out.print(result.statistics) # Output the statistics first + if result.diagnostics: + out.print(ValidationDiagnosticsTextOutputFormatter(result)) if not result.passed() and verbose: out.print(result) if interactive: diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 749d258a8..b73e98c84 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -520,7 +520,10 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke """ if isinstance(value, str): if key not in _LITERAL_KEYS and value in identifiers: - return f"Entity '{entity_id}' references '{value}' as a string; use {{\"@id\": \"{value}\"}}" + return ( + "The string value matches a declared entity @id; verify whether this property " + "should use an @id reference" + ) return None if isinstance(value, list): children: Any = value @@ -537,8 +540,9 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke return None @check( - name="Validation of entity references", + name="Heuristic diagnostics for suspicious entity references", depends_on=("File Descriptor JSON format",), + diagnostic=True, ) def check_entity_references(self, context: ValidationContext) -> CheckResultValue: try: @@ -553,8 +557,13 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu continue message = self._find_reference_issue(value, entity_id, identifiers, key) if message: - context.result.add_issue(message, self) - return False + context.result.add_diagnostic( + self, + message, + violating_entity=entity_id, + violating_property=key, + value=value if isinstance(value, str) else None, + ) return True except ROCrateMetadataNotFoundError: logger.debug("Skipping entity reference check: metadata descriptor is not available") @@ -563,10 +572,11 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu except UnicodeDecodeError: context.record_skip(self, "descriptor encoding check reported the failure", "exception") return CheckResult.SKIPPED - except _HANDLED_METADATA_ERRORS: + except _HANDLED_METADATA_ERRORS as error: if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False + logger.exception("Unable to inspect metadata for heuristic diagnostics") + context.record_skip(self, f"unable to inspect metadata for heuristic diagnostics: {error}", "exception") + return CheckResult.SKIPPED @check( name="Validation of subject and keyword properties", diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 091a70f7b..4146aae7a 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -55,6 +55,7 @@ def __init__( level: RequirementLevel | None = LevelCollection.REQUIRED, deactivated: bool = False, depends_on: tuple[str, ...] | None = None, + diagnostic: bool = False, ): """ check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. @@ -66,6 +67,7 @@ def __init__( level=level, deactivated=deactivated, depends_on=tuple(getattr(check_function, "depends_on", depends_on or ())), + diagnostic=diagnostic, ) sig = inspect.signature(check_function) @@ -181,6 +183,7 @@ def __init_checks__(self): severity = self.severity_from_path or Severity.REQUIRED logger.debug("Severity log: %r", severity) deactivated = bool(getattr(member, "deactivated", False)) + diagnostic = bool(getattr(member, "diagnostic", False)) # pylint: disable-next=redefined-outer-name # local 'check' mirrors the decorator name check = self.requirement_check_class( self, @@ -189,6 +192,7 @@ def __init_checks__(self): description=check_description, level=LevelCollection.get(severity.name) if severity else None, deactivated=deactivated, + diagnostic=diagnostic, ) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -235,6 +239,7 @@ def check( severity: Severity | None = None, deactivated: bool = False, depends_on: tuple[str, ...] | None = None, + diagnostic: bool = False, ): """ A decorator to mark a function as a check. @@ -261,6 +266,8 @@ def check( :type deactivated: bool :param depends_on: names of checks that must run before this check. :type depends_on: Optional[Tuple[str, ...]] + :param diagnostic: mark this as an advisory check that does not determine conformance. + :type diagnostic: bool :return: the decorated function :rtype: Callable """ @@ -296,6 +303,7 @@ def decorator(func): func.severity = severity func.deactivated = deactivated func.depends_on = depends_on or () + func.diagnostic = diagnostic return func return decorator diff --git a/rocrate_validator/utils/io_helpers/output/json/formatters.py b/rocrate_validator/utils/io_helpers/output/json/formatters.py index 842098e44..1fb2be2df 100644 --- a/rocrate_validator/utils/io_helpers/output/json/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/json/formatters.py @@ -73,6 +73,7 @@ def format_validation_results( json_output["passed"] = True json_output["skipped_checks"] = 0 json_output["skipped_check_details"] = [] + json_output["diagnostics"] = [] # Initialize the profile results dictionary results_key = "validation_results_by_profile" @@ -98,6 +99,7 @@ def format_validation_results( if "issues" not in json_output: json_output["issues"] = [] json_output["issues"].extend(result_dict.get("issues", [])) + json_output["diagnostics"].extend(result_dict.get("diagnostics", [])) # Add overall statistics stats_dict = _compute_overall_statistics(results, verbose=verbose) diff --git a/rocrate_validator/utils/io_helpers/output/text/formatters.py b/rocrate_validator/utils/io_helpers/output/text/formatters.py index a922f2173..800b079cf 100644 --- a/rocrate_validator/utils/io_helpers/output/text/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/text/formatters.py @@ -19,6 +19,8 @@ from rich.align import Align from rich.markdown import Markdown from rich.padding import Padding +from rich.table import Table +from rich.text import Text from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.colors import get_severity_color @@ -95,6 +97,49 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR yield Padding("\n", (0, 0)) yield Padding("\n", (0, 0)) + yield from self._render_diagnostics(result) + + @staticmethod + def _render_diagnostics(result: ValidationResult) -> RenderResult: + diagnostics = result.diagnostics + if not diagnostics: + return + finding_count = len(diagnostics) + finding_label = "finding" if finding_count == 1 else "findings" + check_count = len({diagnostic.identifier for diagnostic in diagnostics}) + caption = Text(f"{finding_count} {finding_label}: ") + if finding_count == 1: + caption.append("this finding is advisory and does not affect conformance.", style="dim italic") + else: + caption.append("findings are advisory and do not affect conformance.", style="dim italic") + table = Table( + title=f"Diagnostic Checks ({check_count})", + title_style="bold orange1", + border_style="orange1", + header_style="bold orange1", + show_lines=False, + caption=caption, + caption_justify="center", + ) + table.add_column("Profile", style="cyan", no_wrap=True) + table.add_column("Check", no_wrap=True) + table.add_column("Entity", style="cyan", overflow="fold") + table.add_column("Property", style="yellow", overflow="fold") + table.add_column("Value", style="green", overflow="fold") + table.add_column("Finding", overflow="fold") + + for diagnostic in diagnostics: + table.add_row( + Text(result.context.effective_check_profile(diagnostic.check).identifier, style="cyan"), + Text(diagnostic.identifier, style=get_severity_color(diagnostic.check.severity)), + diagnostic.violating_entity or "—", + diagnostic.violating_property or "—", + f'"{diagnostic.value}"' if diagnostic.value is not None else "—", + Text(diagnostic.message), + ) + + yield Padding(table, (1, 2)) + class ValidationStatisticsTextOutputFormatter(OutputFormatter): def __init__(self, validation_statistics: ValidationStatistics): @@ -110,3 +155,13 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR if layout.layout is not None: yield layout.layout yield Padding("\n", (0, 0)) + + +class ValidationDiagnosticsTextOutputFormatter(OutputFormatter): + """Render advisory diagnostics independently from conformance details.""" + + def __init__(self, validation_result: ValidationResult): + self._validation_result = validation_result + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield from ValidationResultTextOutputFormatter._render_diagnostics(self._validation_result) From b39b8e3765e137348be2c0baa64d44f906041153 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 09:54:49 +0200 Subject: [PATCH 212/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20opt-in=20diagnostic=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/test_file_descriptor_format.py | 160 ++++++++++++++++++ tests/test_cli.py | 25 +++ tests/unit/requirements/test_profiles.py | 16 ++ tests/unit/test_validation_settings.py | 1 + tests/unit/test_validation_statistics.py | 27 ++- 5 files changed, 228 insertions(+), 1 deletion(-) diff --git a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py index 98036267b..75e8f415d 100644 --- a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py @@ -12,9 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import shutil +from pathlib import Path + +import pytest +from click.testing import CliRunner from rocrate_validator import models, services +from rocrate_validator.cli.main import cli +from rocrate_validator.utils.io_helpers.output.console import Console +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import InvalidFileDescriptor, ValidROC from tests.shared import do_entity_test @@ -26,6 +35,157 @@ paths = InvalidFileDescriptor() +def _crate_with_suspicious_string_reference(tmp_path: Path, version: str) -> Path: + source = ( + Path(__file__).resolve().parents[3] + / "data" + / "crates" + / f"rocrate-{version}" + / "7_root_data_entity" + / "recommended_reference_type_for_licence" + / "valid" + ) + crate = tmp_path / f"rocrate-{version}" + shutil.copytree(source, crate) + metadata_path = crate / "ro-crate-metadata.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + license_id = "https://creativecommons.org/licenses/by/4.0/" + root = next(entity for entity in metadata["@graph"] if entity.get("@id") == "./") + root["license"] = license_id + root["customReference"] = license_id + metadata["@context"] = [ + metadata["@context"], + {"customReference": "https://example.org/vocabulary/customReference"}, + ] + metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") + return crate + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str): + crate = _crate_with_suspicious_string_reference(tmp_path, version) + settings = models.ValidationSettings( + rocrate_uri=models.URI(crate), + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + + default_result = services.validate(settings) + assert default_result.passed() + assert default_result.diagnostics == [] + heuristic_check = next( + check + for check in default_result.statistics.diagnostic_checks + if "suspicious entity references" in check.name.lower() + ) + assert heuristic_check.severity == models.Severity.REQUIRED + assert heuristic_check.diagnostic + assert heuristic_check not in default_result.statistics.checks + conformance_check_count = default_result.statistics.total_checks + assert default_result.get_check_result(heuristic_check) is models.CheckResult.SKIPPED + + settings.include_diagnostic_checks = True + diagnostic_result = services.validate(settings) + assert diagnostic_result.passed() + assert diagnostic_result.statistics.total_checks == conformance_check_count + assert diagnostic_result.statistics.executed_diagnostic_checks == [heuristic_check] + assert len(diagnostic_result.diagnostics) >= 2, ( + diagnostic_result.validation_settings.include_diagnostic_checks, + [ + (check.name, diagnostic_result.get_check_result(check)) + for check in diagnostic_result.statistics.checks + if "suspicious entity references" in check.name.lower() + ], + [detail.message for detail in diagnostic_result.skipped_check_details], + ) + assert {diagnostic.violating_property for diagnostic in diagnostic_result.diagnostics} >= { + "license", + "customReference", + } + assert diagnostic_result.to_dict()["diagnostics"] + + console = Console(record=True, color_system=None, width=120) + console.print(ValidationResultTextOutputFormatter(diagnostic_result)) + text_report = console.export_text() + assert "Diagnostic Checks (1)" in text_report + assert "customReference" in text_report + + cli_result = CliRunner().invoke( + cli, + [ + "--no-interactive", + "validate", + str(crate), + "--profile-identifier", + f"ro-crate-{version}", + "--include-diagnostic-checks", + "--output-format", + "json", + "--no-paging", + ], + ) + assert cli_result.exit_code == 0, cli_result.output + cli_report = json.loads(cli_result.output) + assert cli_report["passed"] + assert cli_report["diagnostics"] + + text_cli_result = CliRunner().invoke( + cli, + [ + "--no-interactive", + "validate", + str(crate), + "--profile-identifier", + f"ro-crate-{version}", + "--include-diagnostic-checks", + "--no-paging", + ], + ) + assert text_cli_result.exit_code == 0, text_cli_result.output + assert "Diagnostic Checks (1)" in text_cli_result.output + assert "customReference" in text_cli_result.output + + skipped_result = CliRunner().invoke( + cli, + [ + "--no-interactive", + "validate", + str(crate), + "--profile-identifier", + f"ro-crate-{version}", + "--include-diagnostic-checks", + "--skip-checks", + heuristic_check.identifier, + "--output-format", + "json", + "--no-paging", + ], + ) + assert skipped_result.exit_code == 0, skipped_result.output + skipped_report = json.loads(skipped_result.output) + assert skipped_report["diagnostics"] == [] + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +def test_required_about_reference_violation_is_detected_without_heuristic(tmp_path: Path, version: str): + crate = _crate_with_suspicious_string_reference(tmp_path, version) + metadata_path = crate / "ro-crate-metadata.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + descriptor = next(entity for entity in metadata["@graph"] if entity.get("@id") == "ro-crate-metadata.json") + descriptor["about"] = {"@id": "./missing"} + metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") + + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(crate), + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + ) + assert not result.passed() + assert result.diagnostics == [] + + def test_missing_file_descriptor(): """Test a RO-Crate without a file descriptor.""" rocrate_path = paths.missing_file_descriptor diff --git a/tests/test_cli.py b/tests/test_cli.py index 08eab49ce..1e5071f54 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -218,6 +218,31 @@ def mock_validate(*args, **kwargs): ) +def test_validate_include_diagnostic_checks_option(cli_runner: CliRunner): + called_args: list = [] + + def mock_validate(*args, **kwargs): + called_args.extend(args) + + with patch("rocrate_validator.cli.commands.validate.services.validate") as mock_validate_rocrate: + mock_validate_rocrate.return_value = None + mock_validate_rocrate.side_effect = mock_validate + result = cli_runner.invoke( + cli, + [ + "--no-interactive", + "validate", + str(ValidROC().sort_and_change_archive), + "--include-diagnostic-checks", + "--no-paging", + ], + ) + + assert result.exit_code == 2 + settings = called_args[0] + assert settings["include_diagnostic_checks"] is True + + def test_validate_output_file_text_report(cli_runner: CliRunner, tmp_path: Path): output_file = tmp_path / "report.txt" result = cli_runner.invoke( diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 5f7f760b8..4e123d5c5 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -975,6 +975,22 @@ def dependent(self, ctx): assert dependent.depends_on == ("base",) # pyright: ignore[reportFunctionMemberAccess] +def test_python_check_decorator_sets_diagnostic_flag(): + """The @check decorator must expose the diagnostic classification.""" + from rocrate_validator.requirements.python import check + + @check(name="advisory", diagnostic=True) + def advisory(self, ctx): + return True + + @check(name="conformance") + def conformance(self, ctx): + return True + + assert advisory.diagnostic is True # pyright: ignore[reportFunctionMemberAccess] + assert conformance.diagnostic is False # pyright: ignore[reportFunctionMemberAccess] + + def test_shacl_shape_with_deactivated_marks_check_skipped(fake_profiles_path: str): """A child profile that overrides an inherited NodeShape by `sh:name` and sets `sh:deactivated true` should produce a check whose `deactivated` diff --git a/tests/unit/test_validation_settings.py b/tests/unit/test_validation_settings.py index 51a7ab1d5..debe2b646 100644 --- a/tests/unit/test_validation_settings.py +++ b/tests/unit/test_validation_settings.py @@ -70,6 +70,7 @@ def test_validation_settings_to_dict(): assert settings_dict["profiles_path"] == "/path/to/profiles" assert settings_dict["requirement_severity"] == Severity.RECOMMENDED assert settings_dict["enable_profile_inheritance"] is False + assert settings_dict["include_diagnostic_checks"] is False def test_validation_settings_enable_profile_inheritance(): diff --git a/tests/unit/test_validation_statistics.py b/tests/unit/test_validation_statistics.py index 91476a9bc..d18133013 100644 --- a/tests/unit/test_validation_statistics.py +++ b/tests/unit/test_validation_statistics.py @@ -19,7 +19,14 @@ from rocrate_validator import services from rocrate_validator.events import EventType, Subscriber -from rocrate_validator.models import CheckResult, SkipCategory, ValidationContext, ValidationSettings, Validator +from rocrate_validator.models import ( + CheckResult, + SkipCategory, + ValidationContext, + ValidationSettings, + ValidationStatistics, + Validator, +) from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.validator import SHACLValidator from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter @@ -107,6 +114,24 @@ def update(self, event, ctx=None): self.snapshots.append([(task.completed, task.total) for task in self.progress.tasks]) +def test_fully_overridden_requirements_are_not_counted(tmp_path: Path): + settings = ValidationSettings( + rocrate_uri=tmp_path, + profile_identifier="ro-crate-1.2", + ) + statistics = ValidationStatistics(settings) + overridden_requirements = [ + requirement + for profile in statistics.profiles + for requirement in profile.get_requirements(settings.requirement_severity) + if requirement.overridden + ] + + assert overridden_requirements + assert all(not requirement.overridden for requirement in statistics.requirements) + assert statistics.total_requirements == len(statistics.requirements) + + @pytest.mark.parametrize("profile", ["overlay", "wrapper"]) def test_deferred_checks_are_not_reported_as_skipped(statistics_settings, profile): statistics_settings.profile_identifier = profile From 2f4f97c5c153dcb96829021808f732fe82965ce8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 10:04:45 +0200 Subject: [PATCH 213/271] =?UTF-8?q?docs(validation):=20=F0=9F=93=9D=20expl?= =?UTF-8?q?ain=20diagnostic=20check=20usage=20and=20authoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/11_writing_a_profile.rst | 34 ++++++++++++++++++++++++++++++++++ docs/2_usage_cli.rst | 18 ++++++++++++++++++ docs/3_usage_api.rst | 24 ++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 5e956f8db..d1bbdcf9b 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -184,6 +184,40 @@ result contains an actionable diagnostic. When it is skipped, prefer ``context.record_skip(...)`` when a reason and category should be exposed to API and CLI consumers. +Diagnostic Python checks +~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``diagnostic=True`` argument to ``@check`` for advisory checks that +report suspicious patterns without deciding whether a crate conforms to its +profile. Diagnostic checks are disabled by default and can be enabled by users +with ``--include-diagnostic-checks`` or the +``ValidationSettings.include_diagnostic_checks`` setting. A diagnostic check +must record each finding with ``context.result.add_diagnostic(...)``; do not +use ``add_issue(...)`` for an advisory finding, because issues contribute to +the conformance result. + +For example: + +.. code-block:: python + + @check(name="Possible missing entity link", diagnostic=True) + def check_entity_link(self, context: ValidationContext) -> bool: + entity = ... # inspect the entity using the validation context + if suspicious_link(entity): + context.result.add_diagnostic( + self, + "This value may refer to an entity that is not declared in the crate.", + violating_entity=entity.get("@id"), + violating_property="relatedLink", + ) + return True + +The return value still follows the regular Python check contract, but a +diagnostic check's failed status does not fail conformance. Findings are +available from ``ValidationResult.diagnostics`` and are presented separately +from conformance issues. For the user-facing options and result fields, see +:ref:`cli-based-validation` and :ref:`programmatic-validation`. + .. _profile-composition: Composing validation profiles diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 4169efce3..2cd95622e 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -34,6 +34,24 @@ CLI Validation :start-after: ## CLI-based Validation :end-before: ## Programmatic Validation + +Diagnostic Checks +----------------- + +Diagnostic checks are advisory checks that can report suspicious metadata +patterns without affecting whether an RO-Crate conforms to its profile. They +are disabled by default. To run all checks classified as diagnostic, pass +``--include-diagnostic-checks`` to ``validate``: + +.. code-block:: console + + rocrate-validator validate --include-diagnostic-checks + +To omit one diagnostic check while running the others, combine this option with +``--skip-checks`` and the check's fully qualified identifier. The validation +report lists diagnostic checks and their findings separately from conformance +issues. + .. seealso:: To validate without network access and manage the HTTP cache from the diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 673f6ec27..1352a8b7f 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -30,6 +30,30 @@ Programmatic Validation :start-after: ## Programmatic Validation :end-before: ## Running the tests + +Diagnostic Checks +----------------- + +Diagnostic checks are advisory and do not affect the conformance result. They +are disabled by default; set ``include_diagnostic_checks`` on +``ValidationSettings`` to run them: + +.. code-block:: python + + from rocrate_validator import models, services + + settings = services.ValidationSettings( + rocrate_uri="path/to/rocrate", + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.REQUIRED, + include_diagnostic_checks=True, + ) + result = services.validate(settings) + +The resulting ``ValidationResult`` exposes these reports separately through +``result.diagnostics``. Diagnostic checks are also listed separately in +``result.statistics``. + .. seealso:: To resolve resources from a local cache or run validation without network From d88a1ce841956a9a3df5f2049397b257d13b6672 Mon Sep 17 00:00:00 2001 From: EttoreM Date: Tue, 13 Oct 2026 10:53:57 +0100 Subject: [PATCH 214/271] Added SHACL constraint to che for the existence of an entuty of type CreateAction that has mainEntity as instrument. --- .../must/4_create_action.ttl | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl diff --git a/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl b/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl new file mode 100644 index 000000000..40f6556fd --- /dev/null +++ b/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl @@ -0,0 +1,38 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix dct: . +@prefix ro-crate: . +@prefix schema: . +@prefix bioschemas: . +@prefix bioschemas-cw: . +@prefix sh: . +@prefix workflow-run-crate: . + + +workflow-run-crate:CreateActionWithMainEntityAsInstrumentRequired a sh:NodeShape ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + sh:name "Main entity execution" ; + sh:path ( + schema:mainEntity + [ sh:inversePath schema:instrument ] + ) ; + sh:qualifiedValueShape [ + sh:class schema:CreateAction ; + ] ; + sh:qualifiedMinCount 1 ; + sh:message "At least one CreateAction MUST reference the root data entity's mainEntity as its instrument." ; + ] . From 9d7d4b763f8e0db550922132f011837464df525c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 13:07:04 +0200 Subject: [PATCH 215/271] =?UTF-8?q?fix(cli):=20=F0=9F=90=9B=20avoid=20dupl?= =?UTF-8?q?icate=20diagnostic=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 001d45ec8..c5218ff3d 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -632,7 +632,7 @@ def _render_console_result( result = services.validate(validation_settings) console.register_formatter(TextOutputFormatter()) console.print(result.statistics) - if result.diagnostics: + if result.diagnostics and (result.passed() or not verbose): console.print(ValidationDiagnosticsTextOutputFormatter(result)) if not result.passed() and verbose: out = Console(no_color=console.no_color, width=console.width, height=console.height) @@ -676,7 +676,7 @@ def _render_file_or_collected_result( out = Console(color_system=None, width=output_line_width, height=31, file=f) out.register_formatter(TextOutputFormatter()) out.print(result.statistics) # Output the statistics first - if result.diagnostics: + if result.diagnostics and (result.passed() or not verbose): out.print(ValidationDiagnosticsTextOutputFormatter(result)) if not result.passed() and verbose: out.print(result) From 9374cac37ba5a207c4defb8ab0df2fe9e501232c Mon Sep 17 00:00:00 2001 From: EttoreM Date: Tue, 13 Oct 2026 12:22:56 +0100 Subject: [PATCH 216/271] Added python test to test new SHACL constraint about CrateAction --- .../test_wfrc_create_action.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py diff --git a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py new file mode 100644 index 000000000..7574da850 --- /dev/null +++ b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py @@ -0,0 +1,72 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.models import Severity +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER +from tests.ro_crates import ValidROC +from tests.shared import do_entity_test + + +EXPECTED_ISSUE = "At least one CreateAction MUST reference the root data entity's mainEntity as its instrument." + + +def test_wfrc_no_create_action(): + """A crate without any CreateAction must fail the main entity execution check.""" + do_entity_test( + ValidROC().workflow_run_crate, + Severity.REQUIRED, + False, + expected_triggered_issues=[EXPECTED_ISSUE], + profile_identifier="workflow-run-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_mod_sparql=""" + PREFIX schema: + DELETE { ?action ?predicate ?value . } + WHERE { + ?action a schema:CreateAction ; + ?predicate ?value . + } + """, + ) + + +def test_wfrc_no_create_action_with_main_entity_as_instrument(): + """A valid instrument pointing to another tool cannot satisfy main entity execution.""" + do_entity_test( + ValidROC().workflow_run_crate, + Severity.REQUIRED, + False, + expected_triggered_issues=[EXPECTED_ISSUE], + profile_identifier="workflow-run-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_mod_sparql=""" + PREFIX schema: + DELETE { ?action schema:instrument ?workflow . } + INSERT { + ?action schema:instrument . + a schema:SoftwareApplication ; + schema:name "Another tool" . + } + WHERE { + ?root schema:mainEntity ?workflow . + ?metadata schema:about ?root . + FILTER( + STRENDS(STR(?metadata), "ro-crate-metadata.json") || + STRENDS(STR(?metadata), "ro-crate-metadata.jsonld") + ) + ?action a schema:CreateAction ; + schema:instrument ?workflow . + } + """, + ) From 0282206a416c3884e38fdd669d87ffafca038818 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 15:29:28 +0200 Subject: [PATCH 217/271] =?UTF-8?q?feat(models):=20=F0=9F=94=8E=20add=20st?= =?UTF-8?q?ructured=20profile=20detection=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/__init__.py | 3 + rocrate_validator/models/profile_detection.py | 205 ++++++++++++++++++ rocrate_validator/models/validation.py | 54 ++--- 3 files changed, 223 insertions(+), 39 deletions(-) create mode 100644 rocrate_validator/models/profile_detection.py diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index fb64fc324..bf8fee092 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -27,6 +27,7 @@ ProfileCheckResult, ProfileCheckSuite, ) +from rocrate_validator.models.profile_detection import ProfileDetectionDiagnostic, ProfileDetectionResult from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck, RequirementCheckRelation from rocrate_validator.models.requirement import ( Requirement, @@ -73,6 +74,8 @@ "ProfileCheckFailure", "ProfileCheckResult", "ProfileCheckSuite", + "ProfileDetectionDiagnostic", + "ProfileDetectionResult", "ProfileValidationEvent", "Requirement", "RequirementCheck", diff --git a/rocrate_validator/models/profile_detection.py b/rocrate_validator/models/profile_detection.py new file mode 100644 index 000000000..7097b7cab --- /dev/null +++ b/rocrate_validator/models/profile_detection.py @@ -0,0 +1,205 @@ +"""Structured results returned by automatic profile detection.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Callable, Sequence + +from rocrate_validator.models._logging import logger +from rocrate_validator.models.profile import Profile +from rocrate_validator.rocrate.entity import ROCrateEntity + +if TYPE_CHECKING: + from rocrate_validator.models.validation import ValidationContext + + +@dataclass(frozen=True) +class ProfileDetectionDiagnostic: + """Explain a metadata value that could not be used for profile detection.""" + + code: str + property: str | None + location: str | None + value: Any + message: str + + +@dataclass +class ProfileDetectionResult: + """Detected profiles, diagnostics, and an optional user-confirmable suggestion.""" + + profiles: list[Profile] + diagnostics: list[ProfileDetectionDiagnostic] + suggested_profile: Profile | None = None + + +def detect_rocrate_profiles( + context: ValidationContext, + profile_loader: Callable[[], Sequence[Profile]] | None = None, +) -> ProfileDetectionResult: + """Detect profiles from an existing context and describe unresolved metadata hints.""" + + # Keep both the parsed references and malformed raw values: references can + # select profiles, while malformed values are needed for diagnostics and + # may allow a cautious suggestion when a URI was written as a string. + hints, malformed_values = _get_profile_hints(context.ro_crate.metadata) + candidate_profile_uris = {value.id for _, value in hints if isinstance(value.id, str) and value.id} + diagnostics = [ + ProfileDetectionDiagnostic( + code="INVALID_REFERENCE" if prop == "about" else "INVALID_CONFORMS_TO", + property=prop, + location=location, + value=value, + message=( + "Expected a JSON-LD reference object containing `@id`; " + 'for example, {"@id": "https://example.org/profile"}.' + ), + ) + for prop, location, value in malformed_values + ] + for diagnostic in diagnostics: + logger.warning( + "Cannot use %s `%s` value %r for profile detection: %s", + diagnostic.location, + diagnostic.property, + diagnostic.value, + diagnostic.message, + ) + + logger.debug("Candidate profiles: %s", candidate_profile_uris) + has_malformed_profile_hint = any(prop == "conformsTo" for prop, _, _ in malformed_values) + has_string_profile_hint = any( + prop == "conformsTo" and isinstance(value, str) for prop, _, value in malformed_values + ) + + # No hint at all is different from a malformed hint: report absence only + # when the crate supplied no conformsTo value that we could inspect. + if not candidate_profile_uris and not has_malformed_profile_hint: + diagnostics.append( + ProfileDetectionDiagnostic( + code="NO_PROFILE_HINT", + property="conformsTo", + location=None, + value=None, + message="No `conformsTo` profile reference was found in the metadata descriptor or Root Data Entity.", + ) + ) + logger.debug("Unable to determine the profile to validate against") + + # A string conformsTo is the one malformed form for which we can try to + # identify the intended profile. Other malformed values cannot match a + # profile, so avoid loading the profile catalog just to return diagnostics. + if not candidate_profile_uris and not has_string_profile_hint: + logger.debug("No usable profile URI or string suggestion candidate found") + return ProfileDetectionResult([], diagnostics) + + # Load profiles if valid URIs need matching or a string value might support + # a suggestion. The latter is never treated as a detected profile here. + if profile_loader is None: + available_profiles = Profile.load_profiles( + context.profiles_path, + extra_profiles_path=context.extra_profiles_path, + publicID=context.publicID, + severity=context.requirement_severity, + ) + else: + available_profiles = profile_loader() + matched_profiles = [profile for profile in available_profiles if profile.uri in candidate_profile_uris] + candidate_profiles = [] + for profile in matched_profiles: + candidate_profiles.append(profile) + for inherited_profile in profile.inherited_profiles: + # Keep the most specific matched profiles; their inherited + # profiles are applied through the inheritance relationship. + if inherited_profile in candidate_profiles: + candidate_profiles.remove(inherited_profile) + logger.debug("%d Candidate Profiles found: %s", len(candidate_profiles), candidate_profiles) + + unmatched_profile_uris = candidate_profile_uris.difference({profile.uri for profile in matched_profiles}) + logger.debug("Unmatched Candidate Profiles URIs: %s", unmatched_profile_uris) + if unmatched_profile_uris: + logger.warning( + "The conformance to the following profiles could not be verified: %s", + ", ".join(unmatched_profile_uris), + ) + for profile_uri in unmatched_profile_uris: + locations = [location for location, value in hints if value.id == profile_uri] + diagnostics.append( + ProfileDetectionDiagnostic( + code="PROFILE_NOT_FOUND", + property="conformsTo", + location=", ".join(locations), + value=profile_uri, + message="No available validation profile has this URI.", + ) + ) + + # Suggestions are deliberately separate from detected profiles: callers + # must decide whether to accept one, and validation still checks metadata. + suggested_profile = _suggest_profile(candidate_profiles, candidate_profile_uris, diagnostics, available_profiles) + result = ProfileDetectionResult(candidate_profiles, diagnostics, suggested_profile) + logger.debug("Detailed profile detection result: %s", result) + return result + + +def _get_profile_hints(metadata) -> tuple[list[tuple[str, ROCrateEntity]], list[tuple[str, str, Any]]]: + """Read descriptor and Root Data Entity profile hints and retain their locations.""" + hints: list[tuple[str, ROCrateEntity]] = [] + diagnostics: list[tuple[str, str, Any]] = [] + try: + descriptor = metadata.get_file_descriptor_entity() + except Exception: + # Without a descriptor there is no reliable source for either the + # descriptor's conformsTo or the Root Data Entity's hints. + return hints, diagnostics + + root_data_entity = descriptor.get_property("about") + entities: list[tuple[str, ROCrateEntity]] = [("RO-Crate Metadata Descriptor", descriptor)] + if isinstance(root_data_entity, ROCrateEntity): + entities.append(("Root Data Entity", root_data_entity)) + elif root_data_entity is not None: + # `about` should point to an entity. Preserve a wrong-shaped value as a + # diagnostic rather than attempting to read properties from it. + diagnostics.append(("about", "RO-Crate Metadata Descriptor", root_data_entity)) + + for location, entity in entities: + raw_values = entity.get_property("conformsTo", []) + # JSON-LD permits one value or an array of values for a property. + values = raw_values if isinstance(raw_values, list) else [raw_values] + for value in values: + if value is None: + continue + if isinstance(value, ROCrateEntity) and isinstance(value.id, str) and value.id: + hints.append((location, value)) + else: + diagnostics.append(("conformsTo", location, value)) + return hints, diagnostics + + +def _suggest_profile( + candidates: list[Profile], + candidate_profile_uris: set[str], + diagnostics: list[ProfileDetectionDiagnostic], + available_profiles: list[Profile], +) -> Profile | None: + """Return one profile only when malformed string form is the sole detection issue.""" + string_diagnostics = [diagnostic for diagnostic in diagnostics if diagnostic.code == "INVALID_CONFORMS_TO"] + + # Do not guess if a valid reference, another kind of metadata problem, or + # conflicting string values could change which profile the crate intends. + can_suggest = ( + not candidates + and not candidate_profile_uris + and len(string_diagnostics) == len(diagnostics) + and bool(string_diagnostics) + and all(isinstance(diagnostic.value, str) for diagnostic in string_diagnostics) + and len({diagnostic.value for diagnostic in string_diagnostics}) == 1 + ) + if not can_suggest: + return None + + # Matching must be unique across available profiles; zero matches means + # unknown URI, while multiple matches would make the suggestion ambiguous. + suggestion_uri = string_diagnostics[0].value + matches = [profile for profile in available_profiles if profile.uri == suggestion_uri] + return matches[0] if len(matches) == 1 else None diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index fd03d59d8..c38102db8 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -38,6 +38,7 @@ ValidationEvent, ) from rocrate_validator.models.profile import Profile +from rocrate_validator.models.profile_detection import detect_rocrate_profiles from rocrate_validator.models.requirement import ( Requirement, RequirementCheck, @@ -57,7 +58,7 @@ import tempfile from rdflib.term import Node - + from rocrate_validator.models.profile_detection import ProfileDetectionResult from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck @@ -270,47 +271,22 @@ def detect_rocrate_profiles( """ Detect the profiles to validate against """ + return self.detect_rocrate_profiles_detailed(rocrate_uri, metadata_dict=metadata_dict).profiles + + def detect_rocrate_profiles_detailed( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + ) -> ProfileDetectionResult: + """Detect profiles and describe unusable or unresolved profile hints.""" settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) with self.__run_lock: - return self.__detect_rocrate_profiles__(settings) - - def __detect_rocrate_profiles__(self, settings: ValidationSettings) -> list[Profile]: - # initialize the validation context - context = ValidationContext(self, settings) - candidate_profiles_uris: set[str] = set() - candidate_profiles_uris.update(context.ro_crate.metadata.get_conforms_to() or []) - candidate_profiles_uris.update(context.ro_crate.metadata.get_root_data_entity_conforms_to() or []) - - logger.debug("Candidate profiles: %s", candidate_profiles_uris) - if not candidate_profiles_uris: - logger.debug("Unable to determine the profile to validate against") - return [] - # load the profiles - profiles = [] - candidate_profiles = [] - available_profiles = self.__get_prepared_profile_catalog__(context) - profiles = [p for p in available_profiles if p.uri in candidate_profiles_uris] - # get the candidate profiles - for profile in profiles: - candidate_profiles.append(profile) - inherited_profiles = profile.inherited_profiles - for inherited_profile in inherited_profiles: - if inherited_profile in candidate_profiles: - candidate_profiles.remove(inherited_profile) - logger.debug( - "%d Candidate Profiles found: %s", - len(candidate_profiles), - candidate_profiles, - ) - # unmatched candidate profiles - unmatched_profiles = candidate_profiles_uris.difference({p.uri for p in profiles}) - logger.debug("Unmatched Candidate Profiles URIs: %s", unmatched_profiles) - if len(unmatched_profiles) > 0: - logger.warning( - "The conformance to the following profiles could not be verified: %s", - ", ".join(unmatched_profiles), + context = ValidationContext(self, settings) + return detect_rocrate_profiles( + context, + profile_loader=lambda: self.__get_prepared_profile_catalog__(context), ) - return candidate_profiles def validate( self, From 2b0cbaae32ab53989536e1a6662d9d3f6bbb476f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 15:30:17 +0200 Subject: [PATCH 218/271] =?UTF-8?q?feat(validation):=20=F0=9F=A7=AD=20expl?= =?UTF-8?q?ain=20and=20confirm=20profile=20detection=20suggestions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/validate.py | 76 ++++++++++++++----- .../1.2/must/1_file-descriptor_metadata.ttl | 8 ++ rocrate_validator/rocrate/metadata.py | 34 ++++++--- rocrate_validator/services.py | 20 +++-- 4 files changed, 103 insertions(+), 35 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index f7e2c01ba..ab249658a 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -19,6 +19,7 @@ from pathlib import Path import rich_click as click +from rich.markup import escape from rich.padding import Padding from rich.rule import Rule from rich.table import Table @@ -528,7 +529,7 @@ def _parse_skip_checks(skip_checks: list[str] | None) -> list[str]: return skip_checks_list -def _resolve_profile_identifiers( +def _resolve_profile_identifiers( # noqa: C901, PLR0912 console: Console, *, interactive: bool, @@ -548,33 +549,54 @@ def _resolve_profile_identifiers( if not profile_identifiers: # Auto-detect the profile to use for validation (if not disabled) candidate_profiles = None + detection_result = None if not no_auto_profile: - candidate_profiles = services.detect_profiles(settings=validation_settings) + detection_result = services.detect_profiles_detailed(settings=validation_settings) + candidate_profiles = detection_result.profiles logger.debug("Candidate profiles: %s", candidate_profiles) else: logger.info("Auto-detection of the profiles to use for validation is disabled") + suggested_profile = detection_result.suggested_profile if detection_result else None + + reason = None + if detection_result and detection_result.diagnostics: + diagnostic = detection_result.diagnostics[0] + reason = ( + f"`{diagnostic.property}` at {diagnostic.location} has value {escape(repr(diagnostic.value))}. " + f"{diagnostic.message}" + ) + # Prompt the user when interactive and no single profile could be auto-detected if interactive and ( not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles) ): - console.print( - Padding( - Rule( - "[bold yellow]WARNING: [/bold yellow]" - "[bold]Unable to automatically detect the profile to use for validation[/bold]\n", - align="center", - style="bold yellow", - ), - (2, 2, 0, 2), + suggestion_confirmed = suggested_profile and _confirm_suggested_profile(console, suggested_profile, reason) + if suggestion_confirmed: + profile_identifiers = [suggested_profile.identifier] + autodetection = True + if not suggestion_confirmed: + console.print( + Padding( + Rule( + "[bold yellow]WARNING: [/bold yellow]" + "[bold]Unable to automatically detect the profile to use for validation[/bold]" + + (f"\n{reason}" if reason else "") + + "\n", + align="center", + style="bold yellow", + ), + (2, 2, 0, 2), + ) ) - ) - selected_options = multiple_choice(console, available_profiles) - if selected_options is None or isinstance(selected_options, bool): - selected_options = [] - profile_identifiers = [available_profiles[int(o)].identifier for o in selected_options] - logger.debug("Profile selected: %s", selected_options) - console.print(Padding(Rule(style="bold yellow"), (1, 2))) + if not profile_identifiers: + selected_options = multiple_choice(console, available_profiles) + if selected_options is None or isinstance(selected_options, bool): + selected_options = [] + profile_identifiers = [available_profiles[int(o)].identifier for o in selected_options] + logger.debug("Profile selected: %s", selected_options) + if not suggestion_confirmed: + console.print(Padding(Rule(style="bold yellow"), (1, 2))) elif candidate_profiles and len(candidate_profiles) < len(available_profiles): logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) autodetection = True @@ -587,12 +609,30 @@ def _resolve_profile_identifiers( console.print("[bold]Auto-detection of the profiles to use for validation is disabled[/bold]") else: console.print("[bold]Unable to automatically detect the profile to use for validation[/bold]") + if reason: + console.print(f"{' ' * 11}{reason}") console.print(f"{' ' * 11}[bold]The base `ro-crate` profile will be used for validation[/bold]") profile_identifiers = ["ro-crate"] return profile_identifiers, autodetection +def _confirm_suggested_profile(console: Console, profile, reason: str | None) -> bool: + """Explain the malformed conformsTo value and ask before using its profile candidate.""" + console.print( + f"[bold yellow]Unable to automatically detect the profile to use for validation:[/bold yellow] {reason}" + ) + confirmation = get_single_char( + console, + choices=["y", "n"], + message=( + "[bold]Use the suggested profile " + f"[magenta]{profile.identifier}[/magenta]? ([magenta]y/n[/magenta]): [/bold]" + ), + ) + return confirmation == "y" + + def _render_console_result( validation_settings: dict, *, diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl index 77577fd76..6ce09ba1c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl @@ -88,4 +88,12 @@ ro-crate:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; sh:path schema_org:about ; # sh:class ro-crate:RootDataEntity ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor `conformsTo` reference" ; + sh:description "If present, each `conformsTo` value MUST be expressed as a JSON-LD reference object with an `@id`." ; + sh:path dct:conformsTo ; + sh:nodeKind sh:IRI ; + sh:message "Each RO-Crate Metadata Descriptor `conformsTo` value MUST be a reference object containing `@id`; use {\"@id\": \"...\"} instead of a string." ; ] . diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index c9b708abd..6a88a7a8e 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -77,12 +77,7 @@ def get_root_data_entity(self) -> ROCrateEntity: def get_root_data_entity_conforms_to(self) -> list[str] | None: try: root_data_entity = self.get_root_data_entity() - result = root_data_entity.get_property("conformsTo", []) - if result is None: - return None - if not isinstance(result, list): - result = [result] - return [_.id for _ in result] + return self._get_conforms_to(root_data_entity, "Root Data Entity") except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting entity image") @@ -137,17 +132,32 @@ def get_web_data_entities(self) -> list[ROCrateEntity]: def get_conforms_to(self) -> list[str] | None: try: file_descriptor = self.get_file_descriptor_entity() - result = file_descriptor.get_property("conformsTo", []) - if result is None: - return None - if not isinstance(result, list): - result = [result] - return [_.id for _ in result] + return self._get_conforms_to(file_descriptor, "RO-Crate Metadata Descriptor") except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting entity identifiers by type") return None + @staticmethod + def _get_conforms_to(entity: ROCrateEntity, entity_description: str) -> list[str] | None: + """Get profile identifiers and explain values that cannot be used as references.""" + result = entity.get_property("conformsTo", []) + if result is None: + return None + values = result if isinstance(result, list) else [result] + identifiers: list[str] = [] + for value in values: + if isinstance(value, ROCrateEntity) and isinstance(value.id, str) and value.id: + identifiers.append(value.id) + continue + logger.warning( + "Cannot use %s `conformsTo` value %r for profile detection: expected a JSON-LD reference " + 'object containing `@id` (for example, {"@id": "https://example.org/profile"}).', + entity_description, + value, + ) + return identifiers + def __get_file_content__(self, binary_mode: bool) -> str | bytes: """Read the metadata descriptor and identify a missing descriptor explicitly.""" try: diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 7542d47d3..a832b677c 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -23,6 +23,7 @@ from rocrate_validator.errors import ProfileNotFound from rocrate_validator.events import Subscriber from rocrate_validator.models import Profile, Severity, ValidationResult, ValidationSettings, Validator +from rocrate_validator.models.profile_detection import ProfileDetectionResult from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester from rocrate_validator.utils.paths import get_profiles_path @@ -36,12 +37,21 @@ def detect_profiles(settings: dict | ValidationSettings) -> list[Profile]: - # initialize the validator + """Detect profiles, preserving the historical list return type.""" + return detect_profiles_detailed(settings).profiles + + +def detect_profiles_detailed(settings: dict | ValidationSettings) -> ProfileDetectionResult: + """Detect profiles and return structured diagnostics and a confirmable suggestion.""" validator = __initialise_validator__(settings) - # detect the profiles - profiles = validator.detect_rocrate_profiles() - logger.debug("Profiles detected: %s", profiles) - return profiles + result = validator.detect_rocrate_profiles_detailed() + logger.debug("Detailed profile detection result: %s", result) + return result + + +def suggest_profile_for_string_conforms_to(settings: dict | ValidationSettings) -> Profile | None: + """Return a unique available profile matching a malformed literal conformsTo value.""" + return detect_profiles_detailed(settings).suggested_profile def validate_metadata_as_dict( From 9ddc2bffe359eed962fdf81ad4bc44bbf32758e4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 15:36:12 +0200 Subject: [PATCH 219/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20malformed=20profile=20hints=20and=20suggestions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate-1.2/test_metadata_descriptor.py | 16 +++++ tests/test_cli.py | 34 +++++++++- tests/unit/test_rocrate.py | 66 +++++++++++++++++++ tests/unit/test_services.py | 64 +++++++++++++++++- 4 files changed, 176 insertions(+), 4 deletions(-) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py index eb7c5bc3b..92ddc0b1a 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py @@ -83,3 +83,19 @@ def test_invalid_recommended_prefix_conformsTo(): "The RO-Crate metadata file descriptor `conformsTo` URI SHOULD start with https://w3id.org/ro/crate/" ], ) + + +def test_invalid_string_conformsTo_is_reported_at_required_severity(): + """A literal conformsTo value must be reported even with explicit profile selection.""" + do_entity_test( + __metadata_descriptor_crates__.valid_single_value_conformsTo, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + rocrate_entity_patch={ + "ro-crate-metadata.json": {"conformsTo": "https://w3id.org/ro/crate/1.2"}, + }, + expected_triggered_issues=[ + "Each RO-Crate Metadata Descriptor `conformsTo` value MUST be a reference object containing `@id`", + ], + ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 87800be41..edd3e670a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,6 +14,7 @@ import json import re +from io import StringIO from pathlib import Path from unittest.mock import patch @@ -22,12 +23,14 @@ from rdflib import Graph from rocrate_validator import services +from rocrate_validator.cli.commands.validate import _resolve_profile_identifiers from rocrate_validator.cli.main import cli from rocrate_validator.errors import BadSyntaxError -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import ProfileDetectionDiagnostic, ProfileDetectionResult, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.utils import log as logging +from rocrate_validator.utils.io_helpers.output.console import Console from rocrate_validator.utils.versioning import get_version from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import InvalidFileDescriptor, InvalidFileDescriptorEntity, ValidROC @@ -69,6 +72,35 @@ def test_validate_packaging_mode_option(cli_runner: CliRunner, tmp_path: Path): assert incompatible.exit_code != 0 assert "standalone metadata document" in incompatible.output +def test_profile_detection_cli_escapes_metadata_values_in_warning(monkeypatch): + """Malformed metadata values containing Rich markup are rendered as plain text.""" + detection = ProfileDetectionResult( + profiles=[], + diagnostics=[ + ProfileDetectionDiagnostic( + code="INVALID_CONFORMS_TO", + property="conformsTo", + location="RO-Crate Metadata Descriptor", + value="[/]", + message="Expected a JSON-LD reference object containing `@id`.", + ) + ], + ) + output = StringIO() + monkeypatch.setattr(services, "detect_profiles_detailed", lambda **kwargs: detection) + + profile_identifiers, _ = _resolve_profile_identifiers( + Console(file=output, width=200), + interactive=False, + no_auto_profile=False, + available_profiles=[], + profile_identifiers=[], + validation_settings={}, + ) + + assert profile_identifiers == ["ro-crate"] + assert "[/]" in output.getvalue() + def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): result = cli_runner.invoke( diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index ec872c405..1d2f0c7fd 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -19,6 +19,8 @@ import pytest from rocrate_validator.errors import ROCrateInvalidURIError +from rocrate_validator.models import URI, ValidationSettings, Validator +from rocrate_validator.models import profile_detection as profile_detection_module from rocrate_validator.rocrate import ( BagitROCrate, ROCrate, @@ -31,6 +33,7 @@ ROCrateMetadata, ROCrateRemoteZip, ) +from rocrate_validator.rocrate import metadata as metadata_module from rocrate_validator.utils import log as logging from tests.ro_crates import InvalidDataEntity, ValidROC @@ -609,6 +612,69 @@ def _metadata_dict_with_main_entity(main_entity: object) -> dict: } +@pytest.mark.parametrize( + ("entity_id", "getter_name", "location"), + [ + ("ro-crate-metadata.json", "get_conforms_to", "RO-Crate Metadata Descriptor"), + ("./", "get_root_data_entity_conforms_to", "Root Data Entity"), + ], +) +def test_conforms_to_profile_detection_warns_and_skips_string_values(entity_id, getter_name, location, monkeypatch): + """String literals are diagnosed and excluded while valid @id references remain detectable.""" + warnings = [] + monkeypatch.setattr(metadata_module.logger, "warning", lambda message, *args: warnings.append(message % args)) + metadata_dict = _metadata_dict_with_id("remote-file") + metadata_dict["@graph"][0]["conformsTo"] = {"@id": "https://w3id.org/ro/crate/1.1"} + for entity in metadata_dict["@graph"]: + if entity["@id"] == entity_id: + entity["conformsTo"] = [ + {"@id": "https://example.org/profile"}, + "https://example.org/literal-profile", + ] + break + + crate = ROCrate.from_metadata_dict(metadata_dict) + + assert getattr(crate.metadata, getter_name)() == ["https://example.org/profile"] + assert len(warnings) == 1 + assert "Cannot use" in warnings[0] + assert location in warnings[0] + assert "https://example.org/literal-profile" in warnings[0] + assert "containing `@id`" in warnings[0] + + +def test_automatic_profile_detection_reports_string_conforms_to(monkeypatch): + """Automatic detection reports why a literal conformsTo value was not used.""" + warnings = [] + monkeypatch.setattr( + profile_detection_module.logger, + "warning", + lambda message, *args: warnings.append(message % args), + ) + metadata_dict = { + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": "https://w3id.org/ro/crate/1.2", + }, + {"@id": "./", "@type": "Dataset", "name": "Test crate"}, + ], + } + settings = ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + metadata_dict=metadata_dict, + metadata_only=True, + ) + + assert Validator(settings).detect_rocrate_profiles() == [] + assert len(warnings) == 1 + assert "RO-Crate Metadata Descriptor" in warnings[0] + assert "https://w3id.org/ro/crate/1.2" in warnings[0] + + @pytest.mark.parametrize("main_entity", [{"@id": "workflow.ga"}, [{"@id": "workflow.ga"}]]) def test_get_main_workflow_returns_entity_for_object_and_singleton_array(main_entity): """A singular main workflow accessor accepts both JSON-LD representations.""" diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py index be58b7b04..ffb2742f1 100644 --- a/tests/unit/test_services.py +++ b/tests/unit/test_services.py @@ -16,10 +16,17 @@ import shutil import tempfile from pathlib import Path +from types import SimpleNamespace from rocrate_validator.models import URI, ValidationSettings from rocrate_validator.rocrate import ROCrateMetadata -from rocrate_validator.services import detect_profiles, get_profiles, validate +from rocrate_validator.services import ( + detect_profiles, + detect_profiles_detailed, + get_profiles, + validate, + validate_metadata_as_dict, +) from rocrate_validator.utils import log as logging from tests.ro_crates import InvalidFileDescriptorEntity, InvalidMultiProfileROC, ValidROC from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER @@ -31,6 +38,59 @@ metadata_file_descriptor = Path(ROCrateMetadata.METADATA_FILE_DESCRIPTOR) +def _profile_detection_settings(about, conforms_to=None): + descriptor = {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": about} + if conforms_to is not None: + descriptor["conformsTo"] = conforms_to + return ValidationSettings( + rocrate_uri=URI("/tmp/conforms-profile-detection"), + metadata_only=True, + metadata_dict={ + "@context": {}, + "@graph": [descriptor, {"@id": "./", "@type": "Dataset"}], + }, + ) + + +def test_detailed_profile_detection_reports_missing_and_unknown_hints(monkeypatch): + """Detailed detection distinguishes absent hints from unmatched profile references.""" + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: []) + + missing = detect_profiles_detailed(_profile_detection_settings({"@id": "./"})) + unknown = detect_profiles_detailed( + _profile_detection_settings({"@id": "./"}, {"@id": "https://example.org/unknown-profile"}) + ) + + assert [diagnostic.code for diagnostic in missing.diagnostics] == ["NO_PROFILE_HINT"] + assert [diagnostic.code for diagnostic in unknown.diagnostics] == ["PROFILE_NOT_FOUND"] + assert unknown.diagnostics[0].value == "https://example.org/unknown-profile" + + +def test_detailed_profile_detection_handles_invalid_about_without_raising(monkeypatch): + """A malformed about value is returned as a structured diagnostic.""" + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: []) + + result = detect_profiles_detailed(_profile_detection_settings("./")) + + assert {diagnostic.code for diagnostic in result.diagnostics} == {"INVALID_REFERENCE", "NO_PROFILE_HINT"} + + +def test_detailed_profile_detection_suggests_only_matching_string_hint(monkeypatch): + """A sole string hint matching one profile yields a separate suggestion.""" + profile = SimpleNamespace( + uri="https://w3id.org/ro/crate/1.2", + identifier="ro-crate-1.2", + inherited_profiles=[], + ) + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [profile]) + + result = detect_profiles_detailed(_profile_detection_settings({"@id": "./"}, "https://w3id.org/ro/crate/1.2")) + + assert result.profiles == [] + assert result.suggested_profile is profile + assert result.diagnostics[0].code == "INVALID_CONFORMS_TO" + + def test_default_profiles_list(): """ Test the list of profiles. @@ -235,7 +295,5 @@ def test_valid_crate_metadata_dict_with_metadata_only(): # Check the detected profile assert profiles[0].identifier == "ro-crate-1.1", "Expected the 'ro-crate' profile" - from rocrate_validator.services import validate_metadata_as_dict - result = validate_metadata_as_dict(metadata_dict, settings) assert result.passed(), "RO-Crate should be valid in metadata-only mode" From 05f0dc305c89cde0e6f13553e1cdaeac81b09a33 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 15:36:52 +0200 Subject: [PATCH 220/271] =?UTF-8?q?docs(validation):=20=F0=9F=93=9D=20expl?= =?UTF-8?q?ain=20profile=20detection=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2_usage_cli.rst | 17 +++++++++++++++++ docs/3_usage_api.rst | 42 +++++++++++++++++++++++++++++++++++++++++ docs/4_how_it_works.rst | 18 ++++++++++++++---- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index ce33748ef..32e687b85 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -45,3 +45,20 @@ CLI Validation profile, see :doc:`Writing a profile <11_writing_a_profile>`. * For choosing Attached/Detached rules, checking only metadata, and supported inputs, see :ref:`package_modes`. + +Profile detection when ``conformsTo`` is a string +------------------------------------------------- + +Profile detection expects JSON-LD references, for example +``"conformsTo": {"@id": "https://w3id.org/ro/crate/1.2"}``. If a +``conformsTo`` value is instead a string and it uniquely matches an available +profile, interactive validation explains the malformed value and asks whether +to use that profile. Confirming selects the profile for validation; declining +returns to the profile selection prompt. The metadata is not changed, so the +validation report can still include an error for the malformed value. + +In non-interactive mode, the suggestion is not accepted automatically. When +there is no usable profile reference, validation falls back to the base +``ro-crate`` profile. Specify ``--profile-identifier`` to choose a profile +explicitly, or correct ``conformsTo`` in the metadata descriptor or Root Data +Entity. Use ``--no-auto-profile`` to disable profile detection. diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 67288b78c..31b2ec1f6 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -42,6 +42,48 @@ Programmatic Validation * For package modes, scope, and accepted input combinations, see :ref:`package_modes`. +Profile Detection +----------------- + +``detect_profiles`` keeps returning the profiles found from valid JSON-LD +references. To inspect why another value was not used, call +``detect_profiles_detailed``. It returns a ``ProfileDetectionResult`` with +``profiles``, ``diagnostics``, and an optional ``suggested_profile``. + +A suggestion is returned only when profile detection found no usable profile +reference, every detection diagnostic concerns a string-valued ``conformsTo``, +all those values are identical, and exactly one available profile has that URI. +Other malformed hints, unknown valid URIs, or multiple possible profiles do +not produce a suggestion. The suggestion does not change the crate metadata +and must be selected explicitly by the calling application. + +Each diagnostic has a stable ``code`` as well as the affected property, +metadata location, original value, and explanation. Codes distinguish an +invalid reference (``INVALID_REFERENCE``), a malformed ``conformsTo`` value +(``INVALID_CONFORMS_TO``), a URI that does not identify an available profile +(``PROFILE_NOT_FOUND``), and missing profile hints (``NO_PROFILE_HINT``). + +.. code-block:: python + + from rocrate_validator.services import detect_profiles_detailed, validate + + settings = {"rocrate_uri": "/path/to/crate"} + detection = detect_profiles_detailed(settings) + for diagnostic in detection.diagnostics: + print(diagnostic.code, diagnostic.message) + + if detection.suggested_profile is not None: + # The application decides whether to accept the suggestion. + settings["profile_identifier"] = detection.suggested_profile.identifier + result = validate(settings) + +Programmatic detection does not prompt or silently select a suggestion. If the +application does not set ``profile_identifier``, ``validate`` uses the profile +configured in ``ValidationSettings`` (or its default). Selecting a suggested +profile does not repair invalid metadata: ``conformsTo`` must still be +corrected to a JSON-LD reference, such as +``{"@id": "https://w3id.org/ro/crate/1.2"}``. + Metadata-only Validation ------------------------ diff --git a/docs/4_how_it_works.rst b/docs/4_how_it_works.rst index e2ca8f08c..138f85436 100644 --- a/docs/4_how_it_works.rst +++ b/docs/4_how_it_works.rst @@ -72,7 +72,8 @@ Validation profile selection * **Automatic Profile Matching** (default): By default, `rocrate-validator` will attempt to select the correct validation - profiles for the input RO-Crate based on the `conformsTo` property of the Root Data Entity. + profiles for the input RO-Crate using valid `conformsTo` references in the + metadata descriptor and Root Data Entity. - If a precise match is found for the `conformsTo` property, that profile is selected for validation. @@ -80,12 +81,21 @@ Validation profile selection - If no precise match is found: - in **Interactive Mode:** (available only through the CLI) the system - will prompt the user to select a profile from the list of - profiles to which the RO-Crate conforms; + explains why detection failed and prompts the user to select a profile. + If the only issue is a string-valued `conformsTo` that uniquely + identifies an available profile, that profile is offered first and is + used only after the user confirms; - in **Non-Interactive Mode:** the system will validate against all profiles to which the RO-Crate conforms, and for which validation rules are available. - In all cases, input RO-Crates are validated against the base `ro-crate` profile. + If no usable profile reference is found, it falls back to the base + `ro-crate` profile; a suggestion from a malformed string is never + selected automatically. In all cases, input RO-Crates are validated + against the base `ro-crate` profile. + + Selecting a profile only chooses which rules to run. It does not repair + metadata: a string-valued `conformsTo` remains malformed and can still + produce a validation issue. * **Profile Versions**: - It may happen that the RO-Crate profile version to which the input From 7a95271f9f1dc0d2899dafb370b8dc9dbda2826c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 13 Oct 2026 17:32:37 +0200 Subject: [PATCH 221/271] =?UTF-8?q?fix(models):=20=F0=9F=9A=A8=20propagate?= =?UTF-8?q?=20unexpected=20profile=20detection=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile_detection.py | 5 +++-- tests/unit/requirements/test_profiles.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models/profile_detection.py b/rocrate_validator/models/profile_detection.py index 7097b7cab..fb605b792 100644 --- a/rocrate_validator/models/profile_detection.py +++ b/rocrate_validator/models/profile_detection.py @@ -148,9 +148,10 @@ def _get_profile_hints(metadata) -> tuple[list[tuple[str, ROCrateEntity]], list[ diagnostics: list[tuple[str, str, Any]] = [] try: descriptor = metadata.get_file_descriptor_entity() - except Exception: + except ValueError: # Without a descriptor there is no reliable source for either the - # descriptor's conformsTo or the Root Data Entity's hints. + # descriptor's conformsTo or the Root Data Entity's hints. Other + # exceptions indicate unexpected failures and must reach the caller. return hints, diagnostics root_data_entity = descriptor.get_property("about") diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index dad313987..a4298eb46 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -102,7 +102,7 @@ def test_profile_detection_error_is_not_silenced(monkeypatch): """Unexpected metadata errors must not become an empty profile selection.""" class BrokenMetadata: - def get_conforms_to(self): + def get_file_descriptor_entity(self): raise RuntimeError("metadata profile detection failed") class BrokenROCrate: From 272356d0bf22de378c038c7afed73d53b78c5379 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 14 Oct 2026 12:54:38 +0200 Subject: [PATCH 222/271] =?UTF-8?q?style(models):=20=F0=9F=A7=B9=20fix=20p?= =?UTF-8?q?rofile=20detection=20import=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile_detection.py | 4 +++- rocrate_validator/models/validation.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models/profile_detection.py b/rocrate_validator/models/profile_detection.py index fb605b792..e6e256f56 100644 --- a/rocrate_validator/models/profile_detection.py +++ b/rocrate_validator/models/profile_detection.py @@ -3,13 +3,15 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Callable, Sequence +from typing import TYPE_CHECKING, Any from rocrate_validator.models._logging import logger from rocrate_validator.models.profile import Profile from rocrate_validator.rocrate.entity import ROCrateEntity if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from rocrate_validator.models.validation import ValidationContext diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index c38102db8..98c177776 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -58,6 +58,7 @@ import tempfile from rdflib.term import Node + from rocrate_validator.models.profile_detection import ProfileDetectionResult from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck From 37b1c002a9dec639d18bcd3545f6eefb3a6c3eab Mon Sep 17 00:00:00 2001 From: EttoreM Date: Thu, 15 Oct 2026 09:50:53 +0100 Subject: [PATCH 223/271] Sorted imports in test_wfrc_create_action.py --- .../profiles/workflow-run-crate/test_wfrc_create_action.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py index 7574da850..5a24b5043 100644 --- a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py +++ b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py @@ -17,7 +17,6 @@ from tests.ro_crates import ValidROC from tests.shared import do_entity_test - EXPECTED_ISSUE = "At least one CreateAction MUST reference the root data entity's mainEntity as its instrument." From 57a1413f3c1293bd7de673f7eb072ac31d07bc7c Mon Sep 17 00:00:00 2001 From: EttoreM Date: Fri, 16 Oct 2026 13:58:35 +0100 Subject: [PATCH 224/271] Added the other action types ( and ) to the SHACL constraints and corresponding tests. --- .../must/4_create_action.ttl | 8 ++- .../test_wfrc_create_action.py | 51 ++++++++++++++----- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl b/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl index 40f6556fd..7d0d22ad1 100644 --- a/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl +++ b/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl @@ -31,8 +31,12 @@ workflow-run-crate:CreateActionWithMainEntityAsInstrumentRequired a sh:NodeShape [ sh:inversePath schema:instrument ] ) ; sh:qualifiedValueShape [ - sh:class schema:CreateAction ; + sh:or ( + [ sh:class schema:CreateAction ] + [ sh:class schema:ActivateAction ] + [ sh:class schema:UpdateAction ] + ) ; ] ; sh:qualifiedMinCount 1 ; - sh:message "At least one CreateAction MUST reference the root data entity's mainEntity as its instrument." ; + sh:message "At least one CreateAction, ActivateAction or UpdateAction MUST reference the root data entity's mainEntity as its instrument." ; ] . diff --git a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py index 5a24b5043..aac53085c 100644 --- a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py +++ b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py @@ -12,16 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + from rocrate_validator.models import Severity from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import ValidROC from tests.shared import do_entity_test -EXPECTED_ISSUE = "At least one CreateAction MUST reference the root data entity's mainEntity as its instrument." +EXPECTED_ISSUE = "At least one CreateAction, ActivateAction or UpdateAction MUST reference the root data entity's mainEntity as its instrument." -def test_wfrc_no_create_action(): - """A crate without any CreateAction must fail the main entity execution check.""" +def test_wfrc_no_supported_action_with_main_entity_as_instrument(): + """Supported actions referencing another tool cannot satisfy the execution check.""" do_entity_test( ValidROC().workflow_run_crate, Severity.REQUIRED, @@ -31,16 +33,29 @@ def test_wfrc_no_create_action(): skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], rocrate_entity_mod_sparql=""" PREFIX schema: - DELETE { ?action ?predicate ?value . } + DELETE { ?action schema:instrument ?workflow . } + INSERT { + ?action schema:instrument . + a schema:SoftwareApplication ; + schema:name "Another tool" . + } WHERE { - ?action a schema:CreateAction ; - ?predicate ?value . + ?root schema:mainEntity ?workflow . + ?metadata schema:about ?root . + FILTER( + STRENDS(STR(?metadata), "ro-crate-metadata.json") || + STRENDS(STR(?metadata), "ro-crate-metadata.jsonld") + ) + VALUES ?actionType { schema:CreateAction schema:ActivateAction schema:UpdateAction } + ?action a ?actionType ; + schema:instrument ?workflow . } """, ) -def test_wfrc_no_create_action_with_main_entity_as_instrument(): +@pytest.mark.parametrize("action_type", ["CreateAction", "ActivateAction", "UpdateAction"]) +def test_wfrc_no_action_with_main_entity_as_instrument(action_type): """A valid instrument pointing to another tool cannot satisfy main entity execution.""" do_entity_test( ValidROC().workflow_run_crate, @@ -49,23 +64,31 @@ def test_wfrc_no_create_action_with_main_entity_as_instrument(): expected_triggered_issues=[EXPECTED_ISSUE], profile_identifier="workflow-run-crate", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], - rocrate_entity_mod_sparql=""" + rocrate_entity_mod_sparql=f""" PREFIX schema: - DELETE { ?action schema:instrument ?workflow . } - INSERT { + DELETE {{ ?action a ?originalType . }} + INSERT {{ ?action a schema:{action_type} . }} + WHERE {{ + VALUES ?originalType {{ schema:CreateAction schema:ActivateAction schema:UpdateAction }} + ?action a ?originalType . + }} ; + + PREFIX schema: + DELETE {{ ?action schema:instrument ?workflow . }} + INSERT {{ ?action schema:instrument . a schema:SoftwareApplication ; schema:name "Another tool" . - } - WHERE { + }} + WHERE {{ ?root schema:mainEntity ?workflow . ?metadata schema:about ?root . FILTER( STRENDS(STR(?metadata), "ro-crate-metadata.json") || STRENDS(STR(?metadata), "ro-crate-metadata.jsonld") ) - ?action a schema:CreateAction ; + ?action a schema:{action_type} ; schema:instrument ?workflow . - } + }} """, ) From 2b9490aa43a998bb1a2f9b4be2c58df57e8d4182 Mon Sep 17 00:00:00 2001 From: EttoreM Date: Mon, 19 Oct 2026 10:34:10 +0100 Subject: [PATCH 225/271] Fixed ruff error --- .../profiles/workflow-run-crate/test_wfrc_create_action.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py index aac53085c..563523165 100644 --- a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py +++ b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py @@ -19,7 +19,10 @@ from tests.ro_crates import ValidROC from tests.shared import do_entity_test -EXPECTED_ISSUE = "At least one CreateAction, ActivateAction or UpdateAction MUST reference the root data entity's mainEntity as its instrument." +EXPECTED_ISSUE = ( + "At least one CreateAction, ActivateAction or UpdateAction MUST reference the root data entity's mainEntity " + "as its instrument." +) def test_wfrc_no_supported_action_with_main_entity_as_instrument(): From e022f6549358fd5d30ab7a9e3a68354d5aab7c5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:43:25 +0000 Subject: [PATCH 226/271] build(deps): bump crate-ci/typos Bumps the weekly-batch group with 1 update in the / directory: [crate-ci/typos](https://github.com/crate-ci/typos). Updates `crate-ci/typos` from 1.50.3 to 1.51.1 - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/main/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.50.3...v1.51.1) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.51.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: weekly-batch ... Signed-off-by: dependabot[bot] --- .github/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index b872f93f5..f7e463f9a 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -72,7 +72,7 @@ jobs: # Emit a compact rule-count summary and fail the job if needed. ruff check rocrate_validator tests --statistics - name: ⌛ Spell check code and profiles (covers Python and SHACL) - uses: crate-ci/typos@v1.50.3 + uses: crate-ci/typos@v1.51.1 # Runs the tests test: From 756dbb4e4469e855544e964010e30f2fbc102a4b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Oct 2026 12:50:49 +0200 Subject: [PATCH 227/271] =?UTF-8?q?fix(validation):=20=E2=9A=A0=EF=B8=8F?= =?UTF-8?q?=20clarify=20malformed=20profile=20hints=20and=20JSON=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/cli/commands/errors.py | 11 +++ rocrate_validator/cli/commands/validate.py | 99 ++++++++++--------- rocrate_validator/models/profile_detection.py | 35 ++++--- 3 files changed, 85 insertions(+), 60 deletions(-) diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py index 3cd45d1d6..955b2e0cb 100644 --- a/rocrate_validator/cli/commands/errors.py +++ b/rocrate_validator/cli/commands/errors.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import sys import traceback @@ -45,6 +46,16 @@ def handle_error(e: Exception, console: Console, *, debug: bool = False) -> None To see the available profiles, run: [cyan bold]rocrate-validator profiles list[/cyan bold] """ + elif isinstance(e, json.JSONDecodeError): + source_line = e.doc.split("\n")[e.lineno - 1] + error_message = ( + "The RO-Crate metadata is not valid JSON.\n" + f"Reason: line {e.lineno}, column {e.colno}: [red]{escape(e.msg)}[/red]\n" + f"{escape(source_line)}\n" + f"{' ' * (e.colno - 1)}[red]^[/red]\n" + "Correct the JSON syntax before validating the RO-Crate. " + "Profile auto-detection cannot read invalid JSON." + ) elif isinstance(e, BadSyntaxError): location = _format_syntax_error_location(e) error_message = ( diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index ab249658a..cbc4aec7b 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -546,6 +546,7 @@ def _resolve_profile_identifiers( # noqa: C901, PLR0912 Returns the identifiers and whether they were auto-detected. """ autodetection = False + reason = None if not profile_identifiers: # Auto-detect the profile to use for validation (if not disabled) candidate_profiles = None @@ -557,47 +558,71 @@ def _resolve_profile_identifiers( # noqa: C901, PLR0912 else: logger.info("Auto-detection of the profiles to use for validation is disabled") - suggested_profile = detection_result.suggested_profile if detection_result else None - - reason = None if detection_result and detection_result.diagnostics: diagnostic = detection_result.diagnostics[0] reason = ( - f"`{diagnostic.property}` at {diagnostic.location} has value {escape(repr(diagnostic.value))}. " - f"{diagnostic.message}" + f"`{escape(diagnostic.property or '')}` at {escape(diagnostic.location or '')} " + f"has value {escape(repr(diagnostic.value))}. {escape(diagnostic.message)}" ) - - # Prompt the user when interactive and no single profile could be auto-detected - if interactive and ( - not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles) - ): - suggestion_confirmed = suggested_profile and _confirm_suggested_profile(console, suggested_profile, reason) - if suggestion_confirmed: - profile_identifiers = [suggested_profile.identifier] - autodetection = True - if not suggestion_confirmed: + for diagnostic in detection_result.diagnostics: + if diagnostic.code not in ("INVALID_CONFORMS_TO", "INVALID_REFERENCE"): + continue + console.print( + "\n [bold yellow]WARNING: Non-conforming metadata[/bold yellow]\n" + f" `{escape(diagnostic.property or '')}` at {escape(diagnostic.location or '')} " + f"has value {escape(repr(diagnostic.value))}.\n" + f" {escape(diagnostic.message)}" + ) + if candidate_profiles: console.print( - Padding( - Rule( - "[bold yellow]WARNING: [/bold yellow]" - "[bold]Unable to automatically detect the profile to use for validation[/bold]" - + (f"\n{reason}" if reason else "") - + "\n", - align="center", - style="bold yellow", - ), - (2, 2, 0, 2), - ) + " Profiles automatically selected for validation: " + + escape(", ".join(profile.identifier for profile in candidate_profiles)) + ) + + suggested_profile = detection_result.suggested_profile if detection_result else None + if interactive and not candidate_profiles and suggested_profile: + console.print( + " The string URI matches the available profile " + f"[magenta]{escape(suggested_profile.identifier)}[/magenta]. " + "Using it will not correct the metadata." + ) + confirmation = get_single_char( + console, + choices=["y", "n"], + message=("[bold]Use this profile for validation? ([magenta]y/n[/magenta]): [/bold]"), + ) + if confirmation == "y": + return [suggested_profile.identifier], True + elif not interactive and suggested_profile: + console.print( + " The string URI matches profile " + f"{escape(suggested_profile.identifier)}, but using it requires confirmation. " + "Use --profile-identifier to select it explicitly in non-interactive mode." + ) + + # Prompt the user only when no profile could be auto-detected or confirmed. + if interactive and not candidate_profiles: + console.print( + Padding( + Rule( + "[bold yellow]WARNING: [/bold yellow]" + "[bold]Unable to automatically detect the profile to use for validation[/bold]" + + (f"\n{reason}" if reason else "") + + "\n", + align="center", + style="bold yellow", + ), + (2, 2, 0, 2), ) + ) if not profile_identifiers: selected_options = multiple_choice(console, available_profiles) if selected_options is None or isinstance(selected_options, bool): selected_options = [] profile_identifiers = [available_profiles[int(o)].identifier for o in selected_options] logger.debug("Profile selected: %s", selected_options) - if not suggestion_confirmed: - console.print(Padding(Rule(style="bold yellow"), (1, 2))) - elif candidate_profiles and len(candidate_profiles) < len(available_profiles): + console.print(Padding(Rule(style="bold yellow"), (1, 2))) + elif candidate_profiles: logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) autodetection = True profile_identifiers = [_.identifier for _ in candidate_profiles] @@ -617,22 +642,6 @@ def _resolve_profile_identifiers( # noqa: C901, PLR0912 return profile_identifiers, autodetection -def _confirm_suggested_profile(console: Console, profile, reason: str | None) -> bool: - """Explain the malformed conformsTo value and ask before using its profile candidate.""" - console.print( - f"[bold yellow]Unable to automatically detect the profile to use for validation:[/bold yellow] {reason}" - ) - confirmation = get_single_char( - console, - choices=["y", "n"], - message=( - "[bold]Use the suggested profile " - f"[magenta]{profile.identifier}[/magenta]? ([magenta]y/n[/magenta]): [/bold]" - ), - ) - return confirmation == "y" - - def _render_console_result( validation_settings: dict, *, diff --git a/rocrate_validator/models/profile_detection.py b/rocrate_validator/models/profile_detection.py index e6e256f56..cffc0a9a3 100644 --- a/rocrate_validator/models/profile_detection.py +++ b/rocrate_validator/models/profile_detection.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -17,7 +18,7 @@ @dataclass(frozen=True) class ProfileDetectionDiagnostic: - """Explain a metadata value that could not be used for profile detection.""" + """Explain a malformed, missing, or unrecognized profile-detection value.""" code: str property: str | None @@ -41,9 +42,7 @@ def detect_rocrate_profiles( ) -> ProfileDetectionResult: """Detect profiles from an existing context and describe unresolved metadata hints.""" - # Keep both the parsed references and malformed raw values: references can - # select profiles, while malformed values are needed for diagnostics and - # may allow a cautious suggestion when a URI was written as a string. + # Keep string URIs as suggestion hints, not valid profile references. hints, malformed_values = _get_profile_hints(context.ro_crate.metadata) candidate_profile_uris = {value.id for _, value in hints if isinstance(value.id, str) and value.id} diagnostics = [ @@ -53,7 +52,11 @@ def detect_rocrate_profiles( location=location, value=value, message=( - "Expected a JSON-LD reference object containing `@id`; " + "Expected a JSON-LD entity reference containing `@id`, not a string. " + f'Use {{"@id": {json.dumps(value)}}} instead. ' + "The metadata has not been changed." + if prop == "conformsTo" and isinstance(value, str) + else "Expected a JSON-LD reference object containing `@id`; " 'for example, {"@id": "https://example.org/profile"}.' ), ) @@ -61,7 +64,7 @@ def detect_rocrate_profiles( ] for diagnostic in diagnostics: logger.warning( - "Cannot use %s `%s` value %r for profile detection: %s", + "Non-conforming %s `%s` value %r: %s", diagnostic.location, diagnostic.property, diagnostic.value, @@ -88,15 +91,12 @@ def detect_rocrate_profiles( ) logger.debug("Unable to determine the profile to validate against") - # A string conformsTo is the one malformed form for which we can try to - # identify the intended profile. Other malformed values cannot match a - # profile, so avoid loading the profile catalog just to return diagnostics. + # Load the catalog only for valid references or potential string suggestions. if not candidate_profile_uris and not has_string_profile_hint: - logger.debug("No usable profile URI or string suggestion candidate found") + logger.debug("No usable profile URI found") return ProfileDetectionResult([], diagnostics) - # Load profiles if valid URIs need matching or a string value might support - # a suggestion. The latter is never treated as a detected profile here. + # Only valid entity references participate in automatic profile selection. if profile_loader is None: available_profiles = Profile.load_profiles( context.profiles_path, @@ -126,6 +126,9 @@ def detect_rocrate_profiles( ) for profile_uri in unmatched_profile_uris: locations = [location for location, value in hints if value.id == profile_uri] + locations.extend( + location for prop, location, value in malformed_values if prop == "conformsTo" and value == profile_uri + ) diagnostics.append( ProfileDetectionDiagnostic( code="PROFILE_NOT_FOUND", @@ -136,8 +139,6 @@ def detect_rocrate_profiles( ) ) - # Suggestions are deliberately separate from detected profiles: callers - # must decide whether to accept one, and validation still checks metadata. suggested_profile = _suggest_profile(candidate_profiles, candidate_profile_uris, diagnostics, available_profiles) result = ProfileDetectionResult(candidate_profiles, diagnostics, suggested_profile) logger.debug("Detailed profile detection result: %s", result) @@ -150,6 +151,10 @@ def _get_profile_hints(metadata) -> tuple[list[tuple[str, ROCrateEntity]], list[ diagnostics: list[tuple[str, str, Any]] = [] try: descriptor = metadata.get_file_descriptor_entity() + except json.JSONDecodeError: + # JSONDecodeError is also a ValueError, but invalid JSON must not be + # misreported as a missing descriptor or missing conformsTo hint. + raise except ValueError: # Without a descriptor there is no reliable source for either the # descriptor's conformsTo or the Root Data Entity's hints. Other @@ -183,7 +188,7 @@ def _suggest_profile( candidates: list[Profile], candidate_profile_uris: set[str], diagnostics: list[ProfileDetectionDiagnostic], - available_profiles: list[Profile], + available_profiles: Sequence[Profile], ) -> Profile | None: """Return one profile only when malformed string form is the sole detection issue.""" string_diagnostics = [diagnostic for diagnostic in diagnostics if diagnostic.code == "INVALID_CONFORMS_TO"] From bb709b995e2bb48bbf98d5ed77dbcf4235d10b81 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Oct 2026 12:51:50 +0200 Subject: [PATCH 228/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20profile=20suggestions=20and=20invalid=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 78 ++++++++++++++++++++++++++++++++++++- tests/unit/test_rocrate.py | 2 +- tests/unit/test_services.py | 42 ++++++++++++++++++-- 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index edd3e670a..13481eb67 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,10 +16,11 @@ import re from io import StringIO from pathlib import Path +from types import SimpleNamespace from unittest.mock import patch from click.testing import CliRunner -from pytest import fixture +from pytest import fixture, mark from rdflib import Graph from rocrate_validator import services @@ -72,6 +73,7 @@ def test_validate_packaging_mode_option(cli_runner: CliRunner, tmp_path: Path): assert incompatible.exit_code != 0 assert "standalone metadata document" in incompatible.output + def test_profile_detection_cli_escapes_metadata_values_in_warning(monkeypatch): """Malformed metadata values containing Rich markup are rendered as plain text.""" detection = ProfileDetectionResult( @@ -102,6 +104,58 @@ def test_profile_detection_cli_escapes_metadata_values_in_warning(monkeypatch): assert "[/]" in output.getvalue() +@mark.parametrize("interactive, confirmation", [(False, None), (True, "y"), (True, "n")]) +def test_profile_detection_cli_confirms_string_profile(monkeypatch, interactive, confirmation): + """A string profile URI is used only after interactive confirmation.""" + output = StringIO() + profile = SimpleNamespace(identifier="ro-crate-1.3", uri="https://w3id.org/ro/crate/1.3") + settings = { + "rocrate_uri": "/tmp/string-profile-cli", + "metadata_only": True, + "metadata_dict": { + "@context": {}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "about": {"@id": "./"}, "conformsTo": profile.uri}, + {"@id": "./", "@type": "Dataset"}, + ], + }, + } + profile.inherited_profiles = [] + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [profile]) + prompts = [] + + def confirm(*args, **kwargs): + prompts.append(kwargs["message"]) + return confirmation + + monkeypatch.setattr("rocrate_validator.cli.commands.validate.get_single_char", confirm) + monkeypatch.setattr("rocrate_validator.cli.commands.validate.multiple_choice", lambda *args: []) + identifiers, autodetection = _resolve_profile_identifiers( + Console(file=output, width=200), + interactive=interactive, + no_auto_profile=False, + available_profiles=[profile], + profile_identifiers=[], + validation_settings=settings, + ) + + accepted = interactive and confirmation == "y" + assert identifiers == (["ro-crate-1.3"] if accepted else ["ro-crate"]) + assert autodetection is accepted + assert len(prompts) == int(interactive) + text = output.getvalue() + assert "WARNING: Non-conforming metadata" in text + assert "RO-Crate Metadata Descriptor" in text + assert 'Use {"@id": "https://w3id.org/ro/crate/1.3"} instead.' in text + if interactive: + assert "The string URI matches the available profile ro-crate-1.3" in text + assert "Use this profile for validation?" in prompts[0] + else: + assert "--profile-identifier" in text + if accepted: + assert "Unable to automatically detect" not in text + + def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): result = cli_runner.invoke( cli, @@ -111,6 +165,28 @@ def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): assert result.exit_code == 1 +@mark.parametrize("use_directory", [False, True]) +def test_validate_invalid_json_reports_syntax_location(cli_runner: CliRunner, tmp_path: Path, use_directory): + """Malformed JSON is explained before attempting to select a profile.""" + metadata = tmp_path / "ro-crate-metadata.json" + metadata.write_text( + '{\n "@graph": [{\n "@id": "ro-crate-metadata.json",\n' + ' "conformsTo": "@id": "https://w3id.org/ro/crate/1.3"\n }]\n}' + ) + + result = cli_runner.invoke(cli, ["validate", str(tmp_path if use_directory else metadata), "--no-paging"]) + + assert result.exit_code == 2 + assert "The RO-Crate metadata is not valid JSON." in result.output + assert "line 4, column 24" in result.output + assert "Expecting ',' delimiter" in result.output + assert '"conformsTo": "@id":' in result.output + assert "^" in result.output + assert "Correct the JSON syntax" in result.output + assert "Unexpected error" not in result.output + assert "Unable to automatically detect" not in result.output + + def test_validate_subcmd_valid_local_folder_rocrate(cli_runner: CliRunner): result = cli_runner.invoke(cli, ["validate", str(ValidROC().wrroc_paper_long_date), "--verbose", "--no-paging"]) assert result.exit_code == 0 diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index 1d2f0c7fd..e11c0cec1 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -644,7 +644,7 @@ def test_conforms_to_profile_detection_warns_and_skips_string_values(entity_id, def test_automatic_profile_detection_reports_string_conforms_to(monkeypatch): - """Automatic detection reports why a literal conformsTo value was not used.""" + """Automatic detection warns about a literal URI without selecting it.""" warnings = [] monkeypatch.setattr( profile_detection_module.logger, diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py index ffb2742f1..fda9e29d7 100644 --- a/tests/unit/test_services.py +++ b/tests/unit/test_services.py @@ -18,6 +18,8 @@ from pathlib import Path from types import SimpleNamespace +import pytest + from rocrate_validator.models import URI, ValidationSettings from rocrate_validator.rocrate import ROCrateMetadata from rocrate_validator.services import ( @@ -75,8 +77,10 @@ def test_detailed_profile_detection_handles_invalid_about_without_raising(monkey assert {diagnostic.code for diagnostic in result.diagnostics} == {"INVALID_REFERENCE", "NO_PROFILE_HINT"} -def test_detailed_profile_detection_suggests_only_matching_string_hint(monkeypatch): - """A sole string hint matching one profile yields a separate suggestion.""" +@pytest.mark.parametrize("location", ["descriptor", "root"]) +@pytest.mark.parametrize("as_list", [False, True]) +def test_detailed_profile_detection_suggests_matching_string_hint(monkeypatch, location, as_list): + """String URIs suggest profiles without selecting them or repairing metadata.""" profile = SimpleNamespace( uri="https://w3id.org/ro/crate/1.2", identifier="ro-crate-1.2", @@ -84,11 +88,43 @@ def test_detailed_profile_detection_suggests_only_matching_string_hint(monkeypat ) monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [profile]) - result = detect_profiles_detailed(_profile_detection_settings({"@id": "./"}, "https://w3id.org/ro/crate/1.2")) + value = [profile.uri] if as_list else profile.uri + settings = _profile_detection_settings({"@id": "./"}) + assert settings.metadata_dict is not None + entity = settings.metadata_dict["@graph"][0 if location == "descriptor" else 1] + entity["conformsTo"] = value + result = detect_profiles_detailed(settings) assert result.profiles == [] + assert detect_profiles(settings) == [] assert result.suggested_profile is profile assert result.diagnostics[0].code == "INVALID_CONFORMS_TO" + assert "not a string" in result.diagnostics[0].message + assert f'{{"@id": "{profile.uri}"}}' in result.diagnostics[0].message + assert entity["conformsTo"] == value + + +def test_profile_detection_does_not_select_strings_alongside_valid_references(monkeypatch): + """Valid references take precedence over malformed string hints.""" + base = SimpleNamespace(uri="https://example.org/base", identifier="base", inherited_profiles=[]) + specific = SimpleNamespace(uri="https://example.org/specific", identifier="specific", inherited_profiles=[base]) + monkeypatch.setattr( + "rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [base, specific] + ) + settings = _profile_detection_settings( + {"@id": "./"}, + [{"@id": base.uri}, specific.uri, "https://example.org/unknown"], + ) + + result = detect_profiles_detailed(settings) + + assert result.profiles == [base] + assert result.suggested_profile is None + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "INVALID_CONFORMS_TO", + "INVALID_CONFORMS_TO", + ] + assert result.diagnostics[-1].location == "RO-Crate Metadata Descriptor" def test_default_profiles_list(): From 585fef3577dcfaf19e52bc70298b3cda141fd38c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Oct 2026 13:20:30 +0200 Subject: [PATCH 229/271] =?UTF-8?q?docs(validation):=20=F0=9F=93=9D=20clar?= =?UTF-8?q?ify=20profile=20suggestions=20and=20JSON=20syntax=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2_usage_cli.rst | 34 +++++++++++++++++++++++++-------- docs/3_usage_api.rst | 42 +++++++++++++++++++++++++++++------------ docs/4_how_it_works.rst | 37 ++++++++++++++++++++++++------------ 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 32e687b85..a4ef298ff 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -51,14 +51,32 @@ Profile detection when ``conformsTo`` is a string Profile detection expects JSON-LD references, for example ``"conformsTo": {"@id": "https://w3id.org/ro/crate/1.2"}``. If a -``conformsTo`` value is instead a string and it uniquely matches an available -profile, interactive validation explains the malformed value and asks whether -to use that profile. Confirming selects the profile for validation; declining -returns to the profile selection prompt. The metadata is not changed, so the -validation report can still include an error for the malformed value. - -In non-interactive mode, the suggestion is not accepted automatically. When -there is no usable profile reference, validation falls back to the base +``conformsTo`` value is instead a string, interactive validation can suggest +the matching profile and ask for confirmation before using it. For example, +``"conformsTo": "https://w3id.org/ro/crate/1.2"`` can suggest the +``ro-crate-1.2`` profile even though the value should be an ``{"@id": "..."}`` +object. The suggestion is offered only if: + +* No ``conformsTo`` value is already a valid entity reference. +* The only problem found during profile detection is that ``conformsTo`` is + written as a string. +* That URI identifies exactly one available validation profile. + +Declining returns to manual profile selection. A warning identifies the affected +metadata entity and shows the correct ``{"@id": "..."}`` form. The metadata is +not changed, so the validation report can still include an error for the +malformed value. + +In non-interactive mode, string suggestions are not accepted automatically. +When no usable profile reference is found, validation falls back to the base ``ro-crate`` profile. Specify ``--profile-identifier`` to choose a profile explicitly, or correct ``conformsTo`` in the metadata descriptor or Root Data Entity. Use ``--no-auto-profile`` to disable profile detection. + +Invalid JSON syntax +------------------- + +Profile auto-detection requires syntactically valid JSON. If the metadata +descriptor contains invalid JSON, auto-detection stops and the CLI reports +the parser's reason, line and column, and the affected source line with a +pointer to the error. Correct the JSON syntax and run validation again. diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 31b2ec1f6..0151bf2b3 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -45,17 +45,35 @@ Programmatic Validation Profile Detection ----------------- -``detect_profiles`` keeps returning the profiles found from valid JSON-LD -references. To inspect why another value was not used, call -``detect_profiles_detailed``. It returns a ``ProfileDetectionResult`` with -``profiles``, ``diagnostics``, and an optional ``suggested_profile``. - -A suggestion is returned only when profile detection found no usable profile -reference, every detection diagnostic concerns a string-valued ``conformsTo``, -all those values are identical, and exactly one available profile has that URI. -Other malformed hints, unknown valid URIs, or multiple possible profiles do -not produce a suggestion. The suggestion does not change the crate metadata -and must be selected explicitly by the calling application. +``detect_profiles`` returns the available validation profiles identified by +``conformsTo`` values written as JSON-LD entity references, such as +``{"@id": "https://w3id.org/ro/crate/1.2"}``. A URI written as a plain string +does not select a profile through this function. + +Use ``detect_profiles_detailed`` to also find out why profile detection could +not use a metadata value, or which profile a string value may have intended. +It returns a ``ProfileDetectionResult`` with three fields: + +* ``profiles``: the same profiles returned by ``detect_profiles``. +* ``diagnostics``: explanations of missing, malformed, or unrecognized values. + For a string-valued ``conformsTo``, an ``INVALID_CONFORMS_TO`` diagnostic shows + the required ``{"@id": "..."}`` form. +* ``suggested_profile``: the profile the crate appears to request when + ``conformsTo`` contains a URI as a string instead of an ``{"@id": "..."}`` + object. The application can choose to validate against this profile despite + the incorrect metadata format. This field is ``None`` unless all of the + following conditions hold: + + * No ``conformsTo`` value is already a valid entity reference. + * Every problem reported in ``diagnostics`` is a ``conformsTo`` value written + as a string; there are no other profile-detection problems. + * All those strings contain the same URI. + * Exactly one available validation profile has that URI. + +For example, ``"conformsTo": "https://w3id.org/ro/crate/1.2"`` can produce +``suggested_profile`` identifying ``ro-crate-1.2``, but that profile is not added +to ``profiles``. The calling application decides whether to use the suggestion +for validation. Neither function changes the metadata or corrects its errors. Each diagnostic has a stable ``code`` as well as the affected property, metadata location, original value, and explanation. Codes distinguish an @@ -77,7 +95,7 @@ invalid reference (``INVALID_REFERENCE``), a malformed ``conformsTo`` value settings["profile_identifier"] = detection.suggested_profile.identifier result = validate(settings) -Programmatic detection does not prompt or silently select a suggestion. If the +Programmatic detection does not prompt. If the application does not set ``profile_identifier``, ``validate`` uses the profile configured in ``ValidationSettings`` (or its default). Selecting a suggested profile does not repair invalid metadata: ``conformsTo`` must still be diff --git a/docs/4_how_it_works.rst b/docs/4_how_it_works.rst index 138f85436..429e9ad10 100644 --- a/docs/4_how_it_works.rst +++ b/docs/4_how_it_works.rst @@ -80,18 +80,31 @@ Validation profile selection - If no precise match is found: - - in **Interactive Mode:** (available only through the CLI) the system - explains why detection failed and prompts the user to select a profile. - If the only issue is a string-valued `conformsTo` that uniquely - identifies an available profile, that profile is offered first and is - used only after the user confirms; - - - in **Non-Interactive Mode:** the system will validate against all profiles - to which the RO-Crate conforms, and for which validation rules are available. - If no usable profile reference is found, it falls back to the base - `ro-crate` profile; a suggestion from a malformed string is never - selected automatically. In all cases, input RO-Crates are validated - against the base `ro-crate` profile. + - in **Interactive Mode** (CLI only), the validator explains why it could + not select a profile automatically. If `conformsTo` contains a profile + URI as a string, it may ask whether to use the matching profile anyway. + The warning shows how to replace the string with an entity reference + such as `{"@id": "https://w3id.org/ro/crate/1.2"}`. + + This suggestion is offered only if all of the following are true: + + * No `conformsTo` value is already a valid entity reference. + * The only problems found during profile detection are `conformsTo` + values written as strings. + * All those strings contain the same URI. + * Exactly one available validation profile has that URI. + + Accepting the suggestion selects that profile for validation. Declining + it, or having no suggestion, opens the manual profile selection prompt. + + - in **Non-Interactive Mode**, the validator cannot ask for confirmation, + so it does not use a profile suggested by a string-valued `conformsTo`. + Use `--profile-identifier` to select that profile explicitly in the CLI. + If no profile is selected explicitly or detected from valid references, + validation uses the base `ro-crate` profile. + + Profiles detected from valid references are used in either mode. The base + `ro-crate` rules are also applied through profile inheritance. Selecting a profile only chooses which rules to run. It does not repair metadata: a string-valued `conformsTo` remains malformed and can still From 8b0d1fcd39c77e86602ffe40aceba34545e9bfc5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Oct 2026 15:16:42 +0200 Subject: [PATCH 230/271] =?UTF-8?q?refactor(validation,tests,docs):=20?= =?UTF-8?q?=F0=9F=94=8E=20rename=20diagnostic=20checks=20to=20extra=20chec?= =?UTF-8?q?ks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/11_writing_a_profile.rst | 24 +++---- docs/2_usage_cli.rst | 14 ++-- docs/3_usage_api.rst | 10 +-- rocrate_validator/cli/commands/validate.py | 26 ++++---- rocrate_validator/models/__init__.py | 4 +- rocrate_validator/models/requirement.py | 18 +++--- rocrate_validator/models/result.py | 34 +++++----- rocrate_validator/models/settings.py | 4 +- rocrate_validator/models/statistics.py | 64 +++++++++---------- rocrate_validator/models/validation.py | 6 +- .../1.2/must/0_file_descriptor_format.py | 10 +-- .../requirements/python/__init__.py | 16 ++--- .../io_helpers/output/json/formatters.py | 4 +- .../io_helpers/output/text/formatters.py | 34 +++++----- .../ro-crate/test_file_descriptor_format.py | 50 +++++++-------- tests/test_cli.py | 8 +-- tests/unit/requirements/test_profiles.py | 10 +-- tests/unit/test_validation_settings.py | 2 +- 18 files changed, 169 insertions(+), 169 deletions(-) diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index d1bbdcf9b..ffb5ff86b 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -167,7 +167,7 @@ kind of identity as the SHACL example above: return None if not root_entity.get_property("name"): - # A failed check normally records the diagnostic and returns + # A failed check normally records the extra_check and returns # False. The check framework associates the issue with self. context.result.add_issue("The root data entity has no name", self) return False @@ -180,19 +180,19 @@ three equivalent forms: ``True``/``False`` are normalized to passed/failed, ``CheckResult.SKIPPED`` (or the other ``CheckResult`` members) expresses an explicit status, and ``None`` is normalized to skipped. When a check fails, call ``context.result.add_issue(...)`` before returning ``False`` so that the -result contains an actionable diagnostic. When it is skipped, prefer +result contains an actionable extra_check. When it is skipped, prefer ``context.record_skip(...)`` when a reason and category should be exposed to API and CLI consumers. -Diagnostic Python checks +`ExtracCheck` Python checks ~~~~~~~~~~~~~~~~~~~~~~~~ -Use the ``diagnostic=True`` argument to ``@check`` for advisory checks that +Use the ``extra_check=True`` argument to ``@check`` for advisory checks that report suspicious patterns without deciding whether a crate conforms to its -profile. Diagnostic checks are disabled by default and can be enabled by users -with ``--include-diagnostic-checks`` or the -``ValidationSettings.include_diagnostic_checks`` setting. A diagnostic check -must record each finding with ``context.result.add_diagnostic(...)``; do not +profile. `ExtracCheck` checks are disabled by default and can be enabled by users +with ``--extra-checks`` or the +``ValidationSettings.extra_checks`` setting. A extra_check check +must record each finding with ``context.result.add_extra_check(...)``; do not use ``add_issue(...)`` for an advisory finding, because issues contribute to the conformance result. @@ -200,11 +200,11 @@ For example: .. code-block:: python - @check(name="Possible missing entity link", diagnostic=True) + @check(name="Possible missing entity link", extra_check=True) def check_entity_link(self, context: ValidationContext) -> bool: entity = ... # inspect the entity using the validation context if suspicious_link(entity): - context.result.add_diagnostic( + context.result.add_extra_check( self, "This value may refer to an entity that is not declared in the crate.", violating_entity=entity.get("@id"), @@ -213,8 +213,8 @@ For example: return True The return value still follows the regular Python check contract, but a -diagnostic check's failed status does not fail conformance. Findings are -available from ``ValidationResult.diagnostics`` and are presented separately +extra_check check's failed status does not fail conformance. Findings are +available from ``ValidationResult.extra_checks`` and are presented separately from conformance issues. For the user-facing options and result fields, see :ref:`cli-based-validation` and :ref:`programmatic-validation`. diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 2cd95622e..52c2c39f0 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -35,21 +35,21 @@ CLI Validation :end-before: ## Programmatic Validation -Diagnostic Checks +`ExtracCheck` Checks ----------------- -Diagnostic checks are advisory checks that can report suspicious metadata +`ExtracCheck` checks are advisory checks that can report suspicious metadata patterns without affecting whether an RO-Crate conforms to its profile. They -are disabled by default. To run all checks classified as diagnostic, pass -``--include-diagnostic-checks`` to ``validate``: +are disabled by default. To run all checks classified as extra_check, pass +``--extra-checks`` to ``validate``: .. code-block:: console - rocrate-validator validate --include-diagnostic-checks + rocrate-validator validate --extra-checks -To omit one diagnostic check while running the others, combine this option with +To omit one extra_check check while running the others, combine this option with ``--skip-checks`` and the check's fully qualified identifier. The validation -report lists diagnostic checks and their findings separately from conformance +report lists extra_check checks and their findings separately from conformance issues. .. seealso:: diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 1352a8b7f..77d4a3c4a 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -31,11 +31,11 @@ Programmatic Validation :end-before: ## Running the tests -Diagnostic Checks +`ExtracCheck`s ----------------- -Diagnostic checks are advisory and do not affect the conformance result. They -are disabled by default; set ``include_diagnostic_checks`` on +`ExtracCheck` checks are advisory and do not affect the conformance result. They +are disabled by default; set ``extra_checks`` on ``ValidationSettings`` to run them: .. code-block:: python @@ -46,12 +46,12 @@ are disabled by default; set ``include_diagnostic_checks`` on rocrate_uri="path/to/rocrate", profile_identifier="ro-crate-1.3", requirement_severity=models.Severity.REQUIRED, - include_diagnostic_checks=True, + extra_checks=True, ) result = services.validate(settings) The resulting ``ValidationResult`` exposes these reports separately through -``result.diagnostics``. Diagnostic checks are also listed separately in +``result.extra_checks``. `ExtracCheck` checks are also listed separately in ``result.statistics``. .. seealso:: diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c5218ff3d..8159f4422 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -36,7 +36,7 @@ from rocrate_validator.utils.io_helpers.output.console import Console from rocrate_validator.utils.io_helpers.output.json import JSONOutputFormatter from rocrate_validator.utils.io_helpers.output.text import TextOutputFormatter -from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationDiagnosticsTextOutputFormatter +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationExtraChecksTextOutputFormatter from rocrate_validator.utils.io_helpers.output.text.layout.report import LiveTextProgressLayout, get_app_header_rule from rocrate_validator.utils.paths import get_profiles_path from rocrate_validator.utils.uri import validate_rocrate_uri @@ -102,9 +102,9 @@ def validate_uri(ctx, param, value): show_default=True, ) @click.option( - "--include-diagnostic-checks", + "--extra-checks", is_flag=True, - help="Run all checks marked as diagnostic; findings do not affect conformance", + help="Run all checks marked as extra_check; findings do not affect conformance", default=False, show_default=True, ) @@ -283,7 +283,7 @@ def validate( creation_time: bool = False, enforce_availability: bool = False, skip_availability_check: bool = False, - include_diagnostic_checks: bool = False, + extra_checks: bool = False, no_auto_profile: bool = False, disable_profile_inheritance: bool = False, no_profile_checks: bool = False, @@ -369,7 +369,7 @@ def validate( "creation_time": creation_time, "enforce_availability": enforce_availability, "skip_availability_check": skip_availability_check, - "include_diagnostic_checks": include_diagnostic_checks, + "extra_checks": extra_checks, "validate_profile_checks": not no_profile_checks, } @@ -623,17 +623,17 @@ def _render_console_result( ) if verbose_choice == "y" or verbose: command_view.display_validation_result(result) - elif result.diagnostics: - console.print(ValidationDiagnosticsTextOutputFormatter(result)) - elif result.diagnostics: - console.print(ValidationDiagnosticsTextOutputFormatter(result)) + elif result.extra_checks: + console.print(ValidationExtraChecksTextOutputFormatter(result)) + elif result.extra_checks: + console.print(ValidationExtraChecksTextOutputFormatter(result)) return result result = services.validate(validation_settings) console.register_formatter(TextOutputFormatter()) console.print(result.statistics) - if result.diagnostics and (result.passed() or not verbose): - console.print(ValidationDiagnosticsTextOutputFormatter(result)) + if result.extra_checks and (result.passed() or not verbose): + console.print(ValidationExtraChecksTextOutputFormatter(result)) if not result.passed() and verbose: out = Console(no_color=console.no_color, width=console.width, height=console.height) out.register_formatter(TextOutputFormatter()) @@ -676,8 +676,8 @@ def _render_file_or_collected_result( out = Console(color_system=None, width=output_line_width, height=31, file=f) out.register_formatter(TextOutputFormatter()) out.print(result.statistics) # Output the statistics first - if result.diagnostics and (result.passed() or not verbose): - out.print(ValidationDiagnosticsTextOutputFormatter(result)) + if result.extra_checks and (result.passed() or not verbose): + out.print(ValidationExtraChecksTextOutputFormatter(result)) if not result.passed() and verbose: out.print(result) if interactive: diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index 7efbba62a..b2e6d4d2a 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -34,7 +34,7 @@ RequirementLoader, SourceSnippet, ) -from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationDiagnostic, ValidationResult +from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationExtraCheck, ValidationResult from rocrate_validator.models.settings import ( DEFAULT_PROFILES_PATH, BaseTypes, @@ -87,7 +87,7 @@ "SkippedCheckDetail", "SourceSnippet", "ValidationContext", - "ValidationDiagnostic", + "ValidationExtraCheck", "ValidationEvent", "ValidationResult", "ValidationSettings", diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 14dda9b9a..cede9fa01 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -442,7 +442,7 @@ def __execute_check__( # Python check twice. recorded_result = context.result.get_check_result(check) if recorded_result is not None: - failed = recorded_result is CheckResult.FAILED and not check.diagnostic + failed = recorded_result is CheckResult.FAILED and not check.extra_check return all_passed and not failed, failed and context.fail_fast execution_check = self.__resolve_execution_check__(check, context) @@ -497,10 +497,10 @@ def __execute_check__( check.identifier, normalized_result, ) - new_all_passed = all_passed and (check.diagnostic or normalized_result is not CheckResult.FAILED) + new_all_passed = all_passed and (check.extra_check or normalized_result is not CheckResult.FAILED) # A batched backend can also report failures for checks other than the # one driving this execution. - should_break = not check.diagnostic and context.fail_fast and ( + should_break = not check.extra_check and context.fail_fast and ( normalized_result is CheckResult.FAILED or context.result.has_issues() ) return new_all_passed and not should_break, should_break @@ -903,7 +903,7 @@ def __init__( *, deactivated: bool = False, depends_on: tuple[str, ...] | None = None, - diagnostic: bool = False, + extra_check: bool = False, ): self._requirement: Requirement = requirement self._order_number = 0 @@ -913,7 +913,7 @@ def __init__( self._hidden = hidden self._deactivated = deactivated self._depends_on = tuple(depends_on or ()) - self._diagnostic = diagnostic + self._extra_check = extra_check @property def order_number(self) -> int: @@ -958,9 +958,9 @@ def severity(self) -> Severity: return self.level.severity @property - def diagnostic(self) -> bool: - """Whether this check reports advisory diagnostics instead of conformance issues.""" - return self._diagnostic + def extra_check(self) -> bool: + """Whether this check reports advisory extra_checks instead of conformance issues.""" + return self._extra_check @property def overridden_by(self) -> list[RequirementCheck]: @@ -1019,7 +1019,7 @@ def to_dict(self, with_requirement: bool = True, with_profile: bool = True) -> d "name": self.name, "description": self.description, "severity": self.severity.name, - "diagnostic": self.diagnostic, + "extra_check": self.extra_check, "depends_on": list(self.depends_on), } if with_requirement: diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 3554df48d..81b51d532 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -194,8 +194,8 @@ def to_json( ) -class ValidationDiagnostic: - """A non-blocking diagnostic produced by an optional heuristic check.""" +class ValidationExtraCheck: + """A non-blocking extra_check produced by an optional heuristic check.""" def __init__( self, @@ -224,10 +224,10 @@ def to_dict(self) -> dict[str, str | None]: } def __repr__(self) -> str: - return f"ValidationDiagnostic(check={self.identifier!r}, message={self.message!r})" + return f"ValidationExtraCheck(check={self.identifier!r}, message={self.message!r})" def __eq__(self, other: object) -> bool: - return isinstance(other, ValidationDiagnostic) and self.to_dict() == other.to_dict() + return isinstance(other, ValidationExtraCheck) and self.to_dict() == other.to_dict() __hash__ = None # type: ignore[assignment] @@ -256,7 +256,7 @@ def __init__(self, context: ValidationContext): self._validation_settings: ValidationSettings = context.settings # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] - self._diagnostics: list[ValidationDiagnostic] = [] + self._extra_checks: list[ValidationExtraCheck] = [] # keep track of the checks that have been executed self._executed_checks: set[RequirementCheck] = set() self._executed_checks_results: dict[str, bool] = {} @@ -412,19 +412,19 @@ def issues(self) -> list[CheckIssue]: return self._issues.copy() @property - def diagnostics(self) -> list[ValidationDiagnostic]: + def extra_checks(self) -> list[ValidationExtraCheck]: """Non-blocking heuristic findings, separate from conformance issues.""" - return self._diagnostics.copy() + return self._extra_checks.copy() - def add_diagnostic( + def add_extra_check( self, check: RequirementCheck, message: str, violating_entity: str | None = None, violating_property: str | None = None, value: str | None = None, - ) -> ValidationDiagnostic: - diagnostic = ValidationDiagnostic( + ) -> ValidationExtraCheck: + extra_check = ValidationExtraCheck( check, message, self.context, @@ -432,8 +432,8 @@ def add_diagnostic( violating_property=violating_property, value=value, ) - self._diagnostics.append(diagnostic) - return diagnostic + self._extra_checks.append(extra_check) + return extra_check def get_issues(self, min_severity: Severity | None = None) -> list[CheckIssue]: """ @@ -482,8 +482,8 @@ def add_issue( violatingProperty(Optional[str]): The property that caused the issue (if any) violatingPropertyValue(Optional[str]): The value of the violatingProperty (if any) """ - if check.diagnostic: - raise ValueError(f"Diagnostic check '{check.name}' cannot add a conformance issue") + if check.extra_check: + raise ValueError(f"ExtraCheck check '{check.name}' cannot add a conformance issue") c = CheckIssue( check, message, @@ -538,9 +538,9 @@ def __repr__(self): def __eq__(self, other: object) -> bool: if not isinstance(other, ValidationResult): raise TypeError(f"Cannot compare ValidationResult with {type(other)}") - return self._issues == other._issues and self._diagnostics == other._diagnostics + return self._issues == other._issues and self._extra_checks == other._extra_checks - # Equality is based on mutable issue and diagnostic lists, so instances are + # Equality is based on mutable issue and extra_check lists, so instances are # intentionally unhashable (a content-based hash would be unstable). __hash__ = None # type: ignore[assignment] @@ -562,7 +562,7 @@ def to_dict(self) -> dict: "validation_settings": validation_settings, "passed": self.passed(cast("Severity", self.context.settings.requirement_severity)), "issues": [issue.to_dict() for issue in self.issues], - "diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics], + "extra_checks": [extra_check.to_dict() for extra_check in self.extra_checks], "skipped_checks": self.skipped_checks_count, "skipped_check_details": [detail.to_dict() for detail in self.skipped_check_details], } diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index 127ae656d..d6f015c8d 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -88,8 +88,8 @@ class ValidationSettings: disable_check_for_duplicates: bool = False #: Checks to skip skip_checks: list[str] | None = None - #: Run checks classified as diagnostic, without affecting conformance - include_diagnostic_checks: bool = False + #: Run checks classified as extra_check, without affecting conformance + extra_checks: bool = False #: Flag to validate only the metadata of the RO-Crate metadata_only: bool = False #: RO-Crate metadata as dictionary diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index bd79f6733..d6721974f 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -212,23 +212,23 @@ def total_checks(self) -> int: return len(self._stats.get("checks", [])) @property - def diagnostic_checks(self) -> list[RequirementCheck]: - """Get all diagnostic checks available to this validation.""" - return self._stats.get("diagnostic_checks", []) + def extra_check_checks(self) -> list[RequirementCheck]: + """Get all extra_check checks available to this validation.""" + return self._stats.get("extra_check_checks", []) @property - def executed_diagnostic_checks(self) -> list[RequirementCheck]: - """Get diagnostic checks that ran, regardless of their findings.""" - return self._stats.get("executed_diagnostic_checks", []) + def executed_extra_check_checks(self) -> list[RequirementCheck]: + """Get extra_check checks that ran, regardless of their findings.""" + return self._stats.get("executed_extra_check_checks", []) @property - def skipped_diagnostic_checks(self) -> list[RequirementCheck]: - """Get diagnostic checks that were disabled or skipped.""" - return self._stats.get("skipped_diagnostic_checks", []) + def skipped_extra_check_checks(self) -> list[RequirementCheck]: + """Get extra_check checks that were disabled or skipped.""" + return self._stats.get("skipped_extra_check_checks", []) @property - def total_diagnostic_checks(self) -> int: - return len(self.diagnostic_checks) + def total_extra_check_checks(self) -> int: + return len(self.extra_check_checks) @property def validated_profiles(self) -> list[Profile]: @@ -308,7 +308,7 @@ def __collect_requirement_checks__( requirement_checks = [ _ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) - if not _.diagnostic + if not _.extra_check and (not _.overridden or _.requirement.profile.identifier == target_profile_identifier) ] num_checks = len(requirement_checks) @@ -352,7 +352,7 @@ def __initialise__(cls, validation_settings: ValidationSettings): # Initialize the counters checks_by_severity: dict[Severity, set[RequirementCheck]] = {} checks: set[RequirementCheck] = set() - diagnostic_checks: set[RequirementCheck] = set() + extra_check_checks: set[RequirementCheck] = set() requirements: set[Requirement] = set() # Initialize the counters @@ -375,10 +375,10 @@ def __initialise__(cls, validation_settings: ValidationSettings): if requirement.hidden: continue - diagnostic_checks.update( + extra_check_checks.update( check for check in requirement.get_checks() - if check.diagnostic + if check.extra_check and (not check.overridden or check.requirement.profile.identifier == target_profile_identifier) ) @@ -415,7 +415,7 @@ def __initialise__(cls, validation_settings: ValidationSettings): "profiles": profiles, "requirements": requirements, "checks": checks, - "diagnostic_checks": diagnostic_checks, + "extra_check_checks": extra_check_checks, "severity": severity_validation, "checks_by_severity": checks_by_severity, "failed_requirements": [], @@ -428,8 +428,8 @@ def __initialise__(cls, validation_settings: ValidationSettings): "validated_profiles": [], "validated_requirements": [], "validated_checks": [], - "executed_diagnostic_checks": [], - "skipped_diagnostic_checks": [], + "executed_extra_check_checks": [], + "skipped_extra_check_checks": [], } logger.debug(result) return result @@ -477,16 +477,16 @@ def __record_check_status__(self, check: RequirementCheck, result: CheckResult) def __handle_requirement_check_validation_end__(self, event: Event, ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementCheckValidationEvent) assert ctx is not None - if event.requirement_check in self.diagnostic_checks: + if event.requirement_check in self.extra_check_checks: if event.validation_result is not None: - for key in ("executed_diagnostic_checks", "skipped_diagnostic_checks"): + for key in ("executed_extra_check_checks", "skipped_extra_check_checks"): self._stats[key] = [ check for check in self._stats[key] if check.identifier != event.requirement_check.identifier ] result = normalize_check_result(event.validation_result) - status = "skipped_diagnostic_checks" if result is CheckResult.SKIPPED else "executed_diagnostic_checks" + status = "skipped_extra_check_checks" if result is CheckResult.SKIPPED else "executed_extra_check_checks" self._stats[status].append(event.requirement_check) self.notify_listeners() return @@ -567,12 +567,12 @@ def to_dict(self) -> dict: "total_failed_checks": len(self.failed_checks), "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, - "diagnostic_checks": { - "count": self.total_diagnostic_checks, - "executed": len(self.executed_diagnostic_checks), - "skipped": len(self.skipped_diagnostic_checks), - "findings": len(self.validation_result.diagnostics) if self.validation_result else 0, - "identifiers": sorted(check.identifier for check in self.diagnostic_checks), + "extra_check_checks": { + "count": self.total_extra_check_checks, + "executed": len(self.executed_extra_check_checks), + "skipped": len(self.skipped_extra_check_checks), + "findings": len(self.validation_result.extra_checks) if self.validation_result else 0, + "identifiers": sorted(check.identifier for check in self.extra_check_checks), }, # Requirements involved "requirements": { @@ -672,12 +672,12 @@ def to_dict(self) -> dict: "total_failed_checks": len(self.failed_checks), "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, - "diagnostic_checks": { - "count": sum(stats.total_diagnostic_checks for stats in self._statistics_list), - "executed": sum(len(stats.executed_diagnostic_checks) for stats in self._statistics_list), - "skipped": sum(len(stats.skipped_diagnostic_checks) for stats in self._statistics_list), + "extra_check_checks": { + "count": sum(stats.total_extra_check_checks for stats in self._statistics_list), + "executed": sum(len(stats.executed_extra_check_checks) for stats in self._statistics_list), + "skipped": sum(len(stats.skipped_extra_check_checks) for stats in self._statistics_list), "findings": sum( - len(stats.validation_result.diagnostics) + len(stats.validation_result.extra_checks) for stats in self._statistics_list if stats.validation_result is not None ), diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index b7f4c2873..a56ca10fe 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -744,7 +744,7 @@ def is_check_skipped(self, check: RequirementCheck) -> bool: while exposing a target-local effective identifier. Accepting either identity lets callers address the check without mutating its provenance. """ - if check.diagnostic and not self.settings.include_diagnostic_checks: + if check.extra_check and not self.settings.extra_checks: return True skipped_identifiers = self.settings.skip_checks if not skipped_identifiers: @@ -754,8 +754,8 @@ def is_check_skipped(self, check: RequirementCheck) -> bool: def check_skip_reason(self, check: RequirementCheck) -> str: """Describe why validation settings exclude a check.""" - if check.diagnostic and not self.settings.include_diagnostic_checks: - return "diagnostic checks are disabled; use --include-diagnostic-checks to enable them" + if check.extra_check and not self.settings.extra_checks: + return "extra_check checks are disabled; use --extra-checks to enable them" return "Check was skipped by validation settings" def effective_check_replacement(self, check: RequirementCheck) -> RequirementCheck | None: diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index b73e98c84..c9968b8c8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -540,9 +540,9 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke return None @check( - name="Heuristic diagnostics for suspicious entity references", + name="Heuristic extra_checks for suspicious entity references", depends_on=("File Descriptor JSON format",), - diagnostic=True, + extra_check=True, ) def check_entity_references(self, context: ValidationContext) -> CheckResultValue: try: @@ -557,7 +557,7 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu continue message = self._find_reference_issue(value, entity_id, identifiers, key) if message: - context.result.add_diagnostic( + context.result.add_extra_check( self, message, violating_entity=entity_id, @@ -574,8 +574,8 @@ def check_entity_references(self, context: ValidationContext) -> CheckResultValu return CheckResult.SKIPPED except _HANDLED_METADATA_ERRORS as error: if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unable to inspect metadata for heuristic diagnostics") - context.record_skip(self, f"unable to inspect metadata for heuristic diagnostics: {error}", "exception") + logger.exception("Unable to inspect metadata for heuristic extra_checks") + context.record_skip(self, f"unable to inspect metadata for heuristic extra_checks: {error}", "exception") return CheckResult.SKIPPED @check( diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 4146aae7a..96b440be7 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -55,7 +55,7 @@ def __init__( level: RequirementLevel | None = LevelCollection.REQUIRED, deactivated: bool = False, depends_on: tuple[str, ...] | None = None, - diagnostic: bool = False, + extra_check: bool = False, ): """ check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. @@ -67,7 +67,7 @@ def __init__( level=level, deactivated=deactivated, depends_on=tuple(getattr(check_function, "depends_on", depends_on or ())), - diagnostic=diagnostic, + extra_check=extra_check, ) sig = inspect.signature(check_function) @@ -183,7 +183,7 @@ def __init_checks__(self): severity = self.severity_from_path or Severity.REQUIRED logger.debug("Severity log: %r", severity) deactivated = bool(getattr(member, "deactivated", False)) - diagnostic = bool(getattr(member, "diagnostic", False)) + extra_check = bool(getattr(member, "extra_check", False)) # pylint: disable-next=redefined-outer-name # local 'check' mirrors the decorator name check = self.requirement_check_class( self, @@ -192,7 +192,7 @@ def __init_checks__(self): description=check_description, level=LevelCollection.get(severity.name) if severity else None, deactivated=deactivated, - diagnostic=diagnostic, + extra_check=extra_check, ) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -239,7 +239,7 @@ def check( severity: Severity | None = None, deactivated: bool = False, depends_on: tuple[str, ...] | None = None, - diagnostic: bool = False, + extra_check: bool = False, ): """ A decorator to mark a function as a check. @@ -266,8 +266,8 @@ def check( :type deactivated: bool :param depends_on: names of checks that must run before this check. :type depends_on: Optional[Tuple[str, ...]] - :param diagnostic: mark this as an advisory check that does not determine conformance. - :type diagnostic: bool + :param extra_check: mark this as an advisory check that does not determine conformance. + :type extra_check: bool :return: the decorated function :rtype: Callable """ @@ -303,7 +303,7 @@ def decorator(func): func.severity = severity func.deactivated = deactivated func.depends_on = depends_on or () - func.diagnostic = diagnostic + func.extra_check = extra_check return func return decorator diff --git a/rocrate_validator/utils/io_helpers/output/json/formatters.py b/rocrate_validator/utils/io_helpers/output/json/formatters.py index 1fb2be2df..13244f2e8 100644 --- a/rocrate_validator/utils/io_helpers/output/json/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/json/formatters.py @@ -73,7 +73,7 @@ def format_validation_results( json_output["passed"] = True json_output["skipped_checks"] = 0 json_output["skipped_check_details"] = [] - json_output["diagnostics"] = [] + json_output["extra_checks"] = [] # Initialize the profile results dictionary results_key = "validation_results_by_profile" @@ -99,7 +99,7 @@ def format_validation_results( if "issues" not in json_output: json_output["issues"] = [] json_output["issues"].extend(result_dict.get("issues", [])) - json_output["diagnostics"].extend(result_dict.get("diagnostics", [])) + json_output["extra_checks"].extend(result_dict.get("extra_checks", [])) # Add overall statistics stats_dict = _compute_overall_statistics(results, verbose=verbose) diff --git a/rocrate_validator/utils/io_helpers/output/text/formatters.py b/rocrate_validator/utils/io_helpers/output/text/formatters.py index 800b079cf..fb8eb0b4e 100644 --- a/rocrate_validator/utils/io_helpers/output/text/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/text/formatters.py @@ -97,23 +97,23 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR yield Padding("\n", (0, 0)) yield Padding("\n", (0, 0)) - yield from self._render_diagnostics(result) + yield from self._render_extra_checks(result) @staticmethod - def _render_diagnostics(result: ValidationResult) -> RenderResult: - diagnostics = result.diagnostics - if not diagnostics: + def _render_extra_checks(result: ValidationResult) -> RenderResult: + extra_checks = result.extra_checks + if not extra_checks: return - finding_count = len(diagnostics) + finding_count = len(extra_checks) finding_label = "finding" if finding_count == 1 else "findings" - check_count = len({diagnostic.identifier for diagnostic in diagnostics}) + check_count = len({extra_check.identifier for extra_check in extra_checks}) caption = Text(f"{finding_count} {finding_label}: ") if finding_count == 1: caption.append("this finding is advisory and does not affect conformance.", style="dim italic") else: caption.append("findings are advisory and do not affect conformance.", style="dim italic") table = Table( - title=f"Diagnostic Checks ({check_count})", + title=f"ExtraCheck Checks ({check_count})", title_style="bold orange1", border_style="orange1", header_style="bold orange1", @@ -128,14 +128,14 @@ def _render_diagnostics(result: ValidationResult) -> RenderResult: table.add_column("Value", style="green", overflow="fold") table.add_column("Finding", overflow="fold") - for diagnostic in diagnostics: + for extra_check in extra_checks: table.add_row( - Text(result.context.effective_check_profile(diagnostic.check).identifier, style="cyan"), - Text(diagnostic.identifier, style=get_severity_color(diagnostic.check.severity)), - diagnostic.violating_entity or "—", - diagnostic.violating_property or "—", - f'"{diagnostic.value}"' if diagnostic.value is not None else "—", - Text(diagnostic.message), + Text(result.context.effective_check_profile(extra_check.check).identifier, style="cyan"), + Text(extra_check.identifier, style=get_severity_color(extra_check.check.severity)), + extra_check.violating_entity or "—", + extra_check.violating_property or "—", + f'"{extra_check.value}"' if extra_check.value is not None else "—", + Text(extra_check.message), ) yield Padding(table, (1, 2)) @@ -157,11 +157,11 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR yield Padding("\n", (0, 0)) -class ValidationDiagnosticsTextOutputFormatter(OutputFormatter): - """Render advisory diagnostics independently from conformance details.""" +class ValidationExtraChecksTextOutputFormatter(OutputFormatter): + """Render advisory extra_checks independently from conformance details.""" def __init__(self, validation_result: ValidationResult): self._validation_result = validation_result def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: - yield from ValidationResultTextOutputFormatter._render_diagnostics(self._validation_result) + yield from ValidationResultTextOutputFormatter._render_extra_checks(self._validation_result) diff --git a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py index 75e8f415d..4a4c42ded 100644 --- a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py @@ -62,7 +62,7 @@ def _crate_with_suspicious_string_reference(tmp_path: Path, version: str) -> Pat @pytest.mark.parametrize("version", ["1.2", "1.3"]) -def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str): +def test_extra_check_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str): crate = _crate_with_suspicious_string_reference(tmp_path, version) settings = models.ValidationSettings( rocrate_uri=models.URI(crate), @@ -72,42 +72,42 @@ def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: default_result = services.validate(settings) assert default_result.passed() - assert default_result.diagnostics == [] + assert default_result.extra_checks == [] heuristic_check = next( check - for check in default_result.statistics.diagnostic_checks + for check in default_result.statistics.extra_check_checks if "suspicious entity references" in check.name.lower() ) assert heuristic_check.severity == models.Severity.REQUIRED - assert heuristic_check.diagnostic + assert heuristic_check.extra_check assert heuristic_check not in default_result.statistics.checks conformance_check_count = default_result.statistics.total_checks assert default_result.get_check_result(heuristic_check) is models.CheckResult.SKIPPED - settings.include_diagnostic_checks = True - diagnostic_result = services.validate(settings) - assert diagnostic_result.passed() - assert diagnostic_result.statistics.total_checks == conformance_check_count - assert diagnostic_result.statistics.executed_diagnostic_checks == [heuristic_check] - assert len(diagnostic_result.diagnostics) >= 2, ( - diagnostic_result.validation_settings.include_diagnostic_checks, + settings.extra_checks = True + extra_check_result = services.validate(settings) + assert extra_check_result.passed() + assert extra_check_result.statistics.total_checks == conformance_check_count + assert extra_check_result.statistics.executed_extra_check_checks == [heuristic_check] + assert len(extra_check_result.extra_checks) >= 2, ( + extra_check_result.validation_settings.extra_checks, [ - (check.name, diagnostic_result.get_check_result(check)) - for check in diagnostic_result.statistics.checks + (check.name, extra_check_result.get_check_result(check)) + for check in extra_check_result.statistics.checks if "suspicious entity references" in check.name.lower() ], - [detail.message for detail in diagnostic_result.skipped_check_details], + [detail.message for detail in extra_check_result.skipped_check_details], ) - assert {diagnostic.violating_property for diagnostic in diagnostic_result.diagnostics} >= { + assert {extra_check.violating_property for extra_check in extra_check_result.extra_checks} >= { "license", "customReference", } - assert diagnostic_result.to_dict()["diagnostics"] + assert extra_check_result.to_dict()["extra_checks"] console = Console(record=True, color_system=None, width=120) - console.print(ValidationResultTextOutputFormatter(diagnostic_result)) + console.print(ValidationResultTextOutputFormatter(extra_check_result)) text_report = console.export_text() - assert "Diagnostic Checks (1)" in text_report + assert "ExtraCheck Checks (1)" in text_report assert "customReference" in text_report cli_result = CliRunner().invoke( @@ -118,7 +118,7 @@ def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str(crate), "--profile-identifier", f"ro-crate-{version}", - "--include-diagnostic-checks", + "--extra-checks", "--output-format", "json", "--no-paging", @@ -127,7 +127,7 @@ def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: assert cli_result.exit_code == 0, cli_result.output cli_report = json.loads(cli_result.output) assert cli_report["passed"] - assert cli_report["diagnostics"] + assert cli_report["extra_checks"] text_cli_result = CliRunner().invoke( cli, @@ -137,12 +137,12 @@ def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str(crate), "--profile-identifier", f"ro-crate-{version}", - "--include-diagnostic-checks", + "--extra-checks", "--no-paging", ], ) assert text_cli_result.exit_code == 0, text_cli_result.output - assert "Diagnostic Checks (1)" in text_cli_result.output + assert "ExtraCheck Checks (1)" in text_cli_result.output assert "customReference" in text_cli_result.output skipped_result = CliRunner().invoke( @@ -153,7 +153,7 @@ def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str(crate), "--profile-identifier", f"ro-crate-{version}", - "--include-diagnostic-checks", + "--extra-checks", "--skip-checks", heuristic_check.identifier, "--output-format", @@ -163,7 +163,7 @@ def test_diagnostic_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: ) assert skipped_result.exit_code == 0, skipped_result.output skipped_report = json.loads(skipped_result.output) - assert skipped_report["diagnostics"] == [] + assert skipped_report["extra_checks"] == [] @pytest.mark.parametrize("version", ["1.2", "1.3"]) @@ -183,7 +183,7 @@ def test_required_about_reference_violation_is_detected_without_heuristic(tmp_pa ) ) assert not result.passed() - assert result.diagnostics == [] + assert result.extra_checks == [] def test_missing_file_descriptor(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 1e5071f54..9a95fa08a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -87,7 +87,7 @@ def fail_validation(*args, **kwargs): def test_validate_subcmd_profile_syntax_error_is_concise_without_debug(cli_runner: CliRunner, monkeypatch): - """Parser failures must show a concise diagnostic without a traceback by default.""" + """Parser failures must show a concise extra_check without a traceback by default.""" def fail_validation(*args, **kwargs): raise BadSyntaxError( @@ -218,7 +218,7 @@ def mock_validate(*args, **kwargs): ) -def test_validate_include_diagnostic_checks_option(cli_runner: CliRunner): +def test_validate_extra_checks_option(cli_runner: CliRunner): called_args: list = [] def mock_validate(*args, **kwargs): @@ -233,14 +233,14 @@ def mock_validate(*args, **kwargs): "--no-interactive", "validate", str(ValidROC().sort_and_change_archive), - "--include-diagnostic-checks", + "--extra-checks", "--no-paging", ], ) assert result.exit_code == 2 settings = called_args[0] - assert settings["include_diagnostic_checks"] is True + assert settings["extra_checks"] is True def test_validate_output_file_text_report(cli_runner: CliRunner, tmp_path: Path): diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 4e123d5c5..380ab622e 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -975,11 +975,11 @@ def dependent(self, ctx): assert dependent.depends_on == ("base",) # pyright: ignore[reportFunctionMemberAccess] -def test_python_check_decorator_sets_diagnostic_flag(): - """The @check decorator must expose the diagnostic classification.""" +def test_python_check_decorator_sets_extra_check_flag(): + """The @check decorator must expose the extra_check classification.""" from rocrate_validator.requirements.python import check - @check(name="advisory", diagnostic=True) + @check(name="advisory", extra_check=True) def advisory(self, ctx): return True @@ -987,8 +987,8 @@ def advisory(self, ctx): def conformance(self, ctx): return True - assert advisory.diagnostic is True # pyright: ignore[reportFunctionMemberAccess] - assert conformance.diagnostic is False # pyright: ignore[reportFunctionMemberAccess] + assert advisory.extra_check is True # pyright: ignore[reportFunctionMemberAccess] + assert conformance.extra_check is False # pyright: ignore[reportFunctionMemberAccess] def test_shacl_shape_with_deactivated_marks_check_skipped(fake_profiles_path: str): diff --git a/tests/unit/test_validation_settings.py b/tests/unit/test_validation_settings.py index debe2b646..bb27378cf 100644 --- a/tests/unit/test_validation_settings.py +++ b/tests/unit/test_validation_settings.py @@ -70,7 +70,7 @@ def test_validation_settings_to_dict(): assert settings_dict["profiles_path"] == "/path/to/profiles" assert settings_dict["requirement_severity"] == Severity.RECOMMENDED assert settings_dict["enable_profile_inheritance"] is False - assert settings_dict["include_diagnostic_checks"] is False + assert settings_dict["extra_checks"] is False def test_validation_settings_enable_profile_inheritance(): From 5224f5bc7765e60348d3bf75e63b2d83b5192182 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Oct 2026 15:29:55 +0200 Subject: [PATCH 231/271] =?UTF-8?q?chore(validation,tests,docs):=20?= =?UTF-8?q?=F0=9F=A7=B9=20apply=20lint=20and=20terminology=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 1 - docs/11_writing_a_profile.rst | 4 ++-- docs/2_usage_cli.rst | 4 ++-- docs/3_usage_api.rst | 6 +++--- rocrate_validator/models/__init__.py | 2 +- rocrate_validator/models/requirement.py | 6 ++++-- rocrate_validator/models/result.py | 1 + rocrate_validator/models/statistics.py | 8 ++++---- .../profiles/ro-crate-1.2/test_metadata_document.py | 5 +---- .../ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py | 4 +--- 10 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6968afc79..9e8e4f7f5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,3 @@ updates: weekly-batch: patterns: - "*" - diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index ffb5ff86b..c68760d97 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -184,12 +184,12 @@ result contains an actionable extra_check. When it is skipped, prefer ``context.record_skip(...)`` when a reason and category should be exposed to API and CLI consumers. -`ExtracCheck` Python checks +``ExtraCheck`` Python checks ~~~~~~~~~~~~~~~~~~~~~~~~ Use the ``extra_check=True`` argument to ``@check`` for advisory checks that report suspicious patterns without deciding whether a crate conforms to its -profile. `ExtracCheck` checks are disabled by default and can be enabled by users +profile. ``ExtraCheck`` checks are disabled by default and can be enabled by users with ``--extra-checks`` or the ``ValidationSettings.extra_checks`` setting. A extra_check check must record each finding with ``context.result.add_extra_check(...)``; do not diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 52c2c39f0..040c3e9cd 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -35,10 +35,10 @@ CLI Validation :end-before: ## Programmatic Validation -`ExtracCheck` Checks +``ExtraCheck`` Checks ----------------- -`ExtracCheck` checks are advisory checks that can report suspicious metadata +``ExtraCheck`` checks are advisory checks that can report suspicious metadata patterns without affecting whether an RO-Crate conforms to its profile. They are disabled by default. To run all checks classified as extra_check, pass ``--extra-checks`` to ``validate``: diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index 77d4a3c4a..d94aed893 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -31,10 +31,10 @@ Programmatic Validation :end-before: ## Running the tests -`ExtracCheck`s +``ExtraCheck``s ----------------- -`ExtracCheck` checks are advisory and do not affect the conformance result. They +``ExtraCheck`` checks are advisory and do not affect the conformance result. They are disabled by default; set ``extra_checks`` on ``ValidationSettings`` to run them: @@ -51,7 +51,7 @@ are disabled by default; set ``extra_checks`` on result = services.validate(settings) The resulting ``ValidationResult`` exposes these reports separately through -``result.extra_checks``. `ExtracCheck` checks are also listed separately in +``result.extra_checks``. ``ExtraCheck`` checks are also listed separately in ``result.statistics``. .. seealso:: diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index b2e6d4d2a..bd24e2f47 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -87,8 +87,8 @@ "SkippedCheckDetail", "SourceSnippet", "ValidationContext", - "ValidationExtraCheck", "ValidationEvent", + "ValidationExtraCheck", "ValidationResult", "ValidationSettings", "ValidationStatistics", diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index cede9fa01..fa4fccd83 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -500,8 +500,10 @@ def __execute_check__( new_all_passed = all_passed and (check.extra_check or normalized_result is not CheckResult.FAILED) # A batched backend can also report failures for checks other than the # one driving this execution. - should_break = not check.extra_check and context.fail_fast and ( - normalized_result is CheckResult.FAILED or context.result.has_issues() + should_break = ( + not check.extra_check + and context.fail_fast + and (normalized_result is CheckResult.FAILED or context.result.has_issues()) ) return new_all_passed and not should_break, should_break diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 81b51d532..28f91ea26 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -231,6 +231,7 @@ def __eq__(self, other: object) -> bool: __hash__ = None # type: ignore[assignment] + class ValidationResult: """ Represents the result of a validation. diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index d6721974f..1d5254501 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -481,12 +481,12 @@ def __handle_requirement_check_validation_end__(self, event: Event, ctx: Validat if event.validation_result is not None: for key in ("executed_extra_check_checks", "skipped_extra_check_checks"): self._stats[key] = [ - check - for check in self._stats[key] - if check.identifier != event.requirement_check.identifier + check for check in self._stats[key] if check.identifier != event.requirement_check.identifier ] result = normalize_check_result(event.validation_result) - status = "skipped_extra_check_checks" if result is CheckResult.SKIPPED else "executed_extra_check_checks" + status = ( + "skipped_extra_check_checks" if result is CheckResult.SKIPPED else "executed_extra_check_checks" + ) self._stats[status].append(event.requirement_check) self.notify_listeners() return diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index e7ce584f5..d208ea1ea 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -449,10 +449,7 @@ def test_https_entity_id_is_not_recommended(): metadata_dict=metadata_dict, ) ) - assert not any( - issue.check.requirement.name == "Schema.org @id compatibility" - for issue in result.get_issues() - ) + assert not any(issue.check.requirement.name == "Schema.org @id compatibility" for issue in result.get_issues()) def test_https_nested_schema_org_id_is_recommended_for_compatibility(): diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py index 855556104..17f93c287 100644 --- a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py @@ -44,9 +44,7 @@ def test_root_data_entity_type_target_ignores_preview_about_literal(): ) for profile_version, descriptor_name in descriptor_cases: - profile_path = ( - Path(__file__).resolve().parents[4] / "rocrate_validator/profiles/ro-crate" / profile_version - ) + profile_path = Path(__file__).resolve().parents[4] / "rocrate_validator/profiles/ro-crate" / profile_version shapes = Graph() shapes.parse(profile_path / "prefixes.ttl", format="turtle") shapes.parse(profile_path / "must/2_root_data_entity_metadata.ttl", format="turtle") From 70a12717d2cae79b174cc4ed5b4b02d463f985ab Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 21 Oct 2026 10:17:30 +0200 Subject: [PATCH 232/271] =?UTF-8?q?chore(deps):=20=F0=9F=94=A7=20update=20?= =?UTF-8?q?dependencies=20in=20poetry.lock=20(v0.13.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 1398 +++++++++++++++++++++++++++++---------------------- 1 file changed, 793 insertions(+), 605 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2bff7eb26..89c42b62e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,69 +85,69 @@ files = [ [[package]] name = "ast-serialize" -version = "0.11.2" +version = "0.13.1" description = "Python bindings for mypy AST serialization" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b"}, - {file = "ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f"}, - {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d"}, - {file = "ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1"}, - {file = "ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4"}, - {file = "ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365"}, - {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575"}, - {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719"}, - {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa"}, - {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9"}, - {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106"}, - {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07"}, - {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e"}, - {file = "ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99"}, - {file = "ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484"}, - {file = "ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27"}, - {file = "ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450"}, + {file = "ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3"}, + {file = "ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43"}, + {file = "ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025"}, ] [[package]] @@ -317,14 +317,14 @@ ujson = ["ujson (>=5.10.0)"] [[package]] name = "certifi" -version = "2026.7.22" +version = "2026.10.19" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, - {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, + {file = "certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b"}, + {file = "certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae"}, ] [[package]] @@ -929,14 +929,14 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "distlib" -version = "0.4.3" +version = "0.4.4" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b"}, - {file = "distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed"}, + {file = "distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638"}, + {file = "distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d"}, ] [[package]] @@ -1027,18 +1027,18 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "executing" -version = "2.2.1" +version = "2.3.0" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, - {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, + {file = "executing-2.3.0-py3-none-any.whl", hash = "sha256:736e859c9f8701f11fcf516856f26f562e04776387824b43a35a1dfe21c84122"}, + {file = "executing-2.3.0.tar.gz", hash = "sha256:15919cb5d667e5cb4e099511971d00d659573fff2dd5c4e6cd8b71636c7858d2"}, ] [package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] +tests = ["asttokens (>=2.1.0)", "coverage-enable-subprocess", "coverage[toml]", "ipython", "littleutils", "pysource-minimize (>=0.10.1)", "pytest", "rich ; python_version >= \"3.11\""] [[package]] name = "fastjsonschema" @@ -1057,14 +1057,28 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "4.0.7" +version = "4.1.0" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev", "docs"] +markers = "python_version == \"3.10\"" files = [ - {file = "filelock-4.0.7-py3-none-any.whl", hash = "sha256:a93c4d93269b339a6af4848342c7e940d0f9928ad95eff64764699e5f1bf8a6a"}, - {file = "filelock-4.0.7.tar.gz", hash = "sha256:da5915714a70b55d167fdc7e251ad91302b0a36816fb574dfafae8f4f2c9bb21"}, + {file = "filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1"}, + {file = "filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d"}, +] + +[[package]] +name = "filelock" +version = "4.2.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.11" +groups = ["dev", "docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e"}, + {file = "filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d"}, ] [[package]] @@ -1182,14 +1196,14 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "iniconfig" -version = "2.3.0" +version = "2.3.1" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, + {file = "iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"}, + {file = "iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960"}, ] [[package]] @@ -1366,18 +1380,18 @@ plugins = ["setuptools"] [[package]] name = "jedi" -version = "0.20.0" +version = "0.20.1" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67"}, - {file = "jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011"}, + {file = "jedi-0.20.1-py2.py3-none-any.whl", hash = "sha256:0fb16d86c4a4c73c37ba518c77419975e30fcc620658a8d14fbb5720cdd34142"}, + {file = "jedi-0.20.1.tar.gz", hash = "sha256:2f71208c3f9c1bca057c0e90d3f272aba44ace88fc4067d7587e9e069331b7e5"}, ] [package.dependencies] -parso = ">=0.8.6,<0.9.0" +parso = ">=0.8.7,<0.9.0" [package.extras] dev = ["Django", "attrs", "colorama", "docopt", "flake8 (==7.1.2)", "pytest (<9.0.0)", "types-setuptools (==80.9.0.20250529)", "typing-extensions", "zuban (==0.7.0)"] @@ -1670,101 +1684,163 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "3.0.3" +version = "3.0.4" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, + {file = "markupsafe-3.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889"}, + {file = "markupsafe-3.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a"}, + {file = "markupsafe-3.0.4-cp310-cp310-win32.whl", hash = "sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278"}, + {file = "markupsafe-3.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7"}, + {file = "markupsafe-3.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7"}, + {file = "markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e"}, + {file = "markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17"}, + {file = "markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4"}, + {file = "markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741"}, + {file = "markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba"}, + {file = "markupsafe-3.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd"}, + {file = "markupsafe-3.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8"}, + {file = "markupsafe-3.0.4-cp39-cp39-win32.whl", hash = "sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624"}, + {file = "markupsafe-3.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b"}, + {file = "markupsafe-3.0.4-cp39-cp39-win_arm64.whl", hash = "sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378"}, + {file = "markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6"}, ] [[package]] @@ -1831,14 +1907,14 @@ files = [ [[package]] name = "mistune" -version = "3.3.4" +version = "3.4.0" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a"}, - {file = "mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe"}, + {file = "mistune-3.4.0-py3-none-any.whl", hash = "sha256:d03cec4c8620f21517a996d82d14f3b59c30d79bda2cc6c56bd32b839f18ce6b"}, + {file = "mistune-3.4.0.tar.gz", hash = "sha256:2eaf7952af928ca8d33b779762effc1da517b62c6ad85ec8ca957e3b07b9782a"}, ] [package.dependencies] @@ -1947,69 +2023,69 @@ files = [ [[package]] name = "mypy" -version = "2.3.1" +version = "2.4.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41"}, - {file = "mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0"}, - {file = "mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167"}, - {file = "mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13"}, - {file = "mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53"}, - {file = "mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90"}, - {file = "mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531"}, - {file = "mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8"}, - {file = "mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8"}, - {file = "mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01"}, - {file = "mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1"}, - {file = "mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6"}, - {file = "mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff"}, - {file = "mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff"}, - {file = "mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080"}, - {file = "mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355"}, - {file = "mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb"}, - {file = "mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b"}, - {file = "mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d"}, - {file = "mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb"}, - {file = "mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226"}, - {file = "mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74"}, - {file = "mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6"}, - {file = "mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac"}, - {file = "mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d"}, - {file = "mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3"}, - {file = "mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f"}, - {file = "mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82"}, - {file = "mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9"}, - {file = "mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d"}, - {file = "mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595"}, - {file = "mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2"}, - {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc"}, - {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045"}, - {file = "mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0"}, - {file = "mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63"}, - {file = "mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4"}, - {file = "mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57"}, - {file = "mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b"}, - {file = "mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561"}, - {file = "mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133"}, - {file = "mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9"}, - {file = "mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3"}, - {file = "mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523"}, - {file = "mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306"}, - {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021"}, - {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e"}, - {file = "mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc"}, - {file = "mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4"}, - {file = "mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29"}, - {file = "mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb"}, - {file = "mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419"}, + {file = "mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2"}, + {file = "mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23"}, + {file = "mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080"}, + {file = "mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f"}, + {file = "mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb"}, + {file = "mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220"}, + {file = "mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd"}, + {file = "mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74"}, + {file = "mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1"}, + {file = "mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4"}, + {file = "mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b"}, + {file = "mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd"}, + {file = "mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e"}, + {file = "mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90"}, + {file = "mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e"}, + {file = "mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1"}, + {file = "mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946"}, + {file = "mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989"}, + {file = "mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8"}, + {file = "mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324"}, + {file = "mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb"}, + {file = "mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1"}, + {file = "mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf"}, + {file = "mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303"}, + {file = "mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437"}, + {file = "mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e"}, + {file = "mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b"}, + {file = "mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566"}, + {file = "mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe"}, + {file = "mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89"}, + {file = "mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799"}, + {file = "mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97"}, + {file = "mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359"}, + {file = "mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3"}, + {file = "mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c"}, + {file = "mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76"}, + {file = "mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37"}, + {file = "mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a"}, + {file = "mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d"}, + {file = "mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818"}, + {file = "mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5"}, + {file = "mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf"}, + {file = "mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34"}, + {file = "mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3"}, + {file = "mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01"}, + {file = "mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141"}, + {file = "mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0"}, + {file = "mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975"}, + {file = "mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710"}, + {file = "mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5"}, + {file = "mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093"}, + {file = "mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e"}, ] [package.dependencies] -ast-serialize = ">=0.6.0,<1.0.0" -librt = {version = ">=0.13.0", markers = "platform_python_implementation != \"PyPy\""} +ast-serialize = ">=0.11.2,<1.0.0" +librt = {version = ">=0.16.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -2105,14 +2181,14 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= [[package]] name = "nbconvert" -version = "7.17.1" +version = "7.17.2" description = "Convert Jupyter Notebooks (.ipynb files) to other formats." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"}, - {file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"}, + {file = "nbconvert-7.17.2-py3-none-any.whl", hash = "sha256:6a143c11c00bf831604da4b1be4347d36ea52367057eb920135d69bc2862fed2"}, + {file = "nbconvert-7.17.2.tar.gz", hash = "sha256:344a218e8595117ce37396ed025d994f146d076226ebac59255a2fa085ab24d8"}, ] [package.dependencies] @@ -2317,32 +2393,42 @@ docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-pars [[package]] name = "platformdirs" -version = "4.12.2" +version = "4.12.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs"] +markers = "python_version == \"3.10\"" +files = [ + {file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"}, + {file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"}, +] + +[[package]] +name = "platformdirs" +version = "4.13.5" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.11" +groups = ["main", "dev", "docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "platformdirs-4.12.2-py3-none-any.whl", hash = "sha256:29dbf06d96c500bc6bdbce75fb0a14d63279c93b1842f97e72a135b33e856983"}, - {file = "platformdirs-4.12.2.tar.gz", hash = "sha256:eab5f70271a490ef74618bb314fbb86e3c7e82fa3b9c922c2ea0e0a1a155d329"}, + {file = "platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e"}, + {file = "platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0"}, ] [[package]] name = "pluggy" -version = "1.6.0" +version = "1.7.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, + {file = "pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec"}, + {file = "pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"}, ] -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - [[package]] name = "pre-commit" version = "4.6.2" @@ -2350,6 +2436,7 @@ description = "A framework for managing and maintaining multi-language pre-commi optional = false python-versions = ">=3.10" groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"}, {file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"}, @@ -2362,6 +2449,26 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pre-commit" +version = "4.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.11" +groups = ["dev"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877"}, + {file = "pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "prettytable" version = "3.18.0" @@ -2460,15 +2567,15 @@ tests = ["pytest"] [[package]] name = "pycparser" -version = "3.0" +version = "3.11" description = "C parser in Python" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] markers = "implementation_name == \"pypy\"" files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, + {file = "pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80"}, + {file = "pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc"}, ] [[package]] @@ -2674,18 +2781,18 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.6.1" +version = "1.6.4" description = "Python interpreter discovery" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "python_discovery-1.6.1-py3-none-any.whl", hash = "sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3"}, - {file = "python_discovery-1.6.1.tar.gz", hash = "sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e"}, + {file = "python_discovery-1.6.4-py3-none-any.whl", hash = "sha256:530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b"}, + {file = "python_discovery-1.6.4.tar.gz", hash = "sha256:bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df"}, ] [package.dependencies] -filelock = ">=3.15.4" +filelock = ">=3.16.1" [[package]] name = "pyyaml" @@ -3157,129 +3264,141 @@ files = [ [[package]] name = "rpds-py" -version = "2026.6.3" +version = "2026.9.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.11" groups = ["docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, - {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, - {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, - {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, - {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, - {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, - {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, - {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, - {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, - {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, - {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, - {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, - {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, - {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, - {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, - {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, - {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, - {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, - {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, - {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, - {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, - {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, - {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, - {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, - {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, - {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, - {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, - {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, - {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, - {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, - {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, - {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, - {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, - {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, - {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, - {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, - {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, - {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, - {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, - {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, - {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, - {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, - {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, - {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, - {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, - {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, - {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, - {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, - {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, + {file = "rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2"}, + {file = "rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12"}, + {file = "rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af"}, + {file = "rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02"}, + {file = "rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9"}, + {file = "rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f"}, + {file = "rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b"}, + {file = "rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e"}, + {file = "rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9"}, + {file = "rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37"}, + {file = "rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12"}, ] [[package]] @@ -3375,11 +3494,25 @@ description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.10" groups = ["docs"] +markers = "python_version < \"3.12\"" files = [ {file = "soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502"}, {file = "soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0"}, ] +[[package]] +name = "soupsieve" +version = "3.0.3" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.11.5" +groups = ["docs"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "soupsieve-3.0.3-py3-none-any.whl", hash = "sha256:fa30e3ba4809cb81ce1f3209f2fbe3e779fc445f0439bc147a0d7c4601743f21"}, + {file = "soupsieve-3.0.3.tar.gz", hash = "sha256:7dcf6022eed0399eb9934a75e020148f7a2024c37b7dfcd3cf2c5505d69c364e"}, +] + [[package]] name = "sphinx" version = "8.1.3" @@ -3706,18 +3839,20 @@ test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" +version = "2.0.0" +description = "sphinxcontrib-jsmath is a sphinx extension which renders displayed maths in HTML via JavaScript" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, + {file = "sphinxcontrib_jsmath-2.0.0-py3-none-any.whl", hash = "sha256:15ca92fe0eec82f0baf91a7efcc51ffacdb605924be6327683a52ebdacb6744c"}, + {file = "sphinxcontrib_jsmath-2.0.0.tar.gz", hash = "sha256:551f780cf9687f9c3c74f0cf9f18aca06e0d6958853453f3803fe4d357c5b924"}, ] [package.extras] -test = ["flake8", "mypy", "pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -3793,6 +3928,7 @@ description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["docs"] +markers = "python_version == \"3.10\"" files = [ {file = "starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e"}, {file = "starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d"}, @@ -3805,6 +3941,26 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\"" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] +[[package]] +name = "starlette" +version = "1.9.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.11" +groups = ["docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "starlette-1.9.0-py3-none-any.whl", hash = "sha256:fd3f159936e882d06c1108f84688f7cda2cf85fc6a592a50edbd175e882c2ddc"}, + {file = "starlette-1.9.0.tar.gz", hash = "sha256:3d931de246ed870e35ce605974440816ef895e592f255d1396cfdf191d4c4318"}, +] + +[package.dependencies] +anyio = ">=4.0.0,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "tabulate" version = "0.10.0" @@ -3853,72 +4009,90 @@ files = [ [[package]] name = "tomli" -version = "2.4.1" +version = "2.5.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "docs", "test"] markers = "python_version == \"3.10\"" files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, + {file = "tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545"}, + {file = "tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef"}, + {file = "tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b"}, + {file = "tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56"}, + {file = "tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1"}, + {file = "tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885"}, + {file = "tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e"}, + {file = "tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8"}, + {file = "tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980"}, + {file = "tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df"}, + {file = "tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b"}, + {file = "tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0"}, + {file = "tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6"}, + {file = "tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc"}, + {file = "tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7"}, + {file = "tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2"}, + {file = "tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7"}, + {file = "tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea"}, + {file = "tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea"}, + {file = "tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043"}, + {file = "tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0"}, + {file = "tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b"}, + {file = "tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066"}, + {file = "tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b"}, + {file = "tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68"}, + {file = "tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc"}, + {file = "tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84"}, + {file = "tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105"}, + {file = "tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646"}, + {file = "tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b"}, + {file = "tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75"}, + {file = "tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb"}, + {file = "tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3"}, + {file = "tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b"}, + {file = "tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a"}, + {file = "tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3"}, + {file = "tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4"}, + {file = "tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d"}, + {file = "tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9"}, + {file = "tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f"}, + {file = "tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374"}, + {file = "tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442"}, + {file = "tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03"}, + {file = "tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1"}, + {file = "tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0"}, + {file = "tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc"}, + {file = "tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276"}, + {file = "tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52"}, + {file = "tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7"}, + {file = "tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391"}, + {file = "tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859"}, + {file = "tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb"}, + {file = "tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5"}, + {file = "tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd"}, + {file = "tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57"}, + {file = "tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd"}, + {file = "tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01"}, + {file = "tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f"}, + {file = "tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a"}, + {file = "tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142"}, + {file = "tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5"}, + {file = "tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571"}, + {file = "tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7"}, + {file = "tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b"}, + {file = "tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6"}, ] [[package]] name = "tomlkit" -version = "0.15.1" +version = "0.15.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"}, - {file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"}, + {file = "tomlkit-0.15.2-py3-none-any.whl", hash = "sha256:05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437"}, + {file = "tomlkit-0.15.2.tar.gz", hash = "sha256:83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6"}, ] [[package]] @@ -4040,23 +4214,23 @@ standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", " [[package]] name = "virtualenv" -version = "21.14.1" +version = "21.14.12" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.14.1-py3-none-any.whl", hash = "sha256:6fd04089fc0dc33549e7abdff70fc3b63d4e15799f2dbf3281f80d13b9fce522"}, - {file = "virtualenv-21.14.1.tar.gz", hash = "sha256:719b189804e66678017d9f63bbfc590c44f6b96ab4829513806394f9b375929c"}, + {file = "virtualenv-21.14.12-py3-none-any.whl", hash = "sha256:7820aeaef23f1f59a96458edfc7f9d3deac937155718de15f1c9a64471e15551"}, + {file = "virtualenv-21.14.12.tar.gz", hash = "sha256:bbcfb78e36c22ad70c6917c4aecb5e63477d3c138e9f88c946f320e31859c76a"}, ] [package.dependencies] -distlib = ">=0.3.7,<1" +distlib = ">=0.4.3,<1" filelock = {version = ">=3.24.2,<5", markers = "python_version >= \"3.10\""} -packaging = ">=23.1" -platformdirs = ">=3.9.1,<5" -python-discovery = ">=1.6" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} +packaging = ">=26.3" +platformdirs = ">=4.4,<5" +python-discovery = ">=1.6.1" +typing-extensions = {version = ">=4.16", markers = "python_version < \"3.11\""} [[package]] name = "watchfiles" @@ -4106,33 +4280,41 @@ anyio = ">=3.0.0" [[package]] name = "wcwidth" -version = "0.9.1" +version = "0.9.3" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "wcwidth-0.9.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:10b00ba23482e352f874d2e8135e7ace9da838646c7dd800566246bbd46125ff"}, - {file = "wcwidth-0.9.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:40d936d72c9bdc10df43f93a8be502bc5024b487259139f66a328722c07f34a9"}, - {file = "wcwidth-0.9.1-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:69bb970cf5652b88cfdb9d3fdd1764fc15e5f8ad531643e7bb3e894cd969740e"}, - {file = "wcwidth-0.9.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b5da43d6967668982e44a52fb551967d293f86d26cd86036ac95bbdd34394ed9"}, - {file = "wcwidth-0.9.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:747fb724223f417a17541a95a17c1dac3a8ef9a0cf41684950f0eab191a35f65"}, - {file = "wcwidth-0.9.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6e1272b7986cefe79783737e38bdb9eaae0682b333c7bd132024441193dd5ce7"}, - {file = "wcwidth-0.9.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:fe021c4d8de9d36c31a0cb41d0d2546dadd3b0708a301f1a0c66ce200851831f"}, - {file = "wcwidth-0.9.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:708158c082364af442f9983de7b6ec9ac0d2e1b825ada25f0911b1f138d55405"}, - {file = "wcwidth-0.9.1-cp310-abi3-win32.whl", hash = "sha256:356376852357b8fca71fe5415808ec421679e04b4a98eb7c9cb6a7984b911a05"}, - {file = "wcwidth-0.9.1-cp310-abi3-win_amd64.whl", hash = "sha256:991d1c8834f548e9c1f16432075ee84638e122312556bbf1ed595ea8fffc4673"}, - {file = "wcwidth-0.9.1-cp310-abi3-win_arm64.whl", hash = "sha256:61bd7aef9cafb6cb77a37a169998d7928ce82a51522146d11f60db9e7d1cb43a"}, - {file = "wcwidth-0.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dc10e262c3ac0abbfd0a2a51e45a848b1b7f500b21ff512630277973ba25674d"}, - {file = "wcwidth-0.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bcb9ed4a367cc025bf1092ac679a156759605182d60b7ed3c28f7221a42ddf55"}, - {file = "wcwidth-0.9.1-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:0d68a30d504c68cfdff2a5f804675c1e7ab4c0bbe878024c8b680ec5579cde67"}, - {file = "wcwidth-0.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9f1636c5075ffd5c2e835b4561874f6e5dd2bbeba3c6c2c99f067d0d16883af8"}, - {file = "wcwidth-0.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c4cead196551112cb8f43cdd1f80c235ef2456e34b1a9955c424537ec99961b2"}, - {file = "wcwidth-0.9.1-cp314-cp314t-win32.whl", hash = "sha256:0665ee822ea04e25801e6e82e5407528be0863e88a17b0e7ca038843a4a3ac0c"}, - {file = "wcwidth-0.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eab587e18e7cadf1a750b0098fc8bebfb62c125eb9306f2268f0443a282a3d78"}, - {file = "wcwidth-0.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:03cfca3dcbffa86564290fe3c9978a6191ba003e8ced7f7dbda315fcb3fbe725"}, - {file = "wcwidth-0.9.1-py3-none-any.whl", hash = "sha256:e0c3a1c45c5b9550c6919a4449e95f5b177f6e165786376981db8f1addae9b21"}, - {file = "wcwidth-0.9.1.tar.gz", hash = "sha256:5823209b0d43af322ce698c689380d7c15ca31fa8e6e3be8459f27031bef0af5"}, + {file = "wcwidth-0.9.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b119ef60177a92b4f298b80e98464c2922a1fba3ed77fe9a2bcb69edf1b9af4"}, + {file = "wcwidth-0.9.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aab90ab489850f87633922c3064f6d0442d8f6ae5407a62f82bdb1f424179c05"}, + {file = "wcwidth-0.9.3-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:9e1cbe18c18d152e1e68e4e2561dab6c2bc988f8fd337a1fb2e1e0809c3fa568"}, + {file = "wcwidth-0.9.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c12abd75ffcbad417e7770a6feb82413c102ccce5c435068c39a82e2218b00b3"}, + {file = "wcwidth-0.9.3-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9ab8bfee107e08de09e15f9f6afb9fe0f64c8d64fd296d0d8557bafe71ea3f93"}, + {file = "wcwidth-0.9.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f875a4c9e62df2ecfa70d99b24c551f2245609858e179c3a40e74631b2d08a4"}, + {file = "wcwidth-0.9.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:70a2ca239b85a219324968993c76c117fe423d4e1a7c9d860c999c66192f2570"}, + {file = "wcwidth-0.9.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:537ea00b6792bb243cd09582322c54dbc848879e5d34ea4c5fee0147529685ab"}, + {file = "wcwidth-0.9.3-cp310-abi3-win32.whl", hash = "sha256:f2f943c4ef14e9baba730740265f4e5bf5fc8473550e36cd92828cd5663561a6"}, + {file = "wcwidth-0.9.3-cp310-abi3-win_amd64.whl", hash = "sha256:569a0cdb826d3097b6dbacfaa45aa0002d72a2ba6fa8a63251ea43ac4dd7b6a4"}, + {file = "wcwidth-0.9.3-cp310-abi3-win_arm64.whl", hash = "sha256:80557db1f77148b78e1b3c2a9cc4ebc4feabef42aaf69a555565057031c32367"}, + {file = "wcwidth-0.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6f3e855d3a09af8b7b6d56e76bc270681032f8a37d527a8d5707a441958e3730"}, + {file = "wcwidth-0.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1bbc8ebf133422cadfefea95a9f89d6c9e9a1ae19930bef7862a9b59b20bbad"}, + {file = "wcwidth-0.9.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7a95d8c784cbccbbcb808eaadfd156cd282b2bdd95e89b32505e3443381ce61d"}, + {file = "wcwidth-0.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a44acbb23b525070a0b8c343dd7cab7a4020aff73b38804f7a66d6a66e16e3fc"}, + {file = "wcwidth-0.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a7c0405a3bf3c564e481d6b2d55c99cc231bb68cf8f4a0995944bbce5ebf1a5d"}, + {file = "wcwidth-0.9.3-cp314-cp314t-win32.whl", hash = "sha256:457f986e4867111cb037ccbf0d97ae100b1ecad030d43a020e7207f1e29649d5"}, + {file = "wcwidth-0.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:f11789e520ade8017fa93b01a4b8985571bb433eaefc2f857b57e36875e2e1d8"}, + {file = "wcwidth-0.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:eef39f3260991d8767189d296f0116fa4b45ad9c3a9a16d5af9187fb66b27564"}, + {file = "wcwidth-0.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:52a13869704d43a4add59c68ebc892198e53063883114783bf0b7457e250e3f4"}, + {file = "wcwidth-0.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9470169fcb4e5340269175ebb3642dd5bfc8f82da8c86f29fc1fefc6c2b9bc1b"}, + {file = "wcwidth-0.9.3-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c4531d93344441a7948e665854f0c40e3a8342ee106436d1531efa7a9fa7641e"}, + {file = "wcwidth-0.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:658dcbce546e7f28ee361f88e4a8e591dd56f46bff6084e11b4ad8310ed99946"}, + {file = "wcwidth-0.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9362be098c43d076aa43754face3cc0180fa81cea9ba304240b15490f2617e8"}, + {file = "wcwidth-0.9.3-cp315-cp315t-win32.whl", hash = "sha256:16d09df90b3a8f799ea14c726a41d9b906c3104f95f46d158e1ebe45cf4d547b"}, + {file = "wcwidth-0.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:3e3e31d6c17d6da3051f7eb854d5e4bebdc52a08a7a52adc2da6f15af51304e8"}, + {file = "wcwidth-0.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5d3b0b6c37f40e8d8298941ec373a426a892bb9a514328abcc9226cdc60ae6eb"}, + {file = "wcwidth-0.9.3-py3-none-any.whl", hash = "sha256:e3c9069c54a2da0bf4f6c7e65a735cb1da0f47601a15cb5fef7e4b6ffca4cf60"}, + {file = "wcwidth-0.9.3.tar.gz", hash = "sha256:dfe7cfbd4cb59db9fe0931bd6091ab4a95b3acc5d56bfb50fa9fef0ccba5dc5c"}, ] [[package]] @@ -4273,174 +4455,180 @@ files = [ [[package]] name = "websockets" -version = "17.2" +version = "17.3" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.11" groups = ["docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "websockets-17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:569ed5db651e420b13279f9333443bb5b84a436cc66b599cbc535697ae4434a0"}, - {file = "websockets-17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3892d76754b5f36fb40619f3ef09c68e5c3091f1ab8840964518ae5a41f30952"}, - {file = "websockets-17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5436ffea003adb50e283ca0684a3fcaa1396104f841736c3322ee6582bd09e98"}, - {file = "websockets-17.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9df9d048def11365d170b375b6ffc8b23a7f188c3560acd4418ba088ca2e2705"}, - {file = "websockets-17.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:376a693697ddb695ea282ead76060f4847f90e564b12b4389f2c7589e6fadb9e"}, - {file = "websockets-17.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd63d0c7ed0d3d719c91b5a3861f0f0b3cec9bf223033ddf69d17aaac74bb6d"}, - {file = "websockets-17.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:48997ed4431d8006988788ef4b62e1fd3f053c7463b4fa793aa6c4f9e96a3bb7"}, - {file = "websockets-17.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e312e07557a5ad348f4e83d3419773527f6e790c7f97928b1911d767b6ea1c7"}, - {file = "websockets-17.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:902ce8cafca2dc14cef9558a6fc3b45dbf7f121d1404bf2ad18a1c894555e48c"}, - {file = "websockets-17.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e53d950e16d4bb672a5ff41fe3131e65a4e5d688d694e1c7074c8c9990bb3ceb"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:946ac2164d646e733004946ae39536b5af473853183d81da5962e29d36e3ad35"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:660aa158127035e741d4b1835dbe79ae18a1fbb21ecd236655f31d60110e68d5"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4733fc2d99fe888261417b7e29995403a72d9ffa78629902882325ea141177f2"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c2ec7e51157a3fa0e9cfdb1a8969bab38d1c22ad1ace7c6cea006383b43a1ad4"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ada04d0262ab06527054a2a497f384d102698ff39b3865dc566a7d24b6f4058c"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c393a202df08e96ed619310f0cd78be700e532a57d9a6ceee5f80b4e35bef14"}, - {file = "websockets-17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:af4c565b923bb5975401b8e4cedc2e17b2fdbf33b905737ee12384e6a6fd9507"}, - {file = "websockets-17.2-cp311-cp311-win32.whl", hash = "sha256:c81d6cdbacccda7e0eef3b076a457fd14c3835cdbc5993d2881580c2fb1f5f26"}, - {file = "websockets-17.2-cp311-cp311-win_amd64.whl", hash = "sha256:55c5b9eab079540bfb639b40b07b7b467e5c5a7ecf97a65cc8665781381c9856"}, - {file = "websockets-17.2-cp311-cp311-win_arm64.whl", hash = "sha256:55f9a808a0e072473337c240c939849818276e288e2374b832255b5b791b0851"}, - {file = "websockets-17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:916ebdfd82e7fc68041d36b2b5f60361b9abce1e087454da15f8bd004839e090"}, - {file = "websockets-17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3621f3686397708b8eeabfd0a9d75267c1f29a7537d2fe31e65d099e71587fa4"}, - {file = "websockets-17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a81e19710d48da88653473b6b9c366d47e99fe4f58e37ce415be47966748f31f"}, - {file = "websockets-17.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2731f9067976c8c4127212c0d2f2ada42d497d935e470419e029802365b12bb"}, - {file = "websockets-17.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6627b913b8586b1c06db9516b31dd0dfbc621de3bb9312616d92a7e44f268a5b"}, - {file = "websockets-17.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0198c4ec6a3406a2f7557c032967de426474c2c995c81076585e09d29a9f407b"}, - {file = "websockets-17.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:88c6a42c2632ff469e84155e44f6ed92cb15ccb047bf5fcb59225ae5a12fd33d"}, - {file = "websockets-17.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb0023e6cdb4b8ece0b33875188dd16104ad8c335361d396a98394f99e30ff7a"}, - {file = "websockets-17.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1c09d5d4646eb96bda2cfb97493bcea21a0956a981de116e6b1f4a9de07f3fd"}, - {file = "websockets-17.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0360c4dc13ac569cc245e0efa2f4d4b1e4733d24c47b8ab3f3747227b1356348"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76693a16dead737946b651375ee3109d7db7ad9569a1c55c60aaed3ef85cfcc6"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:77a42cc507993ec5471b5283f7eef869239173b6000031543e3938a86d1af0fd"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3bbc5543e39ee025d524077c5c15c2d67bc11c9f6676afe5b531839e24d701f6"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8da58558bfb0ca6ccac2419773521f1111e40654038b1afabdfc69c02cb82614"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:01420cb1cb47433e8e7075d32cb8017ad3ffed0654bd1e48c0251b865920dec3"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c49c9edd47d0e44d360299e2d8865e2950d2fcf1b4098782c9d7dcd070919e5a"}, - {file = "websockets-17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96f6c8d0fe21930d1f982bfce2382789d2e8d005d2ab63d21280660f95ef8fe1"}, - {file = "websockets-17.2-cp312-cp312-win32.whl", hash = "sha256:b25659ab2d655d742701487d5591e3f98e8f8b329fc999e05e3d59691ab344a1"}, - {file = "websockets-17.2-cp312-cp312-win_amd64.whl", hash = "sha256:faa763b677e96f1beccc6b4d7e8c079dfeed2f249f57a19debc321b519ee64ec"}, - {file = "websockets-17.2-cp312-cp312-win_arm64.whl", hash = "sha256:63499fc49efe48bccc2fca40723bc7adb198866cbe159093dd979905316994b6"}, - {file = "websockets-17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b24b83fbb34b2d8de06cf0f0d4bd7737344ef854482a614826d4356c0c3f0c12"}, - {file = "websockets-17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a829db795e3f87053904493d184b185c8eb1f497c852f434168ec856aa6f997"}, - {file = "websockets-17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf8811d285acc91216368df7fb55cc8c9bf6fcd90eea42429c7186c7385a12b9"}, - {file = "websockets-17.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89c4898da776193577279173dcf9860487590611d7320d379435a145881b048d"}, - {file = "websockets-17.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d87091c4347daadbcc0833b65812ff38d7350c67339625d4e4a512cf38e3e8ef"}, - {file = "websockets-17.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1110fbfd530c447380e6e6db88b7e43ffe33d54178f5b0ff0aaa5a280301e668"}, - {file = "websockets-17.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:83abd8beab056aa77a116364811f8fc262dffbcc7abea48de0c85ccbfc6f1428"}, - {file = "websockets-17.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:876da8ca5520d65b5d0f2ca6b4e7a00d35bb90ccda35cb2ce3cda4b6c711e84a"}, - {file = "websockets-17.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8462395df8f224d2daa3d80db3ae4450d9d4b7243c8483ac79a82862f1599dd6"}, - {file = "websockets-17.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e9a04e69456015e6ae5e0d486d995137fd435794442122b00ce5f9526ea3ba8"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a2321bcb73758c44c8076509024d02c15ee484fe77ce04edea4bf4d257492cc"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8be4a87b3baca380ec3c7b1643b2dd268ac9d42c5097c0e8dc9a49342faf4774"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:eb7b737ce8d18c8a08beb68f751572b7bf6a18093ecd1406ca1256b50592552e"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d6605630c2808b33f362d6d08582e79821f77ed2bd3f49f9d467ea70defea06d"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd9252828073fd0d69e7667af4275a1b17c18d0833b1ab7f59db272f194a6b9a"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:06c7386128a9d85de4e1960114604f3031c084d2f4eee8db382637f1634cbab1"}, - {file = "websockets-17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98f2d03df74977fd252831c997c388cd6c3f691a8a9d022b266d3cbd9849838f"}, - {file = "websockets-17.2-cp313-cp313-win32.whl", hash = "sha256:5b43a1f7e4853ce08c3f6d3bf69799ee5b46548bfb71792a8158f7e45d66b547"}, - {file = "websockets-17.2-cp313-cp313-win_amd64.whl", hash = "sha256:27c7a59b5352a8f741b422820adfe89dfe47c8f2d84fb32111e76111edaa0e83"}, - {file = "websockets-17.2-cp313-cp313-win_arm64.whl", hash = "sha256:533b7c82bb1eafbeb921dfe131c9f88e55451ddc328d84bde1c9340ba72d2808"}, - {file = "websockets-17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ecb748910e9ba4624ebe2057791df51dcbffb48c37108ab94a3c593472023c9e"}, - {file = "websockets-17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ab9af5cb7265899e659f079eb71691375a1025b6d5fbd3caa495dd08f70833a"}, - {file = "websockets-17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:06e46da092bca3a52e98f0458c66b247993ce501a07cd09c858be3296511ab7d"}, - {file = "websockets-17.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fcce735ffd72ac4056db05325d9f0232382b74826f0196eb6a15ca903abdaa0f"}, - {file = "websockets-17.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:42cbca10f82a8b2fb1536e8a0830ca6ceeb6bb3d8d64b766e0795369135654a8"}, - {file = "websockets-17.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63ff5a21f26bd0e6a8464b53fadbe174825c8718ac14180df45665eaacdb6af"}, - {file = "websockets-17.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63f543463601c1558b755f8dd7618b6ec3dd0934dda051d3b7030d8c76e54de2"}, - {file = "websockets-17.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4c32eb565ad9ce8a6444248e5b7a19dbb86a81c811fe5fcc2fba7a735aed5163"}, - {file = "websockets-17.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d459bbb6c22f26dcebea56924a362aba50d453b9867912862c970434fcf0d94"}, - {file = "websockets-17.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f19ca1a21871f024e38faf4107b433047df27558dff1b72a1dac31481e2c1fe5"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c76b4bcbf0f713194591673fc86a42820e14da6bbd1bb445d3d002cc4d1e4521"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:30201a7f69833b015556c72feb69ea501b645986fd0b90dab13f589e995ff428"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0c8600aec354cc259f1691b0b42816f04a9886a953f82cb227246df76057f97a"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:307fc22ea496be8542d67b82ae8c867a978dfd19ac35573d4f15943fd9277dfe"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c88697fa943bd4ef67cc919a17d81de6581846f52bfa8c6f64a916098986556"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7eac84d4969da82166d5e90d9c38d2f416fe24f9708a7013569b193745b9a31"}, - {file = "websockets-17.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:313f6703023d53baabab6d6c5c37cf637b2c4fee255acf2ed5e92ad69e28f1b7"}, - {file = "websockets-17.2-cp314-cp314-win32.whl", hash = "sha256:08d90cf344bdb971ba3a826b78d4da9bfd56cc6a97a604d9b88cbd40bfa6c735"}, - {file = "websockets-17.2-cp314-cp314-win_amd64.whl", hash = "sha256:dac93bf7a9beb215be3282b8441173cd50806c41c007b8be9bb24e03c60ad563"}, - {file = "websockets-17.2-cp314-cp314-win_arm64.whl", hash = "sha256:2ab742249f953d148a9ba696c8b9944361e8cb92e8bc61ba2dd53a178403afd3"}, - {file = "websockets-17.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a69ce25be5f1330ee1c74eb6fabbbceaa96b384beedd2627cecded7546490c40"}, - {file = "websockets-17.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8e24b878cf54843a63985d90480f163ca7f692689fbcbe9cdbd8165521083a8b"}, - {file = "websockets-17.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f33c7908a6885dcae9f462a4a8347b637053b4ff2b96beb4c23fba1cf7818e5f"}, - {file = "websockets-17.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c796a1bb3e4015249639849f30e8e680df8a431b45d417ba8acf843d2451d95f"}, - {file = "websockets-17.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:983bcdc898662f6ba9d6a025c30d29946ff0986d9ad60d400af0da3671f7cbf3"}, - {file = "websockets-17.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0f088ddfd9d9bc5019e27ff3767411779e92b59db5bb1507f2731a5b61158"}, - {file = "websockets-17.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:19e2511412ad3393191de652513bc7a0ca3c93af143b32d96d46e59fbbddf1d4"}, - {file = "websockets-17.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb5e2bf969ac99a6ae3c71208a5eb05cfde973192540ffa6e1068b57fb78c4f8"}, - {file = "websockets-17.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:691780fca2be3dec512cb603cb91060271968cb4af86b51d07c57445c5754a37"}, - {file = "websockets-17.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d39c19b1ba6a6791050383fd69efdd3b63533e2254693d0263879cd5f5921ba"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e48ac2b302986c6f55cf61e8e36b4dd97d0132c5078a713a697a940934ba422e"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e136197f1262620ef2e507afc3ea759c1ae7d221886da20eec5f4c9f2618c2aa"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3eb44019a2b0b3b91bac95998f1e4e5589730421170e060fe654a2b7be727dc7"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e5855e574804398859c5fbaf4fc7882b96278b7f6572a3d889627e6eb6cfca59"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5dc29815520c329f5662f6eb3ebadecf0d4f8c82dfa416d4d6efbf8f39245559"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d1a4f9462da6496b6cb79bbb09c60d17f7e63e8a1df136797b3afabec9560e4d"}, - {file = "websockets-17.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9496bff5541086478264678bac73c0a75b2fde94fdf6568893bca1f7c6d50d18"}, - {file = "websockets-17.2-cp314-cp314t-win32.whl", hash = "sha256:e1e3bc8090a7eae79fdf634b63bdbfa3c93999991023c37c6fd3b469fc8ff5dc"}, - {file = "websockets-17.2-cp314-cp314t-win_amd64.whl", hash = "sha256:65a89a5bde227bfe908016f35b5bd347970cd1e5b0360f389502eba1c7fde6e0"}, - {file = "websockets-17.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1c27339934109dfaca83f18ab2c23db06714e9d5deca2c8e37e8f492ab90d20b"}, - {file = "websockets-17.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:a7c4bb26de6ef496d24822aee4f6a305d97cd33d21a2b85f290292d69ba1c25e"}, - {file = "websockets-17.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c08da1f15040bd1e1a6074bd4518a6ef20e67b1594ecfb0aa75e5b45f87e6d6d"}, - {file = "websockets-17.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3117abfd32b183bdb6194df9317766d32c6517f3d1c0aa8c62d5c6ccfda0b4a8"}, - {file = "websockets-17.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a046227daa7f191e843d26b911c1146233e9a33d249e0c954dcb3ac7c398710e"}, - {file = "websockets-17.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2901bdf24f20bc884124b3e88c61f7ece260c20c81e610f2196007395264a4aa"}, - {file = "websockets-17.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60e39adfecf998488166aca8ff24ab1ac406c9ecbecbcf9b3bcfc43cb1ec9a1"}, - {file = "websockets-17.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d4df62fd8448a85c752bbea1803cb3a2785e6fc8352009ab64ad7447af079b3c"}, - {file = "websockets-17.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8eea55fdfa9ba65c6981eea38bd20c800bce2f092a2803d82de764ecf0f071a"}, - {file = "websockets-17.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3f0def1279644acaa9bc861d4234af3f82ea9cee7e460dffac5cb63e691501e9"}, - {file = "websockets-17.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb78fb4158c12f77a934a003006784108a27a6553cfc0c6f10483c9c02e94f48"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f8969ad228115ad8869b5fed801f899e52ab8ad376fdb165ba4760a277c8258a"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:4a49ca342efc0800e6ae94ed5c9cbdcb319308f75e73c21181e4c24d6710e8dd"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:06fa3ce9c3154826c33d4395b225b2994aa64f1f3bcd8be8ed932019175d9268"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:50644d8715be7e0ec0682f9d7744b63008e199c5e1618a48fa153756a332235f"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:60deca33e584c09e91f70f8b55a0b1de7d671d6a63f051d154920f48bed717c7"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b5f79366a8d8dbb981d53ba800bb54a95454595ab8a4548c2b95501b32a08326"}, - {file = "websockets-17.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f2bbf3f28d0b63157577c8b774b9136f076afa6797e1a52a2ecd477f23cad3a8"}, - {file = "websockets-17.2-cp315-cp315-win32.whl", hash = "sha256:74836317b7010b579522bb52426f1e225608b042c9e78cbe2493522bebb8a318"}, - {file = "websockets-17.2-cp315-cp315-win_amd64.whl", hash = "sha256:aaead3d926e9ab4124ada727d20cd62d396649917822df4f771d1f07f1079b40"}, - {file = "websockets-17.2-cp315-cp315-win_arm64.whl", hash = "sha256:40960554e60eb60c3eec4ff9e42a80f84f8cd3ca9bc80a5481a61f1e64d807c9"}, - {file = "websockets-17.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9a2a60a7f0ea5f239efb6391d2b28630a640d82dad63e3bee47cf2c623c4495d"}, - {file = "websockets-17.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:cca2fcb72c007103740fa4fc3df19fdb1a318c641c69f3b0cc47ed63a889336e"}, - {file = "websockets-17.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b789356bc4e2e6c20ba52817f92c3fed74e24657654237ecd536c54843b80c6c"}, - {file = "websockets-17.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:222fb626fa15701a850eccc778be17312142b2f6a0e16aea80770b7459adb784"}, - {file = "websockets-17.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4497e87c34a2d21cbec1227858fec3af8e514dd70c47625557a122fcebc081dc"}, - {file = "websockets-17.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6281c171557ce0e408e19d9a223f22d915117ac38a5a7f32ed83809e7492316c"}, - {file = "websockets-17.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:08d97098644728bd1895caa7ecf3090b8e563d70809870d2adb33a107bd061d0"}, - {file = "websockets-17.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1fdb8d5a1660307dc6d36d0b7fc725213cbd7f80800904dc4896aa3208b89121"}, - {file = "websockets-17.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18b0a46e5e9b315e2b54ce8c3bafdeef0e1388ca363114fa868e6aab2dc58512"}, - {file = "websockets-17.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f115d5d804a2163dd89245710049078b0e726a58c1f44a1f86c2c6e79055d76"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1d829946a2e7630f92f9d7b45b62f3abe9f393cc2dea6a35edb3988f865e75f2"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:6c274fc1572edf7c197094a0eb1887d45fdc95254bc80597dc7599550486c06a"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:4173a4b8a025ae44313d9d9b4ecf31e886c7b7faf45386d51a8ca4ff2dcf3f2a"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:d8cfe9522ad69b6abb26b413ed1deca43cb915cefc588433d557cb3ae1c783e2"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:908d81d88bb16141613a6275059b5114656d5c2f0b5400b421d54fe6f1943507"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c6590e1eb624ff6b15b872421bc9a10bc6d2057635d69c6cd244ac3f928f85c6"}, - {file = "websockets-17.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:61040f6f7da5a279d2f77496c69d51132aba75f701c52bded400d4c639277b18"}, - {file = "websockets-17.2-cp315-cp315t-win32.whl", hash = "sha256:f90bad2839c185a1edf8ee22a257cfc8a39e0e337a0490ab185dfa76ef04d1bd"}, - {file = "websockets-17.2-cp315-cp315t-win_amd64.whl", hash = "sha256:315551f4ccedbbf9fd4f7e8bf037a5948c976ade0e919ba5d8f581d465f6f725"}, - {file = "websockets-17.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0a6220bdf8d5f11af71251a599092d89ac1d6bfac691c7f5951c5b07953947a0"}, - {file = "websockets-17.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2de1ccf298f5c9e0f27113836d742edb95f015eee3148f004ac386f7ba9a05b1"}, - {file = "websockets-17.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:761cde41439f0be761aa460e1451a31e2e14baf4a46db6fe4913e5a06a90df66"}, - {file = "websockets-17.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:15a7101b660a9f15fac34108c92cefc9848f6753a50acef8869e3cd94148fdb7"}, - {file = "websockets-17.2-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:214da56dba368f61b3d745c77630b2d03c61c02da7b42fe80ef6efba079d3077"}, - {file = "websockets-17.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80cbc645af23ac5c12096545c161626960114a1bc10f864760558d3b3e82ba18"}, - {file = "websockets-17.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:063508ce9e0db745f30ab52fc652f4e59efc79c2b74934b3837d5cdb974da620"}, - {file = "websockets-17.2-py3-none-any.whl", hash = "sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae"}, - {file = "websockets-17.2.tar.gz", hash = "sha256:36c2fb94c990cc2545143b12690e2de6c16300f9dbe5b4f33fa300cf57dc8792"}, + {file = "websockets-17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45cc1ccb12ced6d242335159e8221733c8696da5ad0b6ceef6debb0f5ce89110"}, + {file = "websockets-17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46ae05aa1aecd47d4d7de0f05ce2d1d2eb418f8de614964d754cc3b9a409c400"}, + {file = "websockets-17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43f270a5f21e12db08c73d83849a877c02ab7b7d0fb6e2718b5101d9c810212f"}, + {file = "websockets-17.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d875224bac1a05a20acade268a7a0e3498e7f8cf890acb7c2b23bacd2bd3659"}, + {file = "websockets-17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ad409b7ea102339041281117cf2dadfe71edbb653dc48037cc55d516fb7f448"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d43c311f3519fb1057b6114dc24e9060bc3fb6aedf4aa11160484154b66b63"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9eacb382f400bb5865e7e6d24bbe709a84510291e707f169dd4a24ee4a5ac6a"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f09f145b89ea292cf6c25fd7c8c0d90207fd6a9e54c7828499669996a1e5bdcc"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:49fb5b4960ebe1bc557b435da8687deef1302a08f030a33c5606fa6fb11b773b"}, + {file = "websockets-17.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:13cffaedde6e06183860fc92d88fe1d190abda73837fe4c14f5d4803c472f5dd"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef2950896673a9809e8e5ab857525924fbcacfc98b858b0509ccf50d341f80f4"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2d08ca4bf5a9cdb6fd81b86b51bd5cfb10527d3a1199888f2b02cc6c6a3d5f80"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5e0c0ed37472c4729f3001066510a656486f08f149dda1ebfd3dfe4450c9756e"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62059d739af7fc035ed6e63c5390118a60a5e9aa5a5541cd0486a83e741842c7"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc6f87d03e3691154673aaa8f08991723d6e3394c83ef2875c7a9049eac9595c"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3d78d93846eddc990551c1c31914d90d71957ca3af468969826f7ed0d7d1d51c"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2a377c7132b1e69be7a50d7434d7f26f69e93c70166ebb6d8f8cabf802ac4506"}, + {file = "websockets-17.3-cp311-cp311-win32.whl", hash = "sha256:29b01520c39ef2f23c88b002f11e6ef4165675f6c92f5f3578fd587d69b5a5fc"}, + {file = "websockets-17.3-cp311-cp311-win_amd64.whl", hash = "sha256:d338813a440bf6d2f0df55140d3f3080284410b1ebfe9df50e3847bcdb1a17bb"}, + {file = "websockets-17.3-cp311-cp311-win_arm64.whl", hash = "sha256:6a8248b1ba8861e48e4c7ced3895a1264d2a3b617de72ec94172ce6e64bf16c7"}, + {file = "websockets-17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:41665c7ea01d3ae497a0c61fd22d3424230d18782a5fceb79f04db9ccb2042c9"}, + {file = "websockets-17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8f7469408740cf06a79b01605f2ae0b2acc8839c03fcbbf05da49baee12d3302"}, + {file = "websockets-17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d68da281ac208b6236ffa8beef23fe7c7c956d8bf09b421d447232fd94441b35"}, + {file = "websockets-17.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078d1d41f45ee34eed5a4f3bbe7c588caecbdd6e12dfca73e8c1b0e803773793"}, + {file = "websockets-17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7f391838e8adcf1f2fe71561d9e5a9f74ea7ffa5052eb2db329aac677383136"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15a165673bb3fb57facbf040edf5c0556d20ccd2e0ccc5f23ebb69fb715fdc1"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28d1935c16782a1e3650c9baa79069b016fff739e292e4cb14bbb830d6930491"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c534c9524a2acd0b97b7022074bec3784ac273c683cf9535087e437672ea8531"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b459d29c8b82a420a98fa0ce8e9cf787ce0062d98cc90604ef9574fb3cc26ff8"}, + {file = "websockets-17.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53209dffd91b0aefed9ea630fa6ad5c2944d6210ba3a8a3a8664dc1575289b14"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9662a9fe457820c60365b3209e03cbd8e4a817195d55de65a80fa680b4bea136"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5670a762c7ae388bf8aa04199fd436180a4d28318dca9c1eae7b38604676dbfd"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ed59cb4ee258674a6ed9b8515e42079db02cb3be94f133fdd3f200183a40c49d"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f78bb2e82fe88912c9d7b25c98e57a9b08655579af89f164fd4c0657a075d62"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:71250e010eede33c1d638e2c7a98288520d67f782fd5eceecaeca3129233a09a"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:729989759feb81ff6493879a0d60a0bd7d2eee41001bbec0381a080318439382"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90d8c4fa5ba293cf92de351fbc292cdaadb1a1d58d031a1a97ff9b8fb6250ec7"}, + {file = "websockets-17.3-cp312-cp312-win32.whl", hash = "sha256:0824e6ecee838cc28e6481062574c7fde49f9a2cedfacd36f0e91cb4b37fd6fe"}, + {file = "websockets-17.3-cp312-cp312-win_amd64.whl", hash = "sha256:6644eecd99c4e8cdf72e646e17b391bb70b054f44becde37dcdc48cbec7e1527"}, + {file = "websockets-17.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a9133dbaedc6f1af358127275676171d9a1bb3e61f47ee62b2ff9acb78af3d0"}, + {file = "websockets-17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:52a2ca6f401fb49a8d5d9e0e48ade5f58f1bdda33132907b0e4e3b702628fa4e"}, + {file = "websockets-17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80578e91764bfca297ddca322c00a3136d993b829e9839e7fa378a19c1fcc25d"}, + {file = "websockets-17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe7a168d805c8a7bf1c2de55b7ab2bc958586bfd88d0e87235a92cac66b590a7"}, + {file = "websockets-17.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:54fcd24944e6d127d198c28dc363967b7131ff0c97833962c2ef2048018de02d"}, + {file = "websockets-17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de2c12555bf05546f3ee75733f2c81de70aab7f530e86d0d2162b0ddcb3c39ae"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c95d25b5382ee0e6191ecafdeabefdc1f7bff8dfee29239b9b79e760ef3b04bd"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8f244dfee05ea29b8064701880865eb1684f3a704a5665bd10b64694444dbbd4"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e99440ef446c7833690c1754e23b0188d440f3cb89812b0764c697a7880cb33"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:26838e2d95d7004c52e8c7b6b51676c872093a3b55a497824f899116a15a8a55"}, + {file = "websockets-17.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:508775efa11d26aec186d10154359f52276e2baf9ca201b52356330883276dfd"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2789d203babb6ceb2770aadc2394c1f99263ec37fba57a93064f9a8ad899e8ff"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cfef7f7aa434a1e632e0ebda14b39f4f1d7975e0fce5c77ea7b43156fabf9bd8"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:39f532fa0d9026de4b9315a175998646112776baad54d55348435c11b05d034e"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:85d6daa3a7ca404570ae130babbeb347ef9d3faca5300fb13cfa4bb48c3d30d1"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ce39283ef08b0f28478e920d4d89a1fc169176e568e8a6a821332673145522eb"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23046098ebf844753cfd211033624a8247520e5bf2194c24cb1a8c222917acff"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:724224d4c457207271239114bf43afafc3eca4a1b4661ccdd428d11869a03bbd"}, + {file = "websockets-17.3-cp313-cp313-win32.whl", hash = "sha256:5af7070d80d278832cbca9c3c3dd44b30a26dcfa3ae1e0f6f19ff34edc27b689"}, + {file = "websockets-17.3-cp313-cp313-win_amd64.whl", hash = "sha256:66027e401c70d0cf55021b91b8a7ec2cd5810e20602f945b4fe6ae8b37ed232b"}, + {file = "websockets-17.3-cp313-cp313-win_arm64.whl", hash = "sha256:6b14914e67044724c31a9360c7b89e9d070117c630746eefdc1620d967545acd"}, + {file = "websockets-17.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bc8fd230ef013951900c1fda100eebe4a4b1b407dde2a44186995042dda2c628"}, + {file = "websockets-17.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cdb8e4b0f6a41b857c74c03116e8ffbef59ef8e533e934b532843178990d01d5"}, + {file = "websockets-17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:018fab26140ce221c3853454a608e4e7004f4f10641cb97819dcd5b917bb2e99"}, + {file = "websockets-17.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd0f3037d501178a761fd2814370aca547f79e99a200a61a1f6e7b79ec3d78b9"}, + {file = "websockets-17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8dbb3862aa649de95660b6337d97101977f160b5b354d07b82fe309874d2c8bc"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af37a1e6b6bd506b597d5fb4cf111fb527d1b2223608a5d441d4738827cfd166"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:740bba4223fa5216d4d6c5dcd29e697f37229b8a61b69e1a50527b0ea993ab2c"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4170556762c6fd2913d99dfdd8dbb4c927584899df7facb1034627e355263c45"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d355b22270be8062360b9df2ee0b10277590789a1059257916fb9e256a0cff80"}, + {file = "websockets-17.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4aa85b34c21cf97b3470793d0bf0763d32bfd1e67b83067a0a3ba8dc3474dacf"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0e2e7c2c1a4f25db7b6596738d810c10732e77f852247300a1bc49e364d6dc9"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7fa69212ea60ea24e2d8aff23fe102b1f87f1b27a320f92d2e7b3468997a496a"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:80a6e37e235780f189558ab804f3cbc01c18f5c578995739b49613d544d34ebf"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:628efe13630ac3397e7d1278b6be6f22f37a86d638a324334b4cd43c1daf9928"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:443fa28652682d2a3ec3eca49af1b87cb2523561257683e62e03a98967b22138"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6705af9d1005127302c04d9f1533ba56e289336c465923ef878430e99295a0b9"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9bde5d82d7ac4859e0cb3a35e148f537a8d4e56aa43f42582b5f263982f095ae"}, + {file = "websockets-17.3-cp314-cp314-win32.whl", hash = "sha256:be7be57097a65a8fab3d0f0a10d12931deb626a7d8082eb732b1ae41f3a202e3"}, + {file = "websockets-17.3-cp314-cp314-win_amd64.whl", hash = "sha256:2b06213c8e1ce93b18d0476d7a5e647436c8f23d099888c9065e930e80f4bed5"}, + {file = "websockets-17.3-cp314-cp314-win_arm64.whl", hash = "sha256:6b7b540bb321f7f0a7b8f5d107f941bb361e3a154c0b594dae0394997b91b743"}, + {file = "websockets-17.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c9d06edfd974e918b40e0662a7e76dac13125529ee5ec9fbe42dc0d6faa209c3"}, + {file = "websockets-17.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a0bcf66289b9d017c175562c35c5304b5cc3ed56aded994ae30b0a9654ba405"}, + {file = "websockets-17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f3ed747b7651668131029c720fbc32c7ee2d129bdad200e6c8f4037b20f7913a"}, + {file = "websockets-17.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5868a02c4f6d698ddd19b4c9742748c03dec045289915806845189d96bc3d314"}, + {file = "websockets-17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa87ea2ad64e59fe5622c1dc5716106b6d6e7e4b26b848cb5f5710a75c292ef5"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65e937576141ac50fac32fa9736dbb3e0e4a7769f91be004011a09216c6ab05f"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:35381b575f70a08d4241e73c0cde4672bb9b2a9aae8767ca6b7fb61305a052c8"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5142798871a248b422f353e0684d51dfa119de3403a6fa0904b7cb806d9790ce"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e164c4a6868d11988c7f2386e3278ccff21c665cd7d08279b62709dd37a3167c"}, + {file = "websockets-17.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10b24e836ee0233ca078e1244f6ba40343a22127a9a736f46a7e7b18de2cc711"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:56a76556ab04ab54a675acce2cac8b4e74ea2cec558a7c9ed37e0367b04b5d50"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:33221e49be79d7bfe6d12bc52ef9458b256ab74fa8ac5660acfa3065de1c7870"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2fa79b9a48ae59744316c71a34cb9f76f30c93e117338522c5135cb7eac10351"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f87f02afe05c1c246012e3de100a77cfbabd17db709fe93cf074271ab239b312"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8135a549e27e9941a70e2d2156676ef2ffeff4b06e9d564dbaaae963482afdef"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ace8dd1efa5f44c482d091aa38322e20e3ffe5adcdba6b67ad9f4e7da58f3f68"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2129c9a5fc05a230ce088745a790af073e582b584badc7b05db067ee326f56d7"}, + {file = "websockets-17.3-cp314-cp314t-win32.whl", hash = "sha256:af7c474f0d8f4d2a196370e85033873c7b063a0b5da9a4f143aa023e3bd25407"}, + {file = "websockets-17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c63ad657695e8f1a48624754cb6b464118ac50d4c1d477c26461c98390f0a62"}, + {file = "websockets-17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:3c4f631dc8d00e470cde3ec0a35d4b58a817bea592411956f1cf4099dbb306b3"}, + {file = "websockets-17.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f300f28dff74ff4148c21e6971e4de6ca0c34e0086a3d95c017c5174ebf85ca5"}, + {file = "websockets-17.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:32029441af953e980b9940f042b337fe4ad9a97fa8b17a4ed878ec67b5b89ef4"}, + {file = "websockets-17.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09f007b2150eff015b2109f006e2ac2012d376f41486defa88ae78a776b00cff"}, + {file = "websockets-17.3-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bdc658c9474864893e0b6b94f1390bf6a912952df1870dd93bb7d1a902b5628"}, + {file = "websockets-17.3-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15decc0fe293d9e3f924bd483f3d76f3d0ff26845112a470e3f8842fec2c82a1"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:997617dd7856c5dad742bf6893b872319156dd43f47587541a59e4a852d52bbc"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a004c1fe5a72c496f28c6c1d8c2dc5cfe3278d99478e18f8b838c70a912b366f"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35fe35d20baa661cef4a12ee7bc42eaf2376d43ef9816ab29dd47ca957f7a73a"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8b3bede7d583bc9f47d8693a29984882e59077db1a5238ad977fc70ae8169741"}, + {file = "websockets-17.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dafbb58a149d1ebd5767b3f064f911c5ee0897608005e51c3904b0ada2a8d99c"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80326bc571164bbbb6bead2e35d0c943b23b24fde629032197c58151529d24d8"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:7d53fc4b90ef6e11de639dc18744d9be8cbd70ccb5ce80027aaffe7b1ab0215a"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:fdd00937f15f5f93c8e53d8060a8c4c010431ab595a9b5b50ddd569a04603944"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1dae5cd6c92d4440b15f20aa2987baa73c47ea14baa0fec8536a542e82e0ab33"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b225c656efa2dedecc81651d2290170af25887b7de4f38c5efc7c5ae2b9442ab"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:bd645b5d2f810a181c44f4a71135e7c365b44f4be9ed3d1892635be13a857553"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d2605a3b2af8d66f8b75312d223642c065ec773446c04566d9030ad5e2db13c9"}, + {file = "websockets-17.3-cp315-cp315-win32.whl", hash = "sha256:82e60c00cfb8493a5ba27cdf9b3695fdd153f666f73e11f18968f78ead1d8705"}, + {file = "websockets-17.3-cp315-cp315-win_amd64.whl", hash = "sha256:4d5f2492cb2192aab2d42e57f2caa2159b4fd20fde06f859ec13354f524b176c"}, + {file = "websockets-17.3-cp315-cp315-win_arm64.whl", hash = "sha256:55494475206113dbaf731dc8c5ffd5e1094175b753b7bbaad09fe3d7df61165c"}, + {file = "websockets-17.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:23cb6f6d64e3a389bb8ba150f947f9a6c8c0aca383e814ca3c5cffab738d5646"}, + {file = "websockets-17.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f27e7e8a85512b6a4b490d5756edde8e6772883a5800e1476172e44eda3f7d7c"}, + {file = "websockets-17.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6342f79d6f50e680c8aa59e0615d745e867e51ffd59c77093be60b7c433067af"}, + {file = "websockets-17.3-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fc23e72bd7f8a8e540fed2244524d0a9aeb6ae3325f3d56f04a7a3debe5b04c2"}, + {file = "websockets-17.3-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:722ab747ff5dc28070b1a8c020dba6466f13f2ab56bc251ad301fefa2454aa1e"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d20d6ea5aabbeba2e054d33e405192ef75dc12947bbd00ec97d23b14969d325"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:90cc9973c51811918247a78d9ab2f54871296e4e37e69a98aba4d3c9acedbf62"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63b87c93e28957e6dc2bb92d31727a863c9bf4a4daa080f0995fdcb0242c9166"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f7431e71072962d3ed1768038200aa4f1e76d9c2d7bedd554d57a55faaf6644"}, + {file = "websockets-17.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:534ed583fd4323de45d196535a444cebd1af2105e6e4e0cdcbf8cea266a8747a"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8879b6a4040ee925c638548238ab7fe612cf5fa8752fe847346da0da8be81c3f"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58e5d1e935a90878a935c7d2737a25c71cf4f1c13b6e6bef454ab722c4f338bb"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:0a657b51e4a5f4ac577c11e706ff9a884ea5aaf3e2666bae283a251c63e66acb"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:411aa0fb234f42a1444dea1e177890ddfc577b36ffdfafec25cb0d2c818e1af8"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:623d4b44f64efdf235b1e8dce266057073d93cde6197e7d7684f20c297f67f30"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5d0c871efc696ef5eb8514df64b86ab28736db753ebb42a2101f3b562f4849c0"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1db2604e6aedb7a2cb2f6e0575d6c9a852ccac8239703a930ac83b63309177a5"}, + {file = "websockets-17.3-cp315-cp315t-win32.whl", hash = "sha256:07246d4f0293ef0b87eff3d7dd88c1f03dd9f678fa39f6de87b2af8504e7e0de"}, + {file = "websockets-17.3-cp315-cp315t-win_amd64.whl", hash = "sha256:856bbb24f2ee6436af7609267e73309e4bb0a0f9982f79f1c2e01019279c6671"}, + {file = "websockets-17.3-cp315-cp315t-win_arm64.whl", hash = "sha256:a22c4956a4d858200dba83f0691bb692d77b0eeddfca80157820d20b85dea4f1"}, + {file = "websockets-17.3-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:34c78052277053bcb09810ac8e9ba4029f9e3e277833fc18ec1c928a6246ac91"}, + {file = "websockets-17.3-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:ef07e0b6b879f699a16fc7a513b974246fdeaef44908a021130cf4afe6a1c89c"}, + {file = "websockets-17.3-pp311-pypy311_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99c5637b74e2805ee4de9e2a43c8b45f678e50abb1c2da9880f0372f6498ba2f"}, + {file = "websockets-17.3-pp311-pypy311_pp80-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c460aeb12c2bd1c204944d9e9199595bf9e505eb542018d304bf9e63cb60cad"}, + {file = "websockets-17.3-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1c174b366565e101f903f9d1cc96d5508f805759dd37d012743f1c73ec0d7b"}, + {file = "websockets-17.3-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:c4b3f2c7d84f303da873b634d0beef83b5e1137294c3de4e23102841dac0cf4a"}, + {file = "websockets-17.3-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:0e65260a1b1d7b81651f47343d836de70b88b02358ae3658efa6382104ec37b1"}, + {file = "websockets-17.3-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:94c02b0702656f083c098bdbd93bce22a7aa7a2b34ba56d2472efb67dab7cf58"}, + {file = "websockets-17.3-pp312-pypy312_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ade76f8c84ade325bbed96806357fffd353fcc29a5166d6dc6d39381f6d9f76c"}, + {file = "websockets-17.3-pp312-pypy312_pp80-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a215749c9a88ef2842e880215193e56ae60b856773c99702b078236bc6d4adb"}, + {file = "websockets-17.3-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7b5d23ef144b32645d97bd99f382c4394c443395f45fe1576786838d26e308"}, + {file = "websockets-17.3-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:13eace1b315345ebac5b27821f5dcce24e5cf9976ab18f79e00a59bdd67e5d19"}, + {file = "websockets-17.3-py3-none-any.whl", hash = "sha256:dc2b495a92b3971966bf102e73589f950db3ec1ca6713265355ce7771d97b199"}, + {file = "websockets-17.3.tar.gz", hash = "sha256:1c7dd93a1c9f89297471493674b1fc1d9467cafd45374f2172213046bdb3a329"}, ] [[package]] name = "zipp" -version = "4.1.0" +version = "4.1.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.10" groups = ["main"] markers = "python_version < \"3.12\"" files = [ - {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, - {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, + {file = "zipp-4.1.1-py3-none-any.whl", hash = "sha256:8979f52d874162f485ff2981e3891f3a3317b7a3dd43ff1e1775b9304f307a9c"}, + {file = "zipp-4.1.1.tar.gz", hash = "sha256:7ebb7a44c021b29fd8dbd7cce6812d0d7b5b454521f93cc71af6ccd155aaa70b"}, ] [package.extras] From 61da28e522445f449339f8762da839d4644a2872 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 21 Oct 2026 10:17:56 +0200 Subject: [PATCH 233/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.13.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4fb64f6..d5bc5dcbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.0] - 2026-10-21 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.2...0.13.0 + +### ✨ Added + +- Profile detection now explains suggested profiles and lets users confirm them before validation + ([2b0cbaae](https://github.com/kikkomep/rocrate-validator/commit/2b0cbaae32ab53989536e1a6662d9d3f6bbb476f)). +- Opt-in extra checks extend validation beyond the standard profile rules + ([e3ef940b](https://github.com/kikkomep/rocrate-validator/commit/e3ef940b1a07ce95c9fa86358114d619f0b67aa5), + [8b0d1fcd](https://github.com/kikkomep/rocrate-validator/commit/8b0d1fcd39c77e86602ffe40aceba34545e9bfc5)). +- Automatic package-type detection and CLI selection distinguish attached and detached crates; + reports show the package type and validation scope + ([bebee42b](https://github.com/kikkomep/rocrate-validator/commit/bebee42bad8783fd696bcaa51a1cd5fbcf25ff35), + [06c29319](https://github.com/kikkomep/rocrate-validator/commit/06c29319284ab982ab6fc6fe3ded716ec366aeb4), + [595fcc8e](https://github.com/kikkomep/rocrate-validator/commit/595fcc8ef5f3504acd52014eac33de31717c82b5)). +- Dictionary metadata can be validated against files in a local package root + ([86a84ea5](https://github.com/kikkomep/rocrate-validator/commit/86a84ea536b50ef2f3e60b1aa4a07540b7152420)). +- Reusable validation preparation avoids repeating setup across validation calls + ([6bd45817](https://github.com/kikkomep/rocrate-validator/commit/6bd45817e2be936179ff007d727a0450eef8d6ba)). +- Workflow Run Crate checks require an action using the main entity as its instrument and support + additional action types + ([d88a1ce8](https://github.com/kikkomep/rocrate-validator/commit/d88a1ce841956a9a3df5f2049397b257d13b6672), + [57a1413f](https://github.com/kikkomep/rocrate-validator/commit/57a1413f3c1293bd7de673f7eb072ac31d07bc7c)). + +### 🐛 Fixed + +- Reusing prepared profiles across crates preserves absolute IRIs, relative identifiers, URI paths, + and SHACL class targets + ([dc258c3a](https://github.com/kikkomep/rocrate-validator/commit/dc258c3a9a384df9cee9181151d67d9e4a936c5f), + [5edb72e4](https://github.com/kikkomep/rocrate-validator/commit/5edb72e4c19e41eb2b839293bb969610909722cc), + [230856a1](https://github.com/kikkomep/rocrate-validator/commit/230856a199688e7af3dc6cd49bc462de36b2f910)). +- Package checks handle attached metadata-only inputs and dictionary metadata consistently; + reports identify the local package root + ([858a0d3c](https://github.com/kikkomep/rocrate-validator/commit/858a0d3c462b0cd6c0c15c7993772579651577b2), + [0c9967c0](https://github.com/kikkomep/rocrate-validator/commit/0c9967c02a42af5a8df443634c064449fe7c75e7), + [545e7448](https://github.com/kikkomep/rocrate-validator/commit/545e744825b4106b58aa549c6c8331c74630ae46)). +- Malformed profile hints and invalid JSON produce clearer messages, while unexpected detection + errors are no longer hidden + ([756dbb4e](https://github.com/kikkomep/rocrate-validator/commit/756dbb4e4469e855544e964010e30f2fbc102a4b), + [7a95271f](https://github.com/kikkomep/rocrate-validator/commit/7a95271f9f1dc0d2899dafb370b8dc9dbda2826c)). +- Removed duplicate extra-check tables from CLI output + ([9d7d4b76](https://github.com/kikkomep/rocrate-validator/commit/9d7d4b763f8e0db550922132f011837464df525c)). +- Fixed Sphinx crashes when including README sections and improved dark-mode readability + ([153efa65](https://github.com/kikkomep/rocrate-validator/commit/153efa655ca9b210fe3f796e0ba3bf3a58543e8d), + [f9b8822d](https://github.com/kikkomep/rocrate-validator/commit/f9b8822d59ebe54ebf3f44169adfd11d91f64e63)). + +### 📚 Documentation + +- Redesigned documentation with the Shibuya theme, clearer navigation, and live preview with + automatic rebuilds + ([c0212aaa](https://github.com/kikkomep/rocrate-validator/commit/c0212aaa79c91b720d2376752f2d466dfadb9504), + [83a84c4a](https://github.com/kikkomep/rocrate-validator/commit/83a84c4a02b0a66d8e476c6d6dfd075edfa2028c), + [d955005f](https://github.com/kikkomep/rocrate-validator/commit/d955005f381ce62cd7db973cb8ea504b85c8083c)). +- Expanded guides for profile detection, extra checks, package modes, and reusable validation + ([05f0dc30](https://github.com/kikkomep/rocrate-validator/commit/05f0dc305c89cde0e6f13553e1cdaeac81b09a33), + [2f4f97c5](https://github.com/kikkomep/rocrate-validator/commit/2f4f97c5c153dcb96829021808f732fe82965ce8), + [3b1a66e9](https://github.com/kikkomep/rocrate-validator/commit/3b1a66e95779d460b30388e905c76330128800ea), + [4c88ced7](https://github.com/kikkomep/rocrate-validator/commit/4c88ced7124ec4cea2caae45d1eacb85489c609d)). + +### ⚡ Performance + +- Reuse prepared profiles across crate bases and selected profiles when computing statistics, + reducing repeated profile loading and preparation + ([08d317e1](https://github.com/kikkomep/rocrate-validator/commit/08d317e1aae9377daed99c43505ca069da580798), + [401b8d64](https://github.com/kikkomep/rocrate-validator/commit/401b8d64b39182983f8ed446b8789fa4dc9e2f88)). + ## [0.12.2] - 2026-10-05 Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.1...0.12.2 From 60f8de7298d5299bf93a27f6ab290c587600a7d4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 21 Oct 2026 10:18:55 +0200 Subject: [PATCH 234/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.13.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7408374e1..538ddfb32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.12.2" +version = "0.13.0" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 64ded52427045db229a8adf6c76c7c076949920f Mon Sep 17 00:00:00 2001 From: Ali Date: Thu, 22 Oct 2026 12:16:58 +0100 Subject: [PATCH 235/271] Allow rich 14 Widen the rich constraint to <15.0 so roc-validator can be installed alongside Flower 1.32+, which requires rich>=14. --- poetry.lock | 11 +++++------ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 89c42b62e..9253aea0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -3071,20 +3071,19 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "rich" -version = "13.9.4" +version = "14.3.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952"}, + {file = "rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -4642,4 +4641,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "9d1199e9fdd35ffb8c9be707c6d0e59393fa99a9f037ecfd3addb5229cebd882" +content-hash = "afecd54c466f9a1dd99de93156d633b600cdef993a82d4fe241bc2d98d818b72" diff --git a/pyproject.toml b/pyproject.toml index 538ddfb32..917b5e669 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ python = ">=3.10,<4.0" rdflib = ">=7.1,<8.0" pyshacl = ">=0.26" click = ">=8.2,<9.0" -rich = ">=13.9,<14.0" +rich = ">=13.9,<15.0" toml = ">=0.10.2,<1.0" rich-click = ">=1.8,<2.0" colorlog = ">=6.9,<7.0" From 4bdea4903b85eacdb3bbe8ddcd3a22848033036e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 26 Oct 2026 11:51:15 +0100 Subject: [PATCH 236/271] =?UTF-8?q?fix(uri):=20=F0=9F=94=97=20handle=20loc?= =?UTF-8?q?al=20paths=20with=20URI-safe=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/utils/uri.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/utils/uri.py b/rocrate_validator/utils/uri.py index 51e90ab9c..8bfd7b567 100644 --- a/rocrate_validator/utils/uri.py +++ b/rocrate_validator/utils/uri.py @@ -16,6 +16,7 @@ import re from pathlib import Path from urllib.parse import ParseResult, parse_qsl, urlparse, urlsplit +from urllib.request import url2pathname from rocrate_validator import errors from rocrate_validator.utils import log as logging @@ -121,6 +122,7 @@ def __init__(self, uri: str | Path): if uri is None or (isinstance(uri, str) and not uri.strip()): raise ValueError("Invalid URI: empty value") self._uri = uri = str(uri) + self._is_filesystem_path = not is_external_reference(uri) try: # Inputs that are not external references are assumed to be local # paths, so the ``file:`` scheme is added explicitly. The @@ -135,7 +137,7 @@ def __init__(self, uri: str | Path): # the authority remains a reliable signal to tell a local file # (``file:///path``) from a remote one (``file://host/path``, # RFC 8089). - if not is_external_reference(uri): + if self._is_filesystem_path: uri = f"file:{uri}" # parse the value to extract the scheme self._parse_result = urlparse(uri) @@ -152,6 +154,9 @@ def uri(self) -> str: @property def base_uri(self) -> str: + if self.is_local_resource(): + # Encode filesystem characters exactly once, without resolving symlinks. + return self.as_path().absolute().as_uri() return f"{self.scheme}://{self._parse_result.netloc}{self._parse_result.path}" @property @@ -186,7 +191,11 @@ def get_query_param(self, param: str) -> str | None: def as_path(self) -> Path: if not self.is_local_resource(): raise ValueError("URI is not a local resource") - return Path(self._uri) + # Raw paths may contain literal percent escapes. Only explicit file URIs + # are decoded; url2pathname also handles platform-specific path syntax. + if self._is_filesystem_path: + return Path(self._uri) + return Path(url2pathname(self.get_path())) def is_remote_resource(self) -> bool: """ @@ -254,7 +263,7 @@ def check_availability(self) -> AvailabilityStatus: if logger.isEnabledFor(logging.DEBUG): logger.debug(e) return AvailabilityStatus.UNAVAILABLE - return AvailabilityStatus.AVAILABLE if Path(self._uri).exists() else AvailabilityStatus.UNAVAILABLE + return AvailabilityStatus.AVAILABLE if self.as_path().exists() else AvailabilityStatus.UNAVAILABLE def is_available(self) -> bool: """ From 8c3fb1d32640e7f619ead353d7993818d7802014 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 26 Oct 2026 12:12:59 +0100 Subject: [PATCH 237/271] =?UTF-8?q?test(uri):=20=F0=9F=A7=AA=20verify=20ha?= =?UTF-8?q?ndling=20of=20local=20paths=20and=20file=20URIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/test_local_crate_locations.py | 132 ++++++++++++++++++ tests/unit/test_uri.py | 45 ++++++ 2 files changed, 177 insertions(+) create mode 100644 tests/integration/test_local_crate_locations.py diff --git a/tests/integration/test_local_crate_locations.py b/tests/integration/test_local_crate_locations.py new file mode 100644 index 000000000..7af6fdba8 --- /dev/null +++ b/tests/integration/test_local_crate_locations.py @@ -0,0 +1,132 @@ +"""Regression coverage for issue #238: filesystem names and file URI inputs.""" + +import json +import shutil +import zipfile + +import pytest +from click.testing import CliRunner +from rdflib import RDF, URIRef + +from rocrate_validator import models, services +from rocrate_validator.cli.main import cli +from rocrate_validator.rocrate import ROCrate +from rocrate_validator.rocrate.package_type import PackageType +from tests.ro_crates import ValidROCrate12 +from tests.ro_crates_v1_3 import ValidROCrate13 + + +@pytest.fixture +def minimal_crate(tmp_path): + crate = tmp_path / "my crate" + crate.mkdir() + metadata = { + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Minimal crate", + "description": "A minimal RO-Crate", + "datePublished": "2026-10-23", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + }, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + return crate + + +@pytest.mark.parametrize("representation", ["path", "string", "uri", "relative"]) +@pytest.mark.parametrize("download", [False, True]) +def test_validate_crate_in_directory_with_spaces(minimal_crate, monkeypatch, representation, download): + monkeypatch.chdir(minimal_crate.parent) + inputs = { + "path": minimal_crate, + "string": str(minimal_crate), + "uri": minimal_crate.as_uri(), + "relative": minimal_crate.name, + } + result = services.validate( + models.ValidationSettings( + rocrate_uri=inputs[representation], + profile_identifier="ro-crate-1.1", + requirement_severity=models.Severity.REQUIRED, + disable_remote_crate_download=not download, + ) + ) + assert result.passed(), [issue.message for issue in result.get_issues()] + assert result.context.publicID == minimal_crate.as_uri() + "/" + graph = result.context.data_graph + descriptor = URIRef(minimal_crate.as_uri() + "/ro-crate-metadata.json") + root = URIRef(minimal_crate.as_uri() + "/") + assert (descriptor, RDF.type, URIRef("http://schema.org/CreativeWork")) in graph + assert (descriptor, URIRef("http://schema.org/about"), root) in graph + + +@pytest.mark.parametrize("as_uri", [False, True]) +def test_cli_validates_crate_with_spaces(minimal_crate, as_uri): + location = minimal_crate.as_uri() if as_uri else str(minimal_crate) + result = CliRunner().invoke( + cli, + [ + "validate", + "--profile-identifier", + "ro-crate-1.1", + "--requirement-severity", + "REQUIRED", + "--no-paging", + location, + ], + ) + assert result.exit_code == 0, result.output + + +@pytest.mark.parametrize("kind", ["zip", "metadata", "bagit"]) +def test_file_uri_loaders_preserve_packaging(minimal_crate, kind): + metadata = (minimal_crate / "ro-crate-metadata.json").read_text() + expected = PackageType.ATTACHED + if kind == "zip": + location = minimal_crate.parent / "my crate.zip" + with zipfile.ZipFile(location, "w") as archive: + archive.writestr("ro-crate-metadata.json", metadata) + elif kind == "metadata": + location = minimal_crate / "ro-crate-metadata.json" + expected = PackageType.DETACHED + else: + location = minimal_crate + (location / "bagit.txt").write_text("BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n") + (location / "data").mkdir() + (location / "data" / "ro-crate-metadata.json").write_text(metadata) + (location / "ro-crate-metadata.json").unlink() + crate = ROCrate.new_instance(location.as_uri()) + assert crate.package_type is expected + assert crate.metadata.as_dict()["@graph"][1]["name"] == "Minimal crate" + + +@pytest.mark.parametrize("version,source", [("1.2", ValidROCrate12().attached), ("1.3", ValidROCrate13().attached)]) +def test_warm_validation_preserves_encoded_crate_bases(tmp_path, version, source): + first = tmp_path / "my crate" + second = tmp_path / "my%20crate" + shutil.copytree(source, first) + shutil.copytree(source, second) + validator = models.Validator( + models.ValidationSettings( + rocrate_uri=first, + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + ) + initial = validator.validate() + repeated = validator.validate(second.as_uri()) + assert initial.passed(), [issue.message for issue in initial.get_issues()] + assert repeated.passed(), [issue.message for issue in repeated.get_issues()] + assert initial.context.publicID == first.as_uri() + "/" + assert repeated.context.publicID == second.as_uri() + "/" + assert initial.context.prepared_validation_plan is repeated.context.prepared_validation_plan diff --git a/tests/unit/test_uri.py b/tests/unit/test_uri.py index bfd26a414..ab017b824 100644 --- a/tests/unit/test_uri.py +++ b/tests/unit/test_uri.py @@ -229,5 +229,50 @@ def test_rocrate_uri_remote_invalid(): assert str(excinfo.value) == f'The RO-crate at the URI "{uri}" is not available' +@pytest.mark.parametrize("name", ["my crate", "my-crate", "caffè", "a+b", "100%", "my%20crate", "a#b?c"]) +@pytest.mark.parametrize("representation", ["path", "string", "uri", "localhost"]) +def test_local_path_and_file_uri_round_trip(tmp_path, name, representation): + path = tmp_path / name + path.mkdir() + values = { + "path": path, + "string": str(path), + "uri": path.as_uri(), + "localhost": path.as_uri().replace("file://", "file://localhost", 1), + } + uri = URI(values[representation]) + assert uri.as_path() == path + assert uri.is_local_directory() + assert uri.is_available() + assert uri.base_uri == path.as_uri() + assert str(uri) == str(values[representation]) + + +def test_literal_percent_path_does_not_select_space_directory(tmp_path): + (tmp_path / "my crate").mkdir() + literal = tmp_path / "my%20crate" + for value in (literal, str(literal), literal.as_uri()): + uri = URI(value) + assert uri.as_path() == literal + assert not uri.is_available() + + +def test_relative_local_base_and_symlink_preserve_path(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + target = tmp_path / "actual crate" + target.mkdir() + link = tmp_path / "linked crate" + link.symlink_to(target, target_is_directory=True) + uri = URI("linked crate") + assert uri.as_path() == type(tmp_path)("linked crate") + assert uri.base_uri == link.as_uri() + assert uri.is_available() + + +def test_remote_file_authority_cannot_be_converted_to_local_path(): + with pytest.raises(ValueError, match="not a local resource"): + URI("file://other-host/tmp/my%20crate").as_path() + + if __name__ == "__main__": unittest.main() From e9ef43ed0f4d1a3a1a8c3e65b37809deca865e6e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 28 Oct 2026 09:25:19 +0100 Subject: [PATCH 238/271] =?UTF-8?q?fix(crate):=20=F0=9F=94=97=20resolve=20?= =?UTF-8?q?entity=20IDs=20against=20JSON-LD=20@base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/rocrate/entity.py | 9 +++++++-- rocrate_validator/rocrate/metadata.py | 9 +++++++++ rocrate_validator/utils/rdf.py | 8 ++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/rocrate/entity.py b/rocrate_validator/rocrate/entity.py index a4a41c6c3..b3b31fd54 100644 --- a/rocrate_validator/rocrate/entity.py +++ b/rocrate_validator/rocrate/entity.py @@ -132,14 +132,19 @@ def id_as_path(self) -> Path: @classmethod def get_id_as_uri(cls, entity_id: str, ro_crate: ROCrate) -> URI: assert entity_id, "Entity ID cannot be None" + # JSON-LD resolves relative identifiers against an explicit document + # @base before they are interpreted as crate-relative payload paths. + # Keep the raw ID on the entity for reporting and graph lookups, while + # using its resolved form for local/remote classification. + resolved_id = ro_crate.metadata.resolve_identifier(entity_id) # Per RO-Crate 1.1 § 4.2.2, an `@id` is either a relative URI path or # an external URI/IRI (RFC 3986/3987). External references are used # as-is (without resolving them against the crate URI) so the entity # is classified as remote/web-based; this covers both authority-based # forms (``http://``, ``scp://``) and scheme-only ones (``urn:``, # ``doi:``, ``arcp:``). - if is_external_reference(entity_id): - return URI(entity_id) + if is_external_reference(resolved_id): + return URI(resolved_id) # Otherwise the `@id` is a relative path: if the RO-Crate itself is # remote, resolve it against the crate URI so the entity is still # classified as remote/web-based. diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index 6a88a7a8e..5511731ff 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -17,11 +17,13 @@ import json from pathlib import Path from typing import TYPE_CHECKING, Any, cast +from urllib.parse import urljoin from rdflib import Graph from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.utils import log as logging +from rocrate_validator.utils.rdf import extract_base_from_jsonld from .entity import ROCrateEntity @@ -186,6 +188,13 @@ def as_dict(self) -> dict[Any, Any]: assert self._dict is not None, "Metadata dictionary should not be None after loading" return self._dict + def resolve_identifier(self, identifier: str) -> str: + """Resolve an identifier using the metadata document's explicit JSON-LD ``@base``.""" + base = extract_base_from_jsonld(self.as_dict()) + if isinstance(base, str): + return urljoin(base, identifier) + return identifier + def as_graph(self, publicID: str | None = None) -> Graph: if not self._graph: # if the graph is not cached, load it diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index f6ee4a3e2..d1464192e 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -256,9 +256,13 @@ def extract_base_from_jsonld(json_data: dict) -> str | None: if isinstance(context, dict): return context.get("@base") - # If @context is a list, look for @base in each context item + # JSON-LD 1.1 §4.1 Context Processing Algorithm: + # https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm + # Contexts are processed in order, so the final local @base takes precedence. if isinstance(context, list): - for ctx in context: + for ctx in reversed(context): + if ctx is None: + return None if isinstance(ctx, dict) and "@base" in ctx: return ctx["@base"] From 49638aa7d8ab2ee87c280a74fea2bdc15ca0ae0e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 28 Oct 2026 09:27:12 +0100 Subject: [PATCH 239/271] =?UTF-8?q?fix(profiles):=20=F0=9F=8C=90=20use=20r?= =?UTF-8?q?esolved=20IDs=20for=20web=20entity=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/1.1/should/5_web_data_entity_metadata.py | 2 +- .../profiles/ro-crate/1.2/must/4_data_entity_metadata.py | 2 +- .../profiles/ro-crate/1.2/should/4_data_entity_metadata.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py index 2baa10480..5f1bdb98a 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py @@ -104,7 +104,7 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: continue if entity.is_available(): content_size = entity.get_property("contentSize") - actual_size = context.ro_crate.get_external_file_size(entity.id) + actual_size = context.ro_crate.get_external_file_size(entity.id_as_uri.uri) if content_size and actual_size is not None and int(content_size) != actual_size: context.result.add_issue( f"The property contentSize={content_size} of the Web-based Data Entity " diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 74ec0cb43..51daccd35 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -341,7 +341,7 @@ def check_availability(self, context: ValidationContext) -> CheckResultValue: logger.debug("Skipping downloadability check for directory entity '%s'", entity.id) continue try: - dl = check_downloadable(entity.id) + dl = check_downloadable(entity.id_as_uri.uri) if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py index 893fef699..28642a2fd 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py @@ -109,7 +109,7 @@ def check_availability_warning(self, context: ValidationContext) -> CheckResultV if entity.id.endswith("/"): continue try: - dl = check_downloadable(entity.id) + dl = check_downloadable(entity.id_as_uri.uri) if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False @@ -147,7 +147,7 @@ def check_content_size(self, context: ValidationContext) -> CheckResultValue: # content_int = int(str(content_value)) except (TypeError, ValueError, OverflowError): content_int = None - external_size = context.ro_crate.get_external_file_size(entity.id) + external_size = context.ro_crate.get_external_file_size(entity.id_as_uri.uri) if external_size is not None and content_int is not None and content_int != external_size: context.result.add_issue( f"The property contentSize={content_size} of the Web-based Data Entity " From 5e6a7de567a89957c52218b88f739286cb36698f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 28 Oct 2026 09:31:45 +0100 Subject: [PATCH 240/271] =?UTF-8?q?test(validation):=20=F0=9F=A7=AA=20cove?= =?UTF-8?q?r=20JSON-LD=20@base=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/test_data_entity_metadata.py | 55 ++++++++++++++++++- tests/unit/test_rocrate.py | 33 +++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py index 4e7fe9046..6d2d2b412 100644 --- a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py +++ b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py @@ -18,8 +18,9 @@ import pytest from rocrate_validator import models, services +from rocrate_validator.utils.http import HttpRequester from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER -from tests.ro_crates import InvalidDataEntity, ValidROC +from tests.ro_crates import CRATES_DATA_PATH, InvalidDataEntity, ValidROC from tests.shared import do_entity_test # set up logging @@ -30,6 +31,58 @@ paths = InvalidDataEntity() +def test_explicit_jsonld_base_does_not_require_web_entities_in_payload(): + """Issue #240: the profile crate's relative IDs resolve against its HTTPS @base.""" + crate_path = CRATES_DATA_PATH / "profile-crate" + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(crate_path), + profile_identifier="ro-crate-1.2", + requirement_severity=models.Severity.REQUIRED, + skip_availability_check=True, + ) + ) + + assert any(check.name == "Data Entity: REQUIRED resource availability" for check in result.executed_checks) + assert not [ + issue.message + for issue in result.issues + if "The RO-Crate does not include the Data Entity" in issue.message + and ("example1/" in issue.message or "index.html" in issue.message) + ] + + +def test_web_availability_uses_identifier_resolved_against_jsonld_base(monkeypatch): + """Web checks must request the expanded URI, not the relative @id.""" + requested_uris = [] + + class DownloadableResponse: + status_code = 200 + headers = {"Content-Type": "application/octet-stream"} + links = {} + + def raise_for_status(self): + pass + + def fake_head(uri, **_kwargs): + requested_uris.append(uri) + return DownloadableResponse() + + monkeypatch.setattr(HttpRequester(), "head", fake_head) + services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(CRATES_DATA_PATH / "profile-crate"), + profile_identifier="ro-crate-1.2", + requirement_severity=models.Severity.REQUIRED, + enforce_availability=True, + ) + ) + + expected_uri = "https://www.researchobject.org/workflow-run-crate/profiles/0.5/process_run_crate/index.html" + assert expected_uri in requested_uris + assert "index.html" not in requested_uris + + def test_missing_data_entity_reference(): """Test a RO-Crate without a root data entity.""" do_entity_test( diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index e11c0cec1..9fbb81540 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -747,6 +747,39 @@ def test_absolute_uri_data_entity_is_classified_as_remote(entity_id): ) +@pytest.mark.parametrize("entity_id", ["example1/", "index.html"]) +def test_explicit_jsonld_base_resolves_relative_data_entity_id(entity_id): + """Relative IDs with an HTTPS @base identify web data entities (issue #240).""" + metadata_dict = _metadata_dict_with_id(entity_id) + metadata_dict["@context"] = [ + "https://w3id.org/ro/crate/1.2/context", + {"@base": "https://www.researchobject.org/workflow-run-crate/profiles/0.5/process_run_crate/"}, + ] + crate = ROCrate.from_metadata_dict(metadata_dict) + entity = crate.metadata.get_entity(entity_id) + + assert entity is not None + assert entity.id_as_uri.uri == ( + "https://www.researchobject.org/workflow-run-crate/profiles/0.5/process_run_crate/" + entity_id + ) + assert entity.is_remote() + assert entity in crate.metadata.get_web_data_entities() + assert entity not in crate.metadata.get_data_entities(exclude_web_data_entities=True) + + +def test_last_explicit_jsonld_base_takes_precedence(): + metadata_dict = _metadata_dict_with_id("index.html") + metadata_dict["@context"] = [ + {"@base": "https://example.org/old/"}, + {"@base": "https://example.org/current/"}, + ] + crate = ROCrate.from_metadata_dict(metadata_dict) + entity = crate.metadata.get_entity("index.html") + + assert entity is not None + assert entity.id_as_uri.uri == "https://example.org/current/index.html" + + @pytest.mark.parametrize( "entity_id", [ From c27cf006536fcdd0b2e5ea3c8916c88d5ba5766f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 29 Oct 2026 08:49:37 +0100 Subject: [PATCH 241/271] =?UTF-8?q?chore(deps):=20=F0=9F=94=A7=20update=20?= =?UTF-8?q?dependencies=20in=20poetry.lock=20(v0.13.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 677 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 457 insertions(+), 220 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9253aea0f..b953e6012 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -317,14 +317,14 @@ ujson = ["ujson (>=5.10.0)"] [[package]] name = "certifi" -version = "2026.10.19" +version = "2026.10.23" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b"}, - {file = "certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae"}, + {file = "certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1"}, + {file = "certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd"}, ] [[package]] @@ -700,6 +700,7 @@ description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["test"] +markers = "python_version == \"3.10\"" files = [ {file = "coverage-7.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23219888477edd736b6fcaec1272d47d93b926e999641ffea7e53a1738e70b2b"}, {file = "coverage-7.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40c0f00899fe6181ae7f434ceb200e51f5ee4b8ed10e3b5f0b605f0cae15da87"}, @@ -830,6 +831,136 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "coverage" +version = "7.17.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.11" +groups = ["test"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2"}, + {file = "coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e"}, + {file = "coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2"}, + {file = "coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc"}, + {file = "coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e"}, + {file = "coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69"}, + {file = "coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9"}, + {file = "coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d"}, + {file = "coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d"}, + {file = "coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e"}, + {file = "coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657"}, + {file = "coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709"}, + {file = "coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f"}, + {file = "coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa"}, + {file = "coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673"}, + {file = "coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089"}, + {file = "coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67"}, + {file = "coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e"}, + {file = "coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35"}, + {file = "coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802"}, + {file = "coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc"}, + {file = "coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c"}, + {file = "coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d"}, + {file = "coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712"}, +] + [[package]] name = "debugpy" version = "1.8.22" @@ -953,14 +1084,14 @@ files = [ [[package]] name = "domdf-python-tools" -version = "3.10.0" +version = "3.11.0" description = "Helpful functions for Python 🐍 🛠️" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["docs"] files = [ - {file = "domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36"}, - {file = "domdf_python_tools-3.10.0.tar.gz", hash = "sha256:2ae308d2f4f1e9145f5f4ba57f840fbfd1c2983ee26e4824347789649d3ae298"}, + {file = "domdf_python_tools-3.11.0-py3-none-any.whl", hash = "sha256:561ac19b452cdfee01121f50117a338d03c51b36084d2f9617651113eec6aac8"}, + {file = "domdf_python_tools-3.11.0.tar.gz", hash = "sha256:518ac47b27a799a72b01377f21588e41550a59918c25a74fb55fc7d15fbb2434"}, ] [package.dependencies] @@ -1176,7 +1307,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version < \"3.12\"" +markers = "python_version == \"3.10\"" files = [ {file = "importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0"}, {file = "importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99"}, @@ -1194,6 +1325,31 @@ perf = ["ipython"] test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.17)"] type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] +[[package]] +name = "importlib-metadata" +version = "10.1.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.11" +groups = ["main"] +markers = "python_version == \"3.11\"" +files = [ + {file = "importlib_metadata-10.1.0-py3-none-any.whl", hash = "sha256:cf45ce66a929c64e994185a809c04840d0501db17e663810d993d0f4829265c7"}, + {file = "importlib_metadata-10.1.0.tar.gz", hash = "sha256:82b04047fa09792c1a8cfeddfe08bec79e4b13c29c8469b0d3949860f61017e8"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.17)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "iniconfig" version = "2.3.1" @@ -1922,169 +2078,169 @@ typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "msgpack" -version = "1.2.3" +version = "1.2.4" description = "MessagePack serializer" optional = false python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "msgpack-1.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec0030361cc861ac699b2ef1c695b741fa145c88f8667fa3d7e3f73deeb648a3"}, - {file = "msgpack-1.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c1efdd9181cb1b719ee46865f368a927f1c0c65d577798340b1194545b7515a"}, - {file = "msgpack-1.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c309a7abae1d14ba29a8bd0ddbd704a5e469d8e9bd9c3dee0e4ff53d7ae01d56"}, - {file = "msgpack-1.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bf390259cb25a6a1cd197c65810999b811f64cd38683251538bcc5a1e41f7d3"}, - {file = "msgpack-1.2.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39b6986c19e1f2dfa549d185dba6ccf1de2e4c0ba10d8cfc0048935b1c5f9109"}, - {file = "msgpack-1.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcc6800daac4922960f6eeb7a0dda3dd4105e0bf7bce0e83ebc465a78cb7bdba"}, - {file = "msgpack-1.2.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:968583e956d0427878050b371308c5f8647088732ef3e66a117dbe1192ec91e0"}, - {file = "msgpack-1.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1d6bcec3dbbdb89ca385d3a73e63ceae7b841fa0d7ca7c676f1a7bfe7fb2cdb8"}, - {file = "msgpack-1.2.3-cp310-cp310-win32.whl", hash = "sha256:a6b63917d60d6df451f328bd6afba8565e33c4afe1f62ec4ad758b78731c827b"}, - {file = "msgpack-1.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:4c0780095871ecc49a58b2ff6b1b43b25214704da67646557ca287a3f49fb2dd"}, - {file = "msgpack-1.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec90a9ae3e1169fa1171147340f0e97d941aa19fcd3b34e8339a55933ed042af"}, - {file = "msgpack-1.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9d7e9cbb0998bbfd363fd9a09c330520d5e9cb323c05b5a1a05865d23ccf2226"}, - {file = "msgpack-1.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6707d2fa2aa1bb5424ea0b05f44ffc989b15ab41a73ff5855bff4944fec7c8ac"}, - {file = "msgpack-1.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382b219de3d436de3baba0f4b0c6d4336e8f5858d0eb047918b13b69a71c6c55"}, - {file = "msgpack-1.2.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:186e6c602b8a9968b8e864c67d622a69279f7d1e55ae25f40e3bff7e815b2b62"}, - {file = "msgpack-1.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9276ba88891338f2617044429dfd080ae008c9868a25f6f1a7d004a35dc9ac0a"}, - {file = "msgpack-1.2.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c942c21a93f36b3a69e828c8945bb72c94dc2ffe488a2086950c812f3edf046c"}, - {file = "msgpack-1.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18a6ed513023001b28dcd3ba54966f6bb90a38274ba8d2640464bcab3a1b81d4"}, - {file = "msgpack-1.2.3-cp311-cp311-win32.whl", hash = "sha256:d0238cd05dec9ffbe0de1071df685ba63e30a36ac155285b1a094e727c38cbe9"}, - {file = "msgpack-1.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:30e1522e4173230dca4d9ad896f038f73c0da6c1edd42f4dbad88ac583cf5d46"}, - {file = "msgpack-1.2.3-cp311-cp311-win_arm64.whl", hash = "sha256:8ca67f77938ea6a3663aa9bd22b3e031f6da84d665be850abab910ee90728dfd"}, - {file = "msgpack-1.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89c930aece4e972b208ba589c8410b4167b05e411a5ea2cb25fd96f8bc47ee43"}, - {file = "msgpack-1.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:905a189853d6bdb204c7ae5f4ab77fb857448abfff574d3d93c62e2815b24b4f"}, - {file = "msgpack-1.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7b3d0018746b5997dd6b14a1870b07cc4c327d9101145d94a1fc264a51a06"}, - {file = "msgpack-1.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede33b2892ceb976283e009ad12fa1834cfdf1f9c43ee9c97849fc588d00a618"}, - {file = "msgpack-1.2.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:666ef5601ab0e6e345e47febc96aa81143cc932201543480cbb9499164f05ffb"}, - {file = "msgpack-1.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87cf2ef05ff2f2493ba29fcdaef27e960ca64dacfd13460ae29e6f92e0ed05bb"}, - {file = "msgpack-1.2.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b774ff994d844e541439ac5d2d49a14def4104830c3465e9394c153f86200ffb"}, - {file = "msgpack-1.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaf7e82249837e3aa97297b34a0bb9ff562027381631e057cea6e1367f10b438"}, - {file = "msgpack-1.2.3-cp312-cp312-win32.whl", hash = "sha256:7c047250096f9fc19dba26e3d1639b5e7a84114003605c94def667149a70ced1"}, - {file = "msgpack-1.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:3ec409b0d6aa8e9eec6eaf881b893caa215dbe68c5319ca96e8a271d81bb111d"}, - {file = "msgpack-1.2.3-cp312-cp312-win_arm64.whl", hash = "sha256:59612b4ed48a04cf024584218e813562f3b30a3bafa5f55abe300b15da314751"}, - {file = "msgpack-1.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:21bfa4d2aa0b04c1806ef778a1199e9e53ea2441bcbf284420a32083896320b8"}, - {file = "msgpack-1.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db84203b13aecc222f465061397fdd5b53b7ae73d2c95ffc1c8dc5be0153a709"}, - {file = "msgpack-1.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e0d7950ca3c1bbae291d0552dd3bb2792fc680629c4c0d44e47e5bab969f3ca"}, - {file = "msgpack-1.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07c9733089d1b176c3dd2f7fa268452f9d5d784d076473499d754a58e8d1fbbb"}, - {file = "msgpack-1.2.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f24a43b3560e20f825b807fe1e874bd73d53abaf8bbdcf258a6eb152cddbc1f5"}, - {file = "msgpack-1.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6576f348ed6cc4f31db6fd915a8e94245f042f50eae08d48732425e70638ea37"}, - {file = "msgpack-1.2.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd5a9f9f86a52c24713679aa2631956835f3842512964ff93f736ff76f1f530d"}, - {file = "msgpack-1.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9ddd28d3e9bbc602a9dced1591882c7fb9ab776eef8837da2c326fde19e2853"}, - {file = "msgpack-1.2.3-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:62cc1a4ef0e553bac32c8342e1f04834aca7de276b92744eb7307db77759b890"}, - {file = "msgpack-1.2.3-cp313-cp313-win32.whl", hash = "sha256:d2f9c4f85e47a44d26d5baf3b041eef23436e224d44eed273f01bd8a12048d9f"}, - {file = "msgpack-1.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb89b5dc30469c84bbf8684826eb851d82412ca95690e111b9ac5e8fb343961a"}, - {file = "msgpack-1.2.3-cp313-cp313-win_arm64.whl", hash = "sha256:471e12a6a42498a31490c206e0069e343b6a7c35db540be73a879eb06f5be047"}, - {file = "msgpack-1.2.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3a31905206722103a84c1f72633fe30692cff6732c9d262e09a27dbc468797c8"}, - {file = "msgpack-1.2.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3372475211a9ce1a23acefe512cb3e121d18c95dc74ed56cb1819ef40836ebf4"}, - {file = "msgpack-1.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9324c54995641c3d1f92a9d55093c8cde0ffa2fbc87a467a688ef60428393220"}, - {file = "msgpack-1.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8ef3a66e4b52d2d7fdd90df2984670124b2ff7546d76bb25dcf68ef47f7df58"}, - {file = "msgpack-1.2.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:902f3490db0e07a7d40b48536a85c9b28fbf1397e7e1658a45a55f958e303620"}, - {file = "msgpack-1.2.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e51eca14fbb65c4e0a5a9657346962bd3dca78c08e04e3d4dee70ef48687d30"}, - {file = "msgpack-1.2.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f42f146752eedb6765f07dcc04d72dab0a25779ec8d4a88c0085263ce114f22c"}, - {file = "msgpack-1.2.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ed5823c4efc20fe87d3530665f40ec18a002be003114814c21235cc8d256207"}, - {file = "msgpack-1.2.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:2487453ca1b6104442c6442f9a1a8fee1fe8f428a70d99d4cba799108b304150"}, - {file = "msgpack-1.2.3-cp314-cp314-win32.whl", hash = "sha256:6df430419f2338cb71e4a34d6e64f83c88ccd321f91f40ba4513400b36d864ec"}, - {file = "msgpack-1.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:84a6616d396ec1bc18a1e83e67c96a393ec35dfe5e17434a5be7b9aa0fe988ab"}, - {file = "msgpack-1.2.3-cp314-cp314-win_arm64.whl", hash = "sha256:7a003b02c6ee2eea6dfe0bb08818631e3597e69f0131f2a8250488a1cc553290"}, - {file = "msgpack-1.2.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ccea05b5542f6d283fef3f0a8e93a7f0be90af0ddeeef84c25c0216ba76dcae1"}, - {file = "msgpack-1.2.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1631e12fe572e181cd77e831f69335d6cd5278eac22e3db3f33cf264ac2ac18"}, - {file = "msgpack-1.2.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e54394b7dbe2e12ab032d9d21feef7bb61a90a150a2623633ba3781ba69dcb1f"}, - {file = "msgpack-1.2.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63bb7448a1e9111319ae2430c09a5596140c160422830d6271bc75730ff2ff9a"}, - {file = "msgpack-1.2.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:382bc88fe90f29f5ac8a0b65c7046ff255356f2f2f3186c30e370215736fa1dc"}, - {file = "msgpack-1.2.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c77e27790ad72989db783d5303825fba0b71550f00a490efba35cde7dc4b719f"}, - {file = "msgpack-1.2.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:700bc0fc9e968a292b9137ee70e7a012f7e115bf0107ce45e3a88202788dfc1e"}, - {file = "msgpack-1.2.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bd5f91ea75c45cafcc5433ba8fae59b708b736ec178d2441c40c499e9e079db"}, - {file = "msgpack-1.2.3-cp314-cp314t-win32.whl", hash = "sha256:7995a7c6a62a1d6e7df211b4a16de513bd99fd053525050a319f80f44fb8015e"}, - {file = "msgpack-1.2.3-cp314-cp314t-win_amd64.whl", hash = "sha256:bfe7d5b62cbe7aa664f0b3e2c49077f10fcdd06183d3014f8271ff3c5edbfbf9"}, - {file = "msgpack-1.2.3-cp314-cp314t-win_arm64.whl", hash = "sha256:1f585407f740a9eac04a3bb82c61d68a0ea78f90e29e670bfb086b9ce3a518dd"}, - {file = "msgpack-1.2.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:13221a6c81ebb8e43ea63a7251c35d54e4175cea37ebf3a62e911bdf42562a3c"}, - {file = "msgpack-1.2.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0955b9000725573d1457c1676944b370dd9643c8d18f25bda5ac72913f850949"}, - {file = "msgpack-1.2.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c91762c48cd686dc9cf2b142c0bc544083952de32f5853d6624c956e54b85e5"}, - {file = "msgpack-1.2.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f4ae8bd4ad9ba085fde95e95d055a896d19210238a4199a771a3cf36dceed49"}, - {file = "msgpack-1.2.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7013534a7163aa4f213c4d9864f1a8a7555daac6fcd48f699a198e29b436bfab"}, - {file = "msgpack-1.2.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:6a834097144aabe948b8ca9020a833e8026f7d0abbd0ec54bc7e50f45a8ce012"}, - {file = "msgpack-1.2.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d31864ba3933a589b6a00249f89c0eb422197f49128fc10da550e57e9cb0f377"}, - {file = "msgpack-1.2.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e15f70588f4db8cd10df0930145b186de70feb9db51710cd378b1399009655bd"}, - {file = "msgpack-1.2.3-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:b949cc25e4a09252cbcc54e66e507de914d0e94a3a7039bd54c299bf7037c098"}, - {file = "msgpack-1.2.3-cp315-cp315-win32.whl", hash = "sha256:8ec7a1d49ca6c2569d722ab5ec86e90089b0713900aa31905b47b4c4d9e78ce0"}, - {file = "msgpack-1.2.3-cp315-cp315-win_amd64.whl", hash = "sha256:79dfa38faf92f804aa61beec140d70b18418e1dde1778dbb77a87a4cce85aa8a"}, - {file = "msgpack-1.2.3-cp315-cp315-win_arm64.whl", hash = "sha256:ed899d73a22f286a72bd9528d63f2ab3030dbad8bf1527fc249319a50d61fb9d"}, - {file = "msgpack-1.2.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f56fba61b2516be7917cb00151f0d060b5b21184e3499bb57f0f7d9259bea124"}, - {file = "msgpack-1.2.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:69ad12cedb674c73527bed869cddb42b742cac79a207a614202a4abaa24ea173"}, - {file = "msgpack-1.2.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db9fb67a3a2e75247bae569d34ebb5ff61c0448a4f0d6dbf991dae68af39b007"}, - {file = "msgpack-1.2.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2574ef81c1c8c38b10e330f3f9406fd09198a776b002030fafcf8e7647e9e06e"}, - {file = "msgpack-1.2.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fafc3b8898b432b841d30a61082c599fa7f4d06885f9dc58ad72259e12059fa6"}, - {file = "msgpack-1.2.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a393e428f6ffb0dcb73308c1fff5593041c16ff42da66e5bac8a83a6107a54b0"}, - {file = "msgpack-1.2.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d1c1e8989a855b7f1f2a64ec4a80b23a631822903952770813857b2e4f460471"}, - {file = "msgpack-1.2.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e0bd394e999949c814f7912284243298de1b5a17b6a3dcb6cc8a79b156ffc4fa"}, - {file = "msgpack-1.2.3-cp315-cp315t-win32.whl", hash = "sha256:3d4c807ed050fe3ddbea5ba7e9f63d7136871ce42861be1f50ff739f0e91047a"}, - {file = "msgpack-1.2.3-cp315-cp315t-win_amd64.whl", hash = "sha256:5f304123b90e8b2e49867981b7f6061612c39f50cca51ee88de007c084cf68d3"}, - {file = "msgpack-1.2.3-cp315-cp315t-win_arm64.whl", hash = "sha256:f41ca154b7737b11893cdce3c78c61d703398a1cd54d4297bdad908392338a8e"}, - {file = "msgpack-1.2.3.tar.gz", hash = "sha256:32edb81a2b5eb7cd7c9d941b2bfbbb082fd2cd09e0e725930316af6b708db186"}, + {file = "msgpack-1.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a4e141a9c91d1502cf98f9ca9f87af222c7857e2bfa49c1d81343157c38a28e"}, + {file = "msgpack-1.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0626ea840cce866237c4da08439e9faa9cf79e6e4c62358978ee055961e8cff2"}, + {file = "msgpack-1.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25e1f2466c4ea0fb9b694a922f75eb92d7ce6262eda8cb8a5dada34fe990d486"}, + {file = "msgpack-1.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85b79ed643cfde70672152b3d1aee438ca647c5d2ffb7dba7226689ee9f6f44b"}, + {file = "msgpack-1.2.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1119e840d2c9164120f067740d99c9608d21d4ee9a172cf0f00dcafdf5624a4"}, + {file = "msgpack-1.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ce22584c8b12aa7e424dab08c481afd95ad4bd3007829fd1767d28b4dbbc3167"}, + {file = "msgpack-1.2.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ab62a50cab6f786d49f3584914c1088b0ed8de76bfe675b26a60c7bec426380"}, + {file = "msgpack-1.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:429329799c72335b0e37be65964ec5b89062e57fe7ad9ee779b886df5e34ddaf"}, + {file = "msgpack-1.2.4-cp310-cp310-win32.whl", hash = "sha256:1cdb51af4f8f8d20af66ea9384883bf99b8ddb3cdf05f09eecda921646e15135"}, + {file = "msgpack-1.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:d626fcacecaf3a5bfcd57e35709ce59a56a39cd4a3cd201d1b43c00d20bcbcda"}, + {file = "msgpack-1.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1095b1ebcc8c9d3e70c51240b7160d8e9da66044e0a45c3ef8403d3a9ef0c5ad"}, + {file = "msgpack-1.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4b47ec7422bef149223786a87df80067ac65bfc7c0565e5cf1dfbdc40acee50"}, + {file = "msgpack-1.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c75cfda0d3ccb1f6abc20e0608ad6b226e11f317b5ac297721504d2509e2bd9"}, + {file = "msgpack-1.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a45ceb5f199d5e388f28b550ba881096a3740d4bda10e661d77f59d9a2e8187a"}, + {file = "msgpack-1.2.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ca62b31a873c70b73f367bb2c1efec1d143016642c5191405549578184ff6ff"}, + {file = "msgpack-1.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f641971d3eec89ffca57fa36d151ad1cf1b6a214b47faca16cb165c7d45ef8be"}, + {file = "msgpack-1.2.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b9eff629190a4b118549b654df32a56ed535351819f690d0c935d185500cc90b"}, + {file = "msgpack-1.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e22c6571ab6ac9036da87d8a6fddb4baf86981844b480c0c60a4a4be417ee48"}, + {file = "msgpack-1.2.4-cp311-cp311-win32.whl", hash = "sha256:5d7d4d2a5fc6a604f20c546ee1e8327bed1e9329be2417cef9af1f799f3e55e0"}, + {file = "msgpack-1.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9db832747bd05834530189e564a3fe31e2617547dc1f3a005f835eafc2b8e048"}, + {file = "msgpack-1.2.4-cp311-cp311-win_arm64.whl", hash = "sha256:deb1730c4c0c1a8c975be2f43efea6ee4834d35031a39075f78917d0ba8b3c04"}, + {file = "msgpack-1.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:90cba4996b18ec1201fcaabd01ec48ff4b9755416bcd52105ded2f89fe145c49"}, + {file = "msgpack-1.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:112933726919f4239646cca9572e2c2fa5626358c670da8440aac09403100a48"}, + {file = "msgpack-1.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ca1e78106675f2aed07dc37b842ce19be70010ab22c5c5c04688f0840d48539"}, + {file = "msgpack-1.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4d5bbdbea8f5669b3a734d6c226772804fb120848f99cbe63a0ddee63b049c9"}, + {file = "msgpack-1.2.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d9ff7f3aaffd84010f1c501b05406a497584f5cab2f732eb9d89bf960e12f6f"}, + {file = "msgpack-1.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa10dc04c03408f31948ac1896a942141fd8d7a954b3612edac79f19663e4076"}, + {file = "msgpack-1.2.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:472a25463678f2795143a7cb42e88a1b59cc2375e9881917e69a9323bde42114"}, + {file = "msgpack-1.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbb8a677da3e845713925e9a203b8a120b1e9da9989836890ba4a4a187902701"}, + {file = "msgpack-1.2.4-cp312-cp312-win32.whl", hash = "sha256:2b0b7dff5beddc10f16ecea6b28c6b98014d1135d7015bd83adff5037f35848d"}, + {file = "msgpack-1.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:6e38b123681df22373d545520f54bbeddc4706d9235a1161061f12bd6648497d"}, + {file = "msgpack-1.2.4-cp312-cp312-win_arm64.whl", hash = "sha256:6795c588b210b2f13d62c8fcc23bf779b509b839ec5c197a7ab4f5191459e126"}, + {file = "msgpack-1.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb811ac3d463a75013b234d2aae56b9f0bdb0105caf4dd2ee65305b6a8a6078d"}, + {file = "msgpack-1.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:36258fd0909e1bf26e94604439108846432abe4ffa9a23c9dc2f6f1c60f94b60"}, + {file = "msgpack-1.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9c5ba0f053a7e98f013f083c9d1b2dffb26cad8c897c533d8286e44ed24c669"}, + {file = "msgpack-1.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fabd415c41e62006c6641fcdf5b3e866f581c4e11c58328c0d0f69584b9577d"}, + {file = "msgpack-1.2.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7433d340de45003db3ed909a642c9469b598d175530f7a707cbaba64287d27ce"}, + {file = "msgpack-1.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccd6e4af5347eb9828147320449bcd30e3321f6397d4583ff16047485dd8be3d"}, + {file = "msgpack-1.2.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:23d1459a1d0b4c9f6629df072d592eb74424b3ed3656b69af507bc9023cc027f"}, + {file = "msgpack-1.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e73761fe9fc55a4e9d2df8e8f59b0979231af4dbf80b8bf90cd64c74173d779"}, + {file = "msgpack-1.2.4-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:d088d1f552e129859db830692268747aba0a1ee3e6e0fe6344dbb3aab1df9f6c"}, + {file = "msgpack-1.2.4-cp313-cp313-win32.whl", hash = "sha256:892d4f17d7962bfd2c2928b0a47542bf2948f074a7c04721b73d4138b70481e4"}, + {file = "msgpack-1.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:318d4e634d2b320be10decd9968ad1ae0be1d80cba23b5528c37b429eeb0e3e0"}, + {file = "msgpack-1.2.4-cp313-cp313-win_arm64.whl", hash = "sha256:bec3e34749b90cfcf116149aad11295c720ff4a74a7d0a35abdb968a396c1524"}, + {file = "msgpack-1.2.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:895e9369185df4a874768849405c30cba7990caceb3fcb1692ebe62bb050f55e"}, + {file = "msgpack-1.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c97f5f17407e1761f23f1944be33cbf1d5ce67017db32544dcd5e16e0b1b4a3b"}, + {file = "msgpack-1.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2444a4e04f924630d059ae6eae8f91fdfcd28ed9fd17b21bacf4e74550a091f7"}, + {file = "msgpack-1.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d65b791ce4283e02b6267742ad540899155e1b32887b9cbdda47f38f5a076b5"}, + {file = "msgpack-1.2.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e4c67dbac05dce90a3fa1d845137617383c415e6b319062baac9734540706ae"}, + {file = "msgpack-1.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:781e856db4f2cb398e4992e2a74305295d3ebf694eec26fa0f4bc766eb9fa0a0"}, + {file = "msgpack-1.2.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cfc1cc237792f9a5664002b66acb961027d4184528ec9d10e0e42330f7972383"}, + {file = "msgpack-1.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f60de6ae13d4d2a12a483f15c5e47cb6fa2731aa32d92fb31178e2017d6574a8"}, + {file = "msgpack-1.2.4-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:06a0657a4110d563255006b7cb7147432f67827144e429c2cdb1c967548d1817"}, + {file = "msgpack-1.2.4-cp314-cp314-win32.whl", hash = "sha256:727a65b35db9f5ef516fc0229c4b05cab983092b1ae274a8a769b2d517fb0499"}, + {file = "msgpack-1.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:80f5d4fda1ba373d95933f98e6c1d18247feffac839fda3930bdd85e6561bb5f"}, + {file = "msgpack-1.2.4-cp314-cp314-win_arm64.whl", hash = "sha256:e5f67f0286f2ba2c6dbb4998404ed880112ca204ceda729cf239879cd89d3acf"}, + {file = "msgpack-1.2.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:45506bfb902cde24a0d33fb0581f58497ddb2682686dd1f6aa07e7b769df7ca4"}, + {file = "msgpack-1.2.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dc9dc681958e69f46b9f37def3e7fd6cc5dcfaa5943fb00bfe9c223a1a1052f6"}, + {file = "msgpack-1.2.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bef691e2a95119cc6bf3ded29325dbd89c4b6f8ff233070431948ed70902421"}, + {file = "msgpack-1.2.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e6106d2d085af42b2de72efeb38ed255241a24b32295ca889707c1d679c442"}, + {file = "msgpack-1.2.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:996dfc4774c240c8e6adf1166ae102141cfbed33c8856fde821bd6c0bd35ee41"}, + {file = "msgpack-1.2.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:efa1357fb0c7201e833de28638be371ee127476f7ccff9e7a94211b0afc33ddf"}, + {file = "msgpack-1.2.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b9270b63409e70c8c6deb5faedc81fc27c6bd0196b38084a8773f68286abf034"}, + {file = "msgpack-1.2.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b597bb01aff61d65e305c2cf2589c93c86609929d42d575bfc359f3c54ea8e1"}, + {file = "msgpack-1.2.4-cp314-cp314t-win32.whl", hash = "sha256:ad5bc540cbc34db236f8535721a9a872d12a9d88d1322470f1fc7c5e9dadfdb2"}, + {file = "msgpack-1.2.4-cp314-cp314t-win_amd64.whl", hash = "sha256:287e52c2ebaa94f8eca045628374859f51584e77bc07086e6835a9bb2204c774"}, + {file = "msgpack-1.2.4-cp314-cp314t-win_arm64.whl", hash = "sha256:f3f19fe5decd348b35b003ae5f32d3fa90877413a0183da676f748a3a7ab9270"}, + {file = "msgpack-1.2.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:765584b93ed6f0b561f0c56e9735f24945c0682f857c9b3ee3b4caad9c32f66a"}, + {file = "msgpack-1.2.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0221748c30ace75fde0dec8c470abe65fb5374cd4f37adb9db6aca7a3c362c0a"}, + {file = "msgpack-1.2.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5917972ce1bf5f85f761031e00dab6fe0ad9f880cb3d2d43ed74b330c850400"}, + {file = "msgpack-1.2.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ae8cf6012372cecb43016132d1b87ccc2ebb8ae5ab5078687399e82423386a"}, + {file = "msgpack-1.2.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c66cf97ac5675037f705ca433ad98ac9177f9523e7fdbb34e486ae29044c8"}, + {file = "msgpack-1.2.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:59887281c3c163a7afff7dffaaad2f78cd2b513b7e1a7843f1ee8e7cd6a94231"}, + {file = "msgpack-1.2.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9f46e70c8d22e96856967ab9d80c51dcf5bc63133e4da2d66fab64c0d4119b82"}, + {file = "msgpack-1.2.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:0a1fd6c31952155b7cd01061288c70303e6554ae855244dfb38a897fd94f8749"}, + {file = "msgpack-1.2.4-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:b1f08ec826417e3c8dc3edadb50f38debba033af6cb849229f0eac72f80f2342"}, + {file = "msgpack-1.2.4-cp315-cp315-win32.whl", hash = "sha256:e8ba23c838739fe929e868095171e71f2f38d6ce0fcf5840b576fb82648e58e6"}, + {file = "msgpack-1.2.4-cp315-cp315-win_amd64.whl", hash = "sha256:67310606cf2528a6dc3ece1298dcd5cc6c57add148b16d22c5111138aa84820c"}, + {file = "msgpack-1.2.4-cp315-cp315-win_arm64.whl", hash = "sha256:ce76e7a7c11459157887a8dadf07c107ff74eca98cf914f0de6ee7ab4b47104d"}, + {file = "msgpack-1.2.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fb9ddbf23b8c6e89752af399fcf4daed60055e239cda44c6acfaa76d19331e8c"}, + {file = "msgpack-1.2.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e283cf563166c21762131b88e3870ee2ec57757b08a8736929757b5a67541061"}, + {file = "msgpack-1.2.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e91bf8325f492e8eb3c3aa6de06b20593c225336bb52b585c913d7c1da025b"}, + {file = "msgpack-1.2.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfa86432bf329d6b976f5a00ad4c9064fd51736e4967a52526750236345cc2da"}, + {file = "msgpack-1.2.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:42e0a7dc26df739149478585d4264338b4d330f24b3e3b57d5aa89e48866e8ab"}, + {file = "msgpack-1.2.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8e680367090a58c966052d61418cc943111863538fd82471d61fca5271d911e1"}, + {file = "msgpack-1.2.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a6c9926f8871b261622fe6b599db72c69be5bca8fdc0976886f9a531a301dee9"}, + {file = "msgpack-1.2.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c50bac325dbf1a9c80e10da6dbd1100d7082f6b12e153084501e18e86cd3a037"}, + {file = "msgpack-1.2.4-cp315-cp315t-win32.whl", hash = "sha256:34051de19ae532ba47fcab5b0abd08c25c7a64ff763ad819f18764d45f3c2c81"}, + {file = "msgpack-1.2.4-cp315-cp315t-win_amd64.whl", hash = "sha256:6ad2f0808099d4281208ff2c756efa958a55a7c8eb21fe1c7052b95c584f02f1"}, + {file = "msgpack-1.2.4-cp315-cp315t-win_arm64.whl", hash = "sha256:e38d854f1c60e94f46d998765201126338bde3fe1b029be0d849094bcbef72c2"}, + {file = "msgpack-1.2.4.tar.gz", hash = "sha256:475c44e12f2a5de766a8448783f149c9b33f3f49b88953c6cd739a77d93c732c"}, ] [[package]] name = "mypy" -version = "2.4.0" +version = "2.4.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2"}, - {file = "mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23"}, - {file = "mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080"}, - {file = "mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f"}, - {file = "mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb"}, - {file = "mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220"}, - {file = "mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd"}, - {file = "mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74"}, - {file = "mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1"}, - {file = "mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4"}, - {file = "mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b"}, - {file = "mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd"}, - {file = "mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e"}, - {file = "mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90"}, - {file = "mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e"}, - {file = "mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1"}, - {file = "mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946"}, - {file = "mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989"}, - {file = "mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8"}, - {file = "mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324"}, - {file = "mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb"}, - {file = "mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1"}, - {file = "mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf"}, - {file = "mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303"}, - {file = "mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437"}, - {file = "mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e"}, - {file = "mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b"}, - {file = "mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566"}, - {file = "mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe"}, - {file = "mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89"}, - {file = "mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799"}, - {file = "mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97"}, - {file = "mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359"}, - {file = "mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3"}, - {file = "mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c"}, - {file = "mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76"}, - {file = "mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37"}, - {file = "mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a"}, - {file = "mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d"}, - {file = "mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818"}, - {file = "mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5"}, - {file = "mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf"}, - {file = "mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34"}, - {file = "mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3"}, - {file = "mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01"}, - {file = "mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141"}, - {file = "mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0"}, - {file = "mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975"}, - {file = "mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710"}, - {file = "mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5"}, - {file = "mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093"}, - {file = "mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e"}, + {file = "mypy-2.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2b4e0aa029ad0c1d42cc494daf86d1e0efd15dab4db7bd096ece29e8a9689c6a"}, + {file = "mypy-2.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c535efcb91f7e66f41b9bd2b5dbd78900a7609ce88f4c020d668e4c82d082b0b"}, + {file = "mypy-2.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0724cf95fabc87ca4a2d5b7f276b43f51afce8a3804bde704490b008e377e50b"}, + {file = "mypy-2.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f0a8b8f5dee799195a3f2670a5b2617ac7193d3563f52dbef42ad6770c6a9db"}, + {file = "mypy-2.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:d4c343b2a720741a016d720b255e75581a50e2bede61d53250484e18562a1d64"}, + {file = "mypy-2.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:8e437811ebd26d3e6258efb562a711b385c139be597dffed5e735c801d15c77f"}, + {file = "mypy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79a1e79199e980fb41f219cb3d4746d25c64b0b623840d3ed57df1cdc13a6502"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e18d0da3898b3bcd16192f15a206e7b8b8dd0f4a0b3e51aa5bd5c2afe749e2c"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69a15fc2e75858d6b15e91ed3a0dcff1a533265183b64734138791fe0f49aa5"}, + {file = "mypy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70971cc0bcb1d3f03d2eb68fa430a6481ccc03558dc001d9534d7e5fa274c392"}, + {file = "mypy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f7225951a277dd0d6add900731d487174b7d24ac133c09d670ed63b4d8cb90d"}, + {file = "mypy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:be4c03f33c55500191df2ed4a9b5e7edbdd1fff067fe361a5a9b1b5d3262da6e"}, + {file = "mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494"}, + {file = "mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531"}, + {file = "mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5"}, + {file = "mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be"}, + {file = "mypy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f1613bfe6726169a5680ec3dad5fe1f6d96285c12aded51ad432eadedd39d45"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:680ba5d9f0e3fe0c746b3d14df15f2a718e4bc308a1145616a1ea691ce7c354b"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb70621535c22fd4b1a0450d37ed1d6c5eb39656a1cebfb189084c12de54ba7"}, + {file = "mypy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4af5ad558033bf71a44a1be149ed61633522824f84c1b6bc2445d3fe2d2a5296"}, + {file = "mypy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:acdd82cb3a2eb10720e87706a128d75785d005a8c47e36a4973221524f41fc56"}, + {file = "mypy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:133886ef1a7a8cf673df55d865e41580c6c46aa9b8e31e1325dfe315c038f745"}, + {file = "mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453"}, + {file = "mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a"}, + {file = "mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a"}, + {file = "mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a"}, + {file = "mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6"}, + {file = "mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373"}, + {file = "mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6"}, + {file = "mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca"}, + {file = "mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971"}, + {file = "mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2"}, + {file = "mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb"}, + {file = "mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692"}, + {file = "mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100"}, + {file = "mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704"}, + {file = "mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1"}, + {file = "mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9"}, + {file = "mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1"}, + {file = "mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f"}, + {file = "mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999"}, + {file = "mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623"}, ] [package.dependencies] -ast-serialize = ">=0.11.2,<1.0.0" +ast-serialize = ">=0.12.1,<1.0.0" librt = {version = ">=0.16.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" @@ -2406,15 +2562,15 @@ files = [ [[package]] name = "platformdirs" -version = "4.13.5" +version = "4.14.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.11" groups = ["main", "dev", "docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e"}, - {file = "platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0"}, + {file = "platformdirs-4.14.0-py3-none-any.whl", hash = "sha256:1dadfc59a8a5d7565b3d9270ae8fa71c065595352c4277b41c11ee6c222c4b91"}, + {file = "platformdirs-4.14.0.tar.gz", hash = "sha256:83f009c1e8c5fc36869b5171755b291887bd2cade7965233cdfabd23d754720b"}, ] [[package]] @@ -2572,12 +2728,25 @@ description = "C parser in Python" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] -markers = "implementation_name == \"pypy\"" +markers = "python_version == \"3.10\" and implementation_name == \"pypy\"" files = [ {file = "pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80"}, {file = "pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc"}, ] +[[package]] +name = "pycparser" +version = "3.12" +description = "C parser in Python" +optional = false +python-versions = ">=3.11" +groups = ["dev", "docs"] +markers = "python_version >= \"3.11\" and implementation_name == \"pypy\"" +files = [ + {file = "pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e"}, + {file = "pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6"}, +] + [[package]] name = "pygments" version = "2.21.0" @@ -2672,14 +2841,14 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyshacl" -version = "0.40.1" +version = "0.40.2" description = "Python SHACL Validator" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyshacl-0.40.1-py3-none-any.whl", hash = "sha256:27dd58c8ddfa103303b4a8c40b2c666332ffc912dbcd3137f7adc7b7bc5e6bda"}, - {file = "pyshacl-0.40.1.tar.gz", hash = "sha256:011e3cf1a68b31747cb762ba3d755ae1bdcc464c8fad0dc212a9adc550719552"}, + {file = "pyshacl-0.40.2-py3-none-any.whl", hash = "sha256:edf0a7c15074902a1a420e8cfb89a764a9eed990b6ea15b8c30c40f2220eef61"}, + {file = "pyshacl-0.40.2.tar.gz", hash = "sha256:3b089fcfe82dccca2a9d560d88665ca0345f828b9cf21de554e611a55c2c7277"}, ] [package.dependencies] @@ -2702,27 +2871,27 @@ oxigraph = ["pyoxigraph (>=0.5.6)"] [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, - {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, + {file = "pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588"}, + {file = "pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=2" +packaging = ">=24" +pluggy = ">=1.6,<2" +pygments = ">=2.15" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "asynctest ; python_version < \"3.11\"", "attrs (>=23.1)", "coverage (>=7.5)", "hypothesis (>=6.75)", "mock", "numpy (>=1.26)", "pexpect (>=4.9)", "pytest-xdist (>=3.5)", "pyyaml", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -2766,29 +2935,30 @@ testing = ["filelock"] [[package]] name = "python-dateutil" -version = "2.9.0.post0" +version = "3.0.0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["dev", "docs"] files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, + {file = "python_dateutil-3.0.0-py2.py3-none-any.whl", hash = "sha256:3c79630202e4152c6add678446984236efe4f57e84d8e892aab0637bab82e9fd"}, + {file = "python_dateutil-3.0.0.tar.gz", hash = "sha256:7ebcb3c29a92f3ceafe79a7a48a5c45677e9e004bbeec5742ac0bbb00560b7a2"}, ] [package.dependencies] -six = ">=1.5" +six = ">=1.6" +tzdata = "*" [[package]] name = "python-discovery" -version = "1.6.4" +version = "1.6.7" description = "Python interpreter discovery" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "python_discovery-1.6.4-py3-none-any.whl", hash = "sha256:530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b"}, - {file = "python_discovery-1.6.4.tar.gz", hash = "sha256:bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df"}, + {file = "python_discovery-1.6.7-py3-none-any.whl", hash = "sha256:5422f67e5f533bb02d7b73de1c422ed74037536fd869e05156c07565926ec68e"}, + {file = "python_discovery-1.6.7.tar.gz", hash = "sha256:7f7c28568c8ec3d988d8bddbdbec8e65f08e8767e52f734e18b8b498549b9b70"}, ] [package.dependencies] @@ -3501,15 +3671,15 @@ files = [ [[package]] name = "soupsieve" -version = "3.0.3" +version = "3.0.4" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.11.5" groups = ["docs"] markers = "python_version >= \"3.12\"" files = [ - {file = "soupsieve-3.0.3-py3-none-any.whl", hash = "sha256:fa30e3ba4809cb81ce1f3209f2fbe3e779fc445f0439bc147a0d7c4601743f21"}, - {file = "soupsieve-3.0.3.tar.gz", hash = "sha256:7dcf6022eed0399eb9934a75e020148f7a2024c37b7dfcd3cf2c5505d69c364e"}, + {file = "soupsieve-3.0.4-py3-none-any.whl", hash = "sha256:8412826614f9c632227067d0985aeb9f584bd15efc852955430e097a9fe7afbf"}, + {file = "soupsieve-3.0.4.tar.gz", hash = "sha256:924fab84ae21604c19e175e1f39815eb6fa83ba23836e09e9de180925e5bc4c0"}, ] [[package]] @@ -3942,15 +4112,15 @@ full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2" [[package]] name = "starlette" -version = "1.9.0" +version = "1.10.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.11" groups = ["docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "starlette-1.9.0-py3-none-any.whl", hash = "sha256:fd3f159936e882d06c1108f84688f7cda2cf85fc6a592a50edbd175e882c2ddc"}, - {file = "starlette-1.9.0.tar.gz", hash = "sha256:3d931de246ed870e35ce605974440816ef895e592f255d1396cfdf191d4c4318"}, + {file = "starlette-1.10.0-py3-none-any.whl", hash = "sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4"}, + {file = "starlette-1.10.0.tar.gz", hash = "sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b"}, ] [package.dependencies] @@ -4101,6 +4271,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib optional = false python-versions = ">=3.9" groups = ["dev", "docs"] +markers = "python_version == \"3.10\"" files = [ {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7"}, {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1"}, @@ -4114,6 +4285,60 @@ files = [ {file = "tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687"}, ] +[[package]] +name = "tornado" +version = "6.6" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.11" +groups = ["dev", "docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "tornado-6.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:01abf15429d64f04ccfafc238b191cc2c177108b7a331f4461707ad77198c8c2"}, + {file = "tornado-6.6-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:11bb97c00981eb8254e55bd6a5ad960c983ae343fef8ae3ae5584cd1fb70236a"}, + {file = "tornado-6.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e152a3b2d3eae113b836d979823f7bb2320f7e334a84449729964921f374a4c"}, + {file = "tornado-6.6-cp311-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e912d4c4babd04db6f34b25b748174a4a176debe86c8610beedf1815f9af7fdd"}, + {file = "tornado-6.6-cp311-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33aabe8a03d426cef4e0fe5669f0643cadd983b54957f1a7ac2789ec326e9644"}, + {file = "tornado-6.6-cp311-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9aaf0e1c2fb9f046f30e2036b8d2060666399e8464d0a72769e43c754089567b"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61fc5513cea97720ab9bf54b04827ac0f4500c63f0b072b963d6145c798b4661"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:31e1a0d5cd704d2fe8a008c1cc27a0e2b67269ca263630a27985dddc160b5537"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:74416e41e9bdf70fbc2fb242f6fbbc7a58ec11e59d39f15fc5a421ecb6845118"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_s390x.whl", hash = "sha256:8cdd69104ae9d705b3f04d2563aa96e357043e267a455802daae9a1e633a37c8"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fc7c8a3263501fbe97518cd699101bc3c5f499a7dd4f75fc6cbb3bd9ee1db1f1"}, + {file = "tornado-6.6-cp311-abi3-win32.whl", hash = "sha256:b54ca69ff93d0374efb083a12a1e9ec2dd2036e8496d197e0e8ef77b8d9135c3"}, + {file = "tornado-6.6-cp311-abi3-win_amd64.whl", hash = "sha256:48f26ed9d7beb64e5f06adc864524e581ab6d62ba8ff46eaaa1ac2d0e6d753fd"}, + {file = "tornado-6.6-cp311-abi3-win_arm64.whl", hash = "sha256:29114ffd4e4de869d390393df9e9a0f191065aae1a96f38a58d35e464c04a867"}, + {file = "tornado-6.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:63fc8d243b52fd8dd9ed4ad02d0df08f2024f9aea632a433a8a6c91075076d44"}, + {file = "tornado-6.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d4b85cac795bfc6a7f20ba81871abb8c2ae8fba019061b8a9ae1c2f97e29637"}, + {file = "tornado-6.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e035c065edadfa696529d05eebcbeb25154c129b5aee6dbfb697a6abf5d4f697"}, + {file = "tornado-6.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b94e589ab855090c448b5c0937b3a75f1c1a604b7b36207e8236d209f720b9c"}, + {file = "tornado-6.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a394c8c9db9137f2a2b42869d9f7b91398fcf19da120d0dde4c4546172710f7"}, + {file = "tornado-6.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0215d1df6e4b5e736ddad5adf0af4138cf7437fdd39c77c5e69768e37850a632"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d711790e0a9576ad924d942c4607e4e401d697588ea6f30f6662824008a7add"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:034a66bae0f55421d007803e2385d2a59707d8829b2f026373d42f08256e9922"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5cb38483b077129056a94dd415a71d29ab4e42b46da894fee8b4657003f10b21"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:46bbbd107efa37777ac4b83601866d19b4dbd5b770a7a9123582633d95959ad3"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b85e9d0263cdbc027b95947c8b873a7bc7fd6a08d16bfb4f5047350b0808e464"}, + {file = "tornado-6.6-cp314-cp314t-win32.whl", hash = "sha256:4276eef0880e5fe4488e6799c2842f59af3b06637e9313ab4fb506f9c9c2d6cb"}, + {file = "tornado-6.6-cp314-cp314t-win_amd64.whl", hash = "sha256:10d4af98c6df09f8429b1340ccaea3a72887b86f695dfb8056bda517c8115a00"}, + {file = "tornado-6.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b1f0ef20a5278854f5fdac31637bafadb055405df28b7fc945e54add12b172e3"}, + {file = "tornado-6.6-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8f8e382da30fe042f27f27a871e21aefc2613870e5a41648de18446c72d6ea59"}, + {file = "tornado-6.6-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8c7a8a328396c2195454a2ecd9499512441bbce99505544a717e6a87d16ff6f9"}, + {file = "tornado-6.6-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca025bb0da81c294aef26f27427316df1525220820315f4bf32dc1e84a4d2232"}, + {file = "tornado-6.6-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:59c22ebc905ac2a267f8d9d8a878f86afbcbbde329d75c485f1422fc45a81b83"}, + {file = "tornado-6.6-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f89802bc0bcd15f988d9325b4e6d2171bb04ea68d188732556335e10d3fd00bd"}, + {file = "tornado-6.6-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:70043256d5958f9c655066153d23d60ea3e29bf9854f7fb75850e223c477758a"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:82b49495e3849d644b841ef33b35491160055997deaa93720ff647ef019421db"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:07b2bcaa4bf447bdbfb664e4c28344226540db3f0ebccd74886528338a240765"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c36b921374c302e54909bae17d9f354061d80e7ae7d8c94f73abbcc217866460"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:7412d9e37aa9f7048e4b591f520a95f7a097278ffab15f1ddf3f5c2320cf6648"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:15bc95112b2d24b0bfaee9a004e3f974da929e33b4244a7f695a22f45b08a5e5"}, + {file = "tornado-6.6-cp315-cp315t-win32.whl", hash = "sha256:f661c21117b875d612371f3eb243d0c69f56d9bdd6aee38b7c24453419dfe554"}, + {file = "tornado-6.6-cp315-cp315t-win_amd64.whl", hash = "sha256:ee8853ab35da510dc952879ebd9995c82bc10ffb7925fd87930dc0d6916f4b39"}, + {file = "tornado-6.6-cp315-cp315t-win_arm64.whl", hash = "sha256:7de62e2dfdf559fd7154d2b9aa58ff3d9cff8304ae0519341feb082618bc2a33"}, + {file = "tornado-6.6.tar.gz", hash = "sha256:34c4aa916d7f2b2d5cadee2c6850f5a4a7f3d688cbf58399ea9bd3057af4abd5"}, +] + [[package]] name = "traitlets" version = "5.16.1" @@ -4155,6 +4380,18 @@ files = [ ] markers = {test = "python_version == \"3.10\""} +[[package]] +name = "tzdata" +version = "2026.6" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["dev", "docs"] +files = [ + {file = "tzdata-2026.6-py2.py3-none-any.whl", hash = "sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec"}, + {file = "tzdata-2026.6.tar.gz", hash = "sha256:85d2cd9c09f81f0bb29db27759fc83af4f1e620efb814f8c5bf8b4cc4feece6d"}, +] + [[package]] name = "url-normalize" version = "3.0.1" @@ -4213,14 +4450,14 @@ standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", " [[package]] name = "virtualenv" -version = "21.14.12" +version = "21.15.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.14.12-py3-none-any.whl", hash = "sha256:7820aeaef23f1f59a96458edfc7f9d3deac937155718de15f1c9a64471e15551"}, - {file = "virtualenv-21.14.12.tar.gz", hash = "sha256:bbcfb78e36c22ad70c6917c4aecb5e63477d3c138e9f88c946f320e31859c76a"}, + {file = "virtualenv-21.15.1-py3-none-any.whl", hash = "sha256:0d14a991689c67600316880eadc0a37b7690cb632e3b4ac67e2bc05b2b2e24a4"}, + {file = "virtualenv-21.15.1.tar.gz", hash = "sha256:e79c90b8f601701599c25905e02839924fede2c798d52325670d24fbdaab74ee"}, ] [package.dependencies] @@ -4279,41 +4516,41 @@ anyio = ">=3.0.0" [[package]] name = "wcwidth" -version = "0.9.3" +version = "0.9.4" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "wcwidth-0.9.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b119ef60177a92b4f298b80e98464c2922a1fba3ed77fe9a2bcb69edf1b9af4"}, - {file = "wcwidth-0.9.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aab90ab489850f87633922c3064f6d0442d8f6ae5407a62f82bdb1f424179c05"}, - {file = "wcwidth-0.9.3-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:9e1cbe18c18d152e1e68e4e2561dab6c2bc988f8fd337a1fb2e1e0809c3fa568"}, - {file = "wcwidth-0.9.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c12abd75ffcbad417e7770a6feb82413c102ccce5c435068c39a82e2218b00b3"}, - {file = "wcwidth-0.9.3-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9ab8bfee107e08de09e15f9f6afb9fe0f64c8d64fd296d0d8557bafe71ea3f93"}, - {file = "wcwidth-0.9.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f875a4c9e62df2ecfa70d99b24c551f2245609858e179c3a40e74631b2d08a4"}, - {file = "wcwidth-0.9.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:70a2ca239b85a219324968993c76c117fe423d4e1a7c9d860c999c66192f2570"}, - {file = "wcwidth-0.9.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:537ea00b6792bb243cd09582322c54dbc848879e5d34ea4c5fee0147529685ab"}, - {file = "wcwidth-0.9.3-cp310-abi3-win32.whl", hash = "sha256:f2f943c4ef14e9baba730740265f4e5bf5fc8473550e36cd92828cd5663561a6"}, - {file = "wcwidth-0.9.3-cp310-abi3-win_amd64.whl", hash = "sha256:569a0cdb826d3097b6dbacfaa45aa0002d72a2ba6fa8a63251ea43ac4dd7b6a4"}, - {file = "wcwidth-0.9.3-cp310-abi3-win_arm64.whl", hash = "sha256:80557db1f77148b78e1b3c2a9cc4ebc4feabef42aaf69a555565057031c32367"}, - {file = "wcwidth-0.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6f3e855d3a09af8b7b6d56e76bc270681032f8a37d527a8d5707a441958e3730"}, - {file = "wcwidth-0.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1bbc8ebf133422cadfefea95a9f89d6c9e9a1ae19930bef7862a9b59b20bbad"}, - {file = "wcwidth-0.9.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7a95d8c784cbccbbcb808eaadfd156cd282b2bdd95e89b32505e3443381ce61d"}, - {file = "wcwidth-0.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a44acbb23b525070a0b8c343dd7cab7a4020aff73b38804f7a66d6a66e16e3fc"}, - {file = "wcwidth-0.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a7c0405a3bf3c564e481d6b2d55c99cc231bb68cf8f4a0995944bbce5ebf1a5d"}, - {file = "wcwidth-0.9.3-cp314-cp314t-win32.whl", hash = "sha256:457f986e4867111cb037ccbf0d97ae100b1ecad030d43a020e7207f1e29649d5"}, - {file = "wcwidth-0.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:f11789e520ade8017fa93b01a4b8985571bb433eaefc2f857b57e36875e2e1d8"}, - {file = "wcwidth-0.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:eef39f3260991d8767189d296f0116fa4b45ad9c3a9a16d5af9187fb66b27564"}, - {file = "wcwidth-0.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:52a13869704d43a4add59c68ebc892198e53063883114783bf0b7457e250e3f4"}, - {file = "wcwidth-0.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9470169fcb4e5340269175ebb3642dd5bfc8f82da8c86f29fc1fefc6c2b9bc1b"}, - {file = "wcwidth-0.9.3-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c4531d93344441a7948e665854f0c40e3a8342ee106436d1531efa7a9fa7641e"}, - {file = "wcwidth-0.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:658dcbce546e7f28ee361f88e4a8e591dd56f46bff6084e11b4ad8310ed99946"}, - {file = "wcwidth-0.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9362be098c43d076aa43754face3cc0180fa81cea9ba304240b15490f2617e8"}, - {file = "wcwidth-0.9.3-cp315-cp315t-win32.whl", hash = "sha256:16d09df90b3a8f799ea14c726a41d9b906c3104f95f46d158e1ebe45cf4d547b"}, - {file = "wcwidth-0.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:3e3e31d6c17d6da3051f7eb854d5e4bebdc52a08a7a52adc2da6f15af51304e8"}, - {file = "wcwidth-0.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5d3b0b6c37f40e8d8298941ec373a426a892bb9a514328abcc9226cdc60ae6eb"}, - {file = "wcwidth-0.9.3-py3-none-any.whl", hash = "sha256:e3c9069c54a2da0bf4f6c7e65a735cb1da0f47601a15cb5fef7e4b6ffca4cf60"}, - {file = "wcwidth-0.9.3.tar.gz", hash = "sha256:dfe7cfbd4cb59db9fe0931bd6091ab4a95b3acc5d56bfb50fa9fef0ccba5dc5c"}, + {file = "wcwidth-0.9.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d"}, + {file = "wcwidth-0.9.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4"}, + {file = "wcwidth-0.9.4-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09"}, + {file = "wcwidth-0.9.4-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef"}, + {file = "wcwidth-0.9.4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2"}, + {file = "wcwidth-0.9.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f"}, + {file = "wcwidth-0.9.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763"}, + {file = "wcwidth-0.9.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17"}, + {file = "wcwidth-0.9.4-cp310-abi3-win32.whl", hash = "sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62"}, + {file = "wcwidth-0.9.4-cp310-abi3-win_amd64.whl", hash = "sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c"}, + {file = "wcwidth-0.9.4-cp310-abi3-win_arm64.whl", hash = "sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49"}, + {file = "wcwidth-0.9.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f"}, + {file = "wcwidth-0.9.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7"}, + {file = "wcwidth-0.9.4-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5"}, + {file = "wcwidth-0.9.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967"}, + {file = "wcwidth-0.9.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633"}, + {file = "wcwidth-0.9.4-cp314-cp314t-win32.whl", hash = "sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3"}, + {file = "wcwidth-0.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab"}, + {file = "wcwidth-0.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95"}, + {file = "wcwidth-0.9.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664"}, + {file = "wcwidth-0.9.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279"}, + {file = "wcwidth-0.9.4-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed"}, + {file = "wcwidth-0.9.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d"}, + {file = "wcwidth-0.9.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d"}, + {file = "wcwidth-0.9.4-cp315-cp315t-win32.whl", hash = "sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27"}, + {file = "wcwidth-0.9.4-cp315-cp315t-win_amd64.whl", hash = "sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6"}, + {file = "wcwidth-0.9.4-cp315-cp315t-win_arm64.whl", hash = "sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b"}, + {file = "wcwidth-0.9.4-py3-none-any.whl", hash = "sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d"}, + {file = "wcwidth-0.9.4.tar.gz", hash = "sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954"}, ] [[package]] @@ -4619,15 +4856,15 @@ files = [ [[package]] name = "zipp" -version = "4.1.1" +version = "4.1.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.10" groups = ["main"] markers = "python_version < \"3.12\"" files = [ - {file = "zipp-4.1.1-py3-none-any.whl", hash = "sha256:8979f52d874162f485ff2981e3891f3a3317b7a3dd43ff1e1775b9304f307a9c"}, - {file = "zipp-4.1.1.tar.gz", hash = "sha256:7ebb7a44c021b29fd8dbd7cce6812d0d7b5b454521f93cc71af6ccd155aaa70b"}, + {file = "zipp-4.1.2-py3-none-any.whl", hash = "sha256:b30cb6cd253122169c47de245ca24a86bac2a5d3802ba15bf7ba6414eb9e144d"}, + {file = "zipp-4.1.2.tar.gz", hash = "sha256:cf474b90b07ea932637e17c6b9479de2b2882900b0d311bdaf4f5c000a15a97f"}, ] [package.extras] From b61e688e6037b0298b9a9a03f9887052e7c55e5a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 29 Oct 2026 08:55:51 +0100 Subject: [PATCH 242/271] =?UTF-8?q?chore:=20=F0=9F=94=96=20update=20change?= =?UTF-8?q?log=20(v0.13.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bc5dcbe..7fec66c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.1] - 2026-10-29 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.13.0...0.13.1 + +### 🔧 Changed + +- chore(deps): update dependencies in poetry.lock, including PySHACL 0.40.2 and Starlette 1.10.0 ([c27cf006](https://github.com/kikkomep/rocrate-validator/commit/c27cf006536fcdd0b2e5ea3c8916c88d5ba5766f)) +- build(deps): allow Rich versions below 15 for compatibility with Flower 1.32+ ([64ded524](https://github.com/kikkomep/rocrate-validator/commit/64ded52427045db229a8adf6c76c7c076949920f)) +- build(deps): bump crate-ci/typos ([e022f654](https://github.com/kikkomep/rocrate-validator/commit/e022f6549358fd5d30ab7a9e3a68354d5aab7c5b)) + +### 🐛 Fixed + +- fix(profiles): use resolved IDs for web entity checks ([49638aa7](https://github.com/kikkomep/rocrate-validator/commit/49638aa7d8ab2ee87c280a74fea2bdc15ca0ae0e)) +- fix(crate): resolve entity IDs against JSON-LD `@base` ([e9ef43ed](https://github.com/kikkomep/rocrate-validator/commit/e9ef43ed0f4d1a3a1a8c3e65b37809deca865e6e)) +- fix(uri): handle local paths with URI-safe conversion ([4bdea490](https://github.com/kikkomep/rocrate-validator/commit/4bdea4903b85eacdb3bbe8ddcd3a22848033036e)) + ## [0.13.0] - 2026-10-21 Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.2...0.13.0 From 7e2c612c4bd948d04a2c40d49cd2962eff94e00d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 29 Oct 2026 08:56:19 +0100 Subject: [PATCH 243/271] =?UTF-8?q?chore(release):=20=F0=9F=94=96=20bump?= =?UTF-8?q?=20version=20to=200.13.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 917b5e669..0039e4a36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.13.0" +version = "0.13.1" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 9edf2e3c2951a776aeabc51767afe94fd673ac02 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 29 Oct 2026 10:33:13 +0100 Subject: [PATCH 244/271] =?UTF-8?q?fix(metadata):=20=F0=9F=94=97=20point?= =?UTF-8?q?=20PyPI=20to=20the=20documentation=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0039e4a36..4a15cf729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ license = "Apache-2.0" # Links to the GitHub repository and the project homepage/documentation repository = "https://github.com/crs4/rocrate-validator" homepage = "https://github.com/crs4/rocrate-validator" -documentation = "https://github.com/crs4/rocrate-validator" +documentation = "https://rocrate-validator.readthedocs.io/en/latest/" # Keywords and classifiers keywords = [ "RO-Crate", From 70777f3659a5af04611da9869d33299cb596d5dd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 5 Nov 2026 09:39:20 +0100 Subject: [PATCH 245/271] =?UTF-8?q?fix(models):=20=F0=9F=A7=A9=20propagate?= =?UTF-8?q?=20effective=20IDs=20through=20profile=20inheritance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models/profile.py | 60 +++++++++++++++++++---------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 3c678caeb..459a52469 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -47,6 +47,7 @@ EffectiveRequirementCheck, RequirementCheckRelation, ) +from rocrate_validator.models.requirement import RequirementLoader from rocrate_validator.models.severity import Severity from rocrate_validator.utils.collections import MapIndex, MultiIndexMap @@ -465,20 +466,22 @@ def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequi A check defined by this profile replaces direct-parent checks with the same name and severity. Checks composed through ``isRuleOverlayOf`` are reported in this profile's namespace; ordinary inherited checks retain - their source identity. + the effective identity exposed by their ancestor. """ source_profile = check.requirement.profile if source_profile == self: replaced_checks = tuple(check.overrides) - identity_check = check + # relative_identifier is " ."; omit the severity in the effective ID. + relative_identifier = check.relative_identifier.split(" ", maxsplit=1)[-1] overlay_replacements = tuple( parent_check for parent_check in replaced_checks if parent_check.requirement.profile.uri in self.is_rule_overlay_of ) if len(overlay_replacements) == 1: - identity_check = overlay_replacements[0] - relative_identifier = identity_check.relative_identifier.split(" ", maxsplit=1)[-1] + parent_check = overlay_replacements[0] + parent_identity = parent_check.requirement.profile.effective_requirement_check(parent_check) + relative_identifier = parent_identity.identifier.rsplit("_", maxsplit=1)[-1] return EffectiveRequirementCheck( check=check, identifier=f"{self.identifier}_{relative_identifier}", @@ -489,14 +492,22 @@ def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequi replaces=replaced_checks, ) - if source_profile.uri in self.is_rule_overlay_of: - relative_identifier = check.relative_identifier.split(" ", maxsplit=1)[-1] - return EffectiveRequirementCheck( - check=check, - identifier=f"{self.identifier}_{relative_identifier}", - profile=self, - relation=RequirementCheckRelation.INHERITED, - ) + # Resolve through the nearest ancestor exposing this check, including + # its replacement identity. Each overlay then rebases that effective + # identity, while ordinary inheritance preserves it. + for inherited_profile in reversed(self.inherited_profiles): + if source_profile == inherited_profile or source_profile in inherited_profile.inherited_profiles: + inherited_identity = inherited_profile.effective_requirement_check(check) + is_overlay = inherited_profile.uri in self.is_rule_overlay_of + relative_identifier = inherited_identity.identifier.rsplit("_", maxsplit=1)[-1] + return EffectiveRequirementCheck( + check=check, + identifier=( + f"{self.identifier}_{relative_identifier}" if is_overlay else inherited_identity.identifier + ), + profile=self if is_overlay else inherited_identity.profile, + relation=RequirementCheckRelation.INHERITED, + ) return EffectiveRequirementCheck( check=check, @@ -589,16 +600,25 @@ def __get_nested_profiles__(cls, source: str) -> list[str]: @property def inherited_profiles(self) -> list[Profile]: inherited_profiles = self.is_transitive_profile_of - if not inherited_profiles or len(inherited_profiles) == 0: + if not inherited_profiles: inherited_profiles = Profile.__get_nested_profiles__(self.uri) profile_keys = self.__profiles_map.keys - return [ + profiles = [ profile for key in inherited_profiles if key in profile_keys for profile in [self.__profiles_map.get_by_key(key)] if isinstance(profile, Profile) ] + # Neither RDF values nor the ancestry walk guarantee dependency order. + # Reuse the stable topological sorter used for requirements and checks. + positions = {profile.uri: index for index, profile in enumerate(profiles)} + edges: dict[int, set[int]] = {} + for child_index, profile in enumerate(profiles): + for parent_uri in profile.is_profile_of: + if parent_uri in positions: + edges.setdefault(positions[parent_uri], set()).add(child_index) + return RequirementLoader._topological_order(profiles, edges, self.identifier, "profiles") def add_requirement(self, requirement: Requirement): self._requirements.append(requirement) @@ -752,13 +772,11 @@ def __load_profiles_paths__( if not profile_root_directory.is_dir(): raise InvalidProfilePath(str(profile_root_directory)) # if the path is a directory, get the profile directories - result.extend( - [ - (profile_root_directory, p.parent) - for p in profile_root_directory.rglob("*.*") - if p.name == PROFILE_SPECIFICATION_FILE - ] - ) + result.extend([ + (profile_root_directory, p.parent) + for p in profile_root_directory.rglob("*.*") + if p.name == PROFILE_SPECIFICATION_FILE + ]) # return the list of profile directories return result From 59d2b9bf837783c72584a728b51e7fc2e189f4d1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 5 Nov 2026 10:24:24 +0100 Subject: [PATCH 246/271] =?UTF-8?q?test(profiles):=20=F0=9F=A7=AA=20cover?= =?UTF-8?q?=20inherited=20profile=20identity=20and=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profiles/effective_checks/c/profile.ttl | 11 +++ tests/unit/requirements/test_profiles.py | 76 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/data/profiles/effective_checks/c/profile.ttl diff --git a/tests/data/profiles/effective_checks/c/profile.ttl b/tests/data/profiles/effective_checks/c/profile.ttl new file mode 100644 index 000000000..1ec3f2270 --- /dev/null +++ b/tests/data/profiles/effective_checks/c/profile.ttl @@ -0,0 +1,11 @@ +@prefix prof: . +@prefix rdfs: . + + + a prof:Profile ; + rdfs:label "Effective checks wrapper profile" ; + prof:isProfileOf ; + prof:isTransitiveProfileOf + , + ; + prof:hasToken "effective-c" . diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index f2e856c6f..f2260d609 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -15,6 +15,7 @@ import logging from dataclasses import replace from pathlib import Path +from shutil import copytree from typing import Any, cast import pytest @@ -529,6 +530,81 @@ def test_effective_checks_expose_overlay_provenance(): } +def test_nested_overlay_preserves_effective_identity_and_orders_ancestors(): + """A wrapper profile keeps its inherited overlay's identity and execution order.""" + profiles = Profile.load_profiles("tests/data/profiles/effective_checks", severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "effective-c") + + assert [item.identifier for item in profile.inherited_profiles] == ["effective-a", "effective-b"] + + inherited = next( + item for item in profile.get_effective_requirement_checks() if item.check.name == "Inherited check" + ) + assert inherited.identifier == "effective-b_1.1" + assert inherited.source_identifier == "effective-a_1.1" + assert inherited.profile.identifier == "effective-b" + assert inherited.source_profile.identifier == "effective-a" + + effective_checks = profile.get_effective_requirement_checks() + identifiers = [item.identifier for item in effective_checks] + assert len(identifiers) == len(set(identifiers)) + replacement = next(item for item in effective_checks if item.check.name == "Shared check") + assert replacement.identifier == "effective-b_1.2" + assert replacement.source_identifier == "effective-b_1.1" + assert replacement.relation == RequirementCheckRelation.INHERITED + + +@pytest.mark.parametrize("local_replacement", [False, True]) +def test_chained_overlays_rebase_effective_identifiers(tmp_path, local_replacement): + """A second overlay must rebase both inherited and replacement identities.""" + profiles_path = tmp_path / "profiles" + copytree("tests/data/profiles/effective_checks", profiles_path) + specification = profiles_path / "c/profile.ttl" + graph = Graph().parse(specification, format="turtle") + profiles_ns = Namespace("https://w3id.org/rocrate-validator/tests/effective-checks/") + validator_ns = Namespace("https://github.com/crs4/rocrate-validator/") + graph.add((profiles_ns.c, validator_ns.isRuleOverlayOf, profiles_ns.b)) + graph.serialize(specification, format="turtle") + if local_replacement: + (profiles_path / "c/checks.py").write_text( + (profiles_path / "b/checks.py").read_text(encoding="utf-8"), encoding="utf-8" + ) + + profiles = Profile.load_profiles(profiles_path, severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "effective-c") + by_name = {item.check.name: item for item in profile.get_effective_requirement_checks()} + assert by_name["Inherited check"].identifier == "effective-c_1.1" + assert by_name["Inherited check"].source_identifier == "effective-a_1.1" + assert by_name["Shared check"].identifier == "effective-c_1.2" + assert by_name["Shared check"].source_identifier == ("effective-c_1.1" if local_replacement else "effective-b_1.1") + assert all(item.profile == profile for item in by_name.values()) + + +@pytest.mark.parametrize("explicit_ancestry", [False, True]) +def test_inherited_profiles_order_shared_ancestors(tmp_path, explicit_ancestry): + """Shared ancestors must precede descendants, even with redundant parent edges.""" + relations = { + "b": "", + "a": "prof:isProfileOf ;", + "c": "prof:isProfileOf , ;", + "d": "prof:isProfileOf ;", + } + if explicit_ancestry: + relations["d"] += " prof:isTransitiveProfileOf , , ;" + for name, predicates in relations.items(): + directory = tmp_path / name + directory.mkdir() + (directory / "profile.ttl").write_text( + "@prefix prof: .\n" + f' a prof:Profile ; {predicates} prof:hasToken "{name}" .', + encoding="utf-8", + ) + + profiles = Profile.load_profiles(tmp_path) + profile = next(item for item in profiles if item.identifier == "d") + assert [item.identifier for item in profile.inherited_profiles] == ["b", "a", "c"] + + def test_effective_checks_retain_ordinary_inherited_identity(): profiles = Profile.load_profiles(Path("rocrate_validator/profiles"), severity=Severity.OPTIONAL) profile = next(item for item in profiles if item.identifier == "process-run-crate-0.5") From f2c50b6086d6287a90e5c89f8a2a407a06c026f6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Nov 2026 12:59:33 +0100 Subject: [PATCH 247/271] =?UTF-8?q?fix(rocrate):=20=F0=9F=8C=90=20read=20r?= =?UTF-8?q?emote=20ZIPs=20using=20absolute=20=20HTTP=20ranges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/rocrate/plain.py | 57 ++++++----------------- rocrate_validator/utils/http.py | 74 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 43 deletions(-) diff --git a/rocrate_validator/rocrate/plain.py b/rocrate_validator/rocrate/plain.py index 7e06f6daf..53c28a6ef 100644 --- a/rocrate_validator/rocrate/plain.py +++ b/rocrate_validator/rocrate/plain.py @@ -14,15 +14,13 @@ from __future__ import annotations -import io -import struct import zipfile from pathlib import Path from typing import TYPE_CHECKING from rocrate_validator.errors import ROCrateInvalidURIError from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HTTPRangeReader, HttpRequester from .base import ROCrate from .metadata import ROCrateMetadata @@ -362,51 +360,24 @@ def __init_zip_reference__(self): if not self.uri.is_available(): raise ROCrateInvalidURIError(uri=url, message="URI is not available") - # Step 1: Fetch the last 22 bytes to find the EOCD record - eocd_data = self.__fetch_range__(url, -22, "") - - # Step 2: Find the EOCD record - eocd_offset = self.__find_eocd__(eocd_data) - - # Step 3: Fetch the EOCD and parse it - eocd_full_data = self.__fetch_range__(url, -22 - eocd_offset, -1) - central_directory_offset, central_directory_size = self.__parse_eocd__(eocd_full_data) - - # Step 4: Fetch the central directory - central_directory_data = self.__fetch_range__( - url, central_directory_offset, central_directory_offset + central_directory_size - 1 - ) - # Step 5: Parse the central directory and return the zip file - self._zipref = zipfile.ZipFile(io.BytesIO(central_directory_data)) # pylint: disable=consider-using-with + # Let zipfile handle directory offsets, comments and entry reads while + # translating its seeks into absolute HTTP ranges (never suffix ranges). + try: + self._zipref = zipfile.ZipFile( # pylint: disable=consider-using-with + HTTPRangeReader(url, self.size) + ) + except zipfile.BadZipFile as exc: + # zipfile masks OSError (including requests' HTTPError) while reading + # the directory. Preserve transport errors instead of reporting a bad ZIP. + if isinstance(exc.__context__, OSError): + raise exc.__context__ from None + raise @property def size(self) -> int: - response = HttpRequester().head(str(self.uri)) + response = HttpRequester().head(str(self.uri), allow_redirects=True, headers={"Accept-Encoding": "identity"}) response.raise_for_status() # Check if the request was successful file_size = response.headers.get("Content-Length") if file_size is not None: return int(file_size) raise ValueError("Could not determine the file size from the headers") - - @staticmethod - def __fetch_range__(uri: str, start, end): - headers = {"Range": f"bytes={start}-{end}"} - response = HttpRequester().get(uri, headers=headers) - response.raise_for_status() - return response.content - - @staticmethod - def __find_eocd__(data): - eocd_signature = b"PK\x05\x06" - eocd_offset = data.rfind(eocd_signature) - if eocd_offset == -1: - raise ValueError("EOCD not found") - return eocd_offset - - @staticmethod - def __parse_eocd__(data): - eocd_size = struct.calcsize("<4s4H2LH") - eocd = struct.unpack("<4s4H2LH", data[-eocd_size:]) - central_directory_size = eocd[5] - central_directory_offset = eocd[6] - return central_directory_offset, central_directory_size diff --git a/rocrate_validator/utils/http.py b/rocrate_validator/utils/http.py index 7b3e44fd6..ecf0ebe9d 100644 --- a/rocrate_validator/utils/http.py +++ b/rocrate_validator/utils/http.py @@ -16,9 +16,11 @@ import atexit import contextlib +import io import random import string import threading +from http import HTTPStatus from pathlib import Path from typing import TYPE_CHECKING, Any @@ -433,6 +435,78 @@ def reset(cls) -> None: cls._instance = None +class HTTPRangeReader(io.RawIOBase): + """ + Seekable HTTP input using bounded ranges and a caller-supplied resource size. + + Uses HttpRequester for transport and retains the full response when a server + ignores Range, so subsequent reads can be served without further downloads. + """ + + def __init__(self, url: str, size: int): + super().__init__() + self._url = url + self._size = size + self._position = 0 + self._full_content: bytes | None = None + + def readable(self) -> bool: + return True + + def seekable(self) -> bool: + return True + + def tell(self) -> int: + return self._position + + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + if whence == io.SEEK_CUR: + offset += self._position + elif whence == io.SEEK_END: + offset += self._size + elif whence != io.SEEK_SET: + raise ValueError(f"Invalid whence: {whence}") + if offset < 0: + raise ValueError("Negative seek position") + self._position = offset + return offset + + def read(self, size: int = -1) -> bytes: + start = self._position + stop = self._size if size < 0 else min(start + size, self._size) + if start >= stop: + return b"" + + if self._full_content is None: + response = HttpRequester().get( + self._url, + headers={"Range": f"bytes={start}-{stop - 1}", "Accept-Encoding": "identity"}, + ) + response.raise_for_status() + if response.status_code == HTTPStatus.OK and len(response.content) == self._size: + # Some servers ignore Range. Retain the complete response so later + # seeks do not download the archive again, and slice at absolute offsets. + logger.warning( + "Server ignored HTTP Range for %s; received the entire resource (%d bytes), " + "which will be retained in memory.", + self._url, + self._size, + ) + self._full_content = response.content + elif ( + response.status_code == HTTPStatus.PARTIAL_CONTENT + and response.headers.get("Content-Range") == f"bytes {start}-{stop - 1}/{self._size}" + and len(response.content) == stop - start + ): + self._position = stop + return response.content + else: + raise OSError(f"Invalid HTTP range response for {self._url}") + + self._position = stop + return self._full_content[start:stop] + + class _OfflineFallbackSession: """ Minimal session used when offline mode is requested but no HTTP cache From ac87badc71bd3baa4fd9dd66b8164c3a8f5aa7df Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Nov 2026 13:01:22 +0100 Subject: [PATCH 248/271] =?UTF-8?q?test(rocrate):=20=F0=9F=A7=AA=20cover?= =?UTF-8?q?=20remote=20ZIP=20ranges=20and=20full-download=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_remote_zip_ranges.py | 160 +++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/unit/test_remote_zip_ranges.py diff --git a/tests/unit/test_remote_zip_ranges.py b/tests/unit/test_remote_zip_ranges.py new file mode 100644 index 000000000..fc466f6ab --- /dev/null +++ b/tests/unit/test_remote_zip_ranges.py @@ -0,0 +1,160 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import re +import zipfile +from pathlib import Path + +import pytest +from requests import HTTPError, Response + +from rocrate_validator.rocrate import BagitROCrate, ROCrate, ROCrateBagitRemoteZip, ROCrateRemoteZip +from rocrate_validator.utils.http import HttpRequester + +URL = "https://example.org/bagit.zip" +METADATA = b'{"@context": "https://w3id.org/ro/crate/1.1/context", "@graph": []}' + + +@pytest.fixture(name="remote_zip") +def remote_zip_fixture(monkeypatch): + """Model servers rejecting suffix ranges, without making any network requests.""" + + def install( + *, + comment=b"", + ignore_range=False, + status=206, + truncate=False, + bad_content_range=False, + compression=zipfile.ZIP_STORED, + bagit=True, + ): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", compression=compression) as archive: + if bagit: + archive.writestr("bagit.txt", "BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n") + archive.writestr("data/ro-crate-metadata.json", METADATA) + archive.writestr("data/large.bin", b"x" * 200_000) + archive.comment = comment + payload = buffer.getvalue() + ranges = [] + + def response(code, content=b"", **headers): + result = Response() + result.status_code = code + result.url = URL + result._content = content # pylint: disable=protected-access + result.headers.update(headers) + return result + + def head(_self, url, **_kwargs): + assert url == URL + if not _kwargs.get("allow_redirects"): + return response(302, **{"Content-Length": "0", "Location": URL + "?redirected"}) + return response(200, **{"Content-Length": str(len(payload))}) + + def get(_self, url, *, headers, **_kwargs): + assert url == URL + match = re.fullmatch(r"bytes=(\d+)-(\d+)", headers.get("Range", "")) + if not match: + return response(501) + start, end = map(int, match.groups()) + assert 0 <= start <= end < len(payload) + ranges.append((start, end)) + if ignore_range: + return response(200, payload[:-1] if truncate else payload) + content = payload[start : end + 1] + return response( + status, + content[:-1] if truncate else content, + **{"Content-Range": "bytes 0-0/1" if bad_content_range else f"bytes {start}-{end}/{len(payload)}"}, + ) + + monkeypatch.setattr(HttpRequester, "head", head, raising=False) + monkeypatch.setattr(HttpRequester, "get", get, raising=False) + return payload, ranges + + return install + + +@pytest.mark.parametrize("ignore_range", [False, True]) +@pytest.mark.parametrize("comment", [b"", b"Archive comment" * 100]) +def test_remote_zip_reads_entries_with_absolute_ranges(remote_zip, ignore_range, comment, caplog): + payload, ranges = remote_zip(comment=comment, ignore_range=ignore_range) + with caplog.at_level("WARNING", logger="rocrate_validator.utils.http"): + crate = ROCrateRemoteZip(URL) + + assert crate.has_file(Path("bagit.txt")) + assert crate.get_file_content(Path("data/ro-crate-metadata.json")) == METADATA + assert crate.get_file_content(Path("data/ro-crate-metadata.json"), binary_mode=False) == METADATA.decode() + assert crate.get_file_size(Path("data/large.bin")) == 200_000 + assert ranges + # Inspecting the directory and reading metadata must not download the large payload. + assert sum(end - start + 1 for start, end in ranges) < len(payload) + if ignore_range: + assert len(ranges) == 1, "Reuse the full response when the server ignores Range" + assert len(caplog.records) == 1 + assert "Server ignored HTTP Range" in caplog.text + assert URL in caplog.text + assert f"{len(payload)} bytes" in caplog.text + assert "retained in memory" in caplog.text + else: + assert not caplog.records + + +@pytest.mark.parametrize("ignore_range", [False, True]) +@pytest.mark.parametrize("bagit", [False, True]) +def test_remote_zip_bagit_detection_and_factory(remote_zip, ignore_range, bagit): + remote_zip(ignore_range=ignore_range, bagit=bagit) + + assert BagitROCrate.is_bagit_wrapping_crate(URL) is bagit + crate = ROCrate.new_instance(URL) + assert isinstance(crate, ROCrateRemoteZip) + assert isinstance(crate, ROCrateBagitRemoteZip) is bagit + path = "ro-crate-metadata.json" if bagit else "data/ro-crate-metadata.json" + assert crate.get_file_content(Path(path)) == METADATA + + +@pytest.mark.parametrize("status", [403, 404, 503]) +def test_remote_zip_propagates_range_http_errors(remote_zip, status): + remote_zip(status=status) + + with pytest.raises(HTTPError, match=str(status)): + ROCrateRemoteZip(URL) + + +@pytest.mark.parametrize("ignore_range", [False, True]) +def test_remote_zip_rejects_truncated_range_response(remote_zip, ignore_range): + remote_zip(truncate=True, ignore_range=ignore_range) + + with pytest.raises(OSError, match="range response"): + ROCrateRemoteZip(URL) + + +def test_remote_zip_rejects_wrong_content_range(remote_zip): + remote_zip(bad_content_range=True) + + with pytest.raises(OSError, match="range response"): + ROCrateRemoteZip(URL) + + +def test_remote_zip_reads_compressed_entries(remote_zip): + remote_zip(compression=zipfile.ZIP_DEFLATED) + crate = ROCrateRemoteZip(URL) + + assert crate.get_file_content(Path("data/ro-crate-metadata.json")) == METADATA + assert crate.get_file_content(Path("data/large.bin")) == b"x" * 200_000 + with pytest.raises(FileNotFoundError): + crate.get_file_content(Path("missing.txt")) From c285191cd8c53574f208d115c06274825d3b51eb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Nov 2026 19:47:03 +0100 Subject: [PATCH 249/271] =?UTF-8?q?fix(profiles):=20=F0=9F=A7=A9=20declare?= =?UTF-8?q?=20RootDataEntity=20equivalence=20between=20RO-Crate=201.3=20an?= =?UTF-8?q?d=201.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/profiles/ro-crate/1.3/ontology.ttl | 1 + 1 file changed, 1 insertion(+) diff --git a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl index 9de529152..ce194f0d7 100644 --- a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl @@ -40,6 +40,7 @@ ro-crate:ROCrateMetadataFileDescriptor # Declare the RootDataEntity class ro-crate:RootDataEntity rdf:type owl:Class ; + owl:equivalentClass ro-crate-1.2:RootDataEntity ; rdfs:subClassOf schema:Dataset ; rdfs:label "RootDataEntity"@en . From 590d064da287e450b41d58e4476e176099b34cff Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Nov 2026 19:47:38 +0100 Subject: [PATCH 250/271] =?UTF-8?q?test(profiles):=20=F0=9F=A7=AA=20cover?= =?UTF-8?q?=20RootDataEntity=20equivalence=20across=20RO-Crate=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/test_profile_namespaces.py | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/unit/requirements/test_profile_namespaces.py b/tests/unit/requirements/test_profile_namespaces.py index 94d832396..9e3d63289 100644 --- a/tests/unit/requirements/test_profile_namespaces.py +++ b/tests/unit/requirements/test_profile_namespaces.py @@ -114,19 +114,52 @@ def test_sparql_prefixes_use_the_profile_ontology_namespace(profile_path: Path) ) -def test_ro_crate_1_3_maps_the_inherited_descriptor_marker() -> None: +@pytest.mark.parametrize("marker", ["ROCrateMetadataFileDescriptor", "RootDataEntity"]) +def test_ro_crate_1_3_maps_the_inherited_marker(marker: str) -> None: """The native 1.3 target class must match the marker emitted by inherited 1.2 discovery.""" ontology = _parse_graph(PROFILES_PATH / "ro-crate/1.3/ontology.ttl") ro_crate_1_2 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/") ro_crate_1_3 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/") assert ( - ro_crate_1_3.ROCrateMetadataFileDescriptor, + ro_crate_1_3[marker], OWL.equivalentClass, - ro_crate_1_2.ROCrateMetadataFileDescriptor, + ro_crate_1_2[marker], ) in ontology +@pytest.mark.parametrize("source_version,target_version", [("1.2", "1.3"), ("1.3", "1.2")]) +@pytest.mark.parametrize("has_name", [False, True], ids=["missing-name", "with-name"]) +def test_root_data_entity_equivalence_applies_shacl_constraints( + source_version: str, target_version: str, has_name: bool +) -> None: + """Root constraints must apply across versions, rather than pass without focus nodes.""" + ontology = _parse_graph(PROFILES_PATH / "ro-crate/1.3/ontology.ttl") + source = Namespace(f"{PROFILE_NAMESPACE_PREFIX}ro-crate-{source_version}/") + target = Namespace(f"{PROFILE_NAMESPACE_PREFIX}ro-crate-{target_version}/") + + data = Graph() + data.add((EXAMPLE.root, RDF.type, source.RootDataEntity)) + if has_name: + data.add((EXAMPLE.root, SCHEMA.name, Literal("Example crate"))) + + shapes = Graph() + shapes.add((EXAMPLE.RootShape, RDF.type, SH.NodeShape)) + shapes.add((EXAMPLE.RootShape, SH.targetClass, target.RootDataEntity)) + shapes.add((EXAMPLE.RootShape, SH.property, EXAMPLE.RequiredName)) + shapes.add((EXAMPLE.RequiredName, SH.path, SCHEMA.name)) + shapes.add((EXAMPLE.RequiredName, SH.minCount, Literal(1))) + + conforms, report, _ = validate(data, shacl_graph=shapes, ont_graph=ontology, inference="owlrl") + + assert conforms == has_name + if not has_name: + results = list(report.subjects(RDF.type, SH.ValidationResult)) + assert len(results) == 1 + assert (results[0], SH.focusNode, EXAMPLE.root) in report + assert (results[0], SH.resultPath, SCHEMA.name) in report + + @pytest.mark.parametrize( "profile_path", _profile_paths_with("ontology.ttl", "must/2_root_data_entity_metadata.ttl"), From 319a2bdfc8419810fd2169c8e3e35662eb713263 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 4 Nov 2026 10:41:16 +0100 Subject: [PATCH 251/271] move wroc 1.0 profile to version-specific subdir --- .../profiles/workflow-ro-crate/{ => 1.0}/may/0_main-workflow.ttl | 0 .../profiles/workflow-ro-crate/{ => 1.0}/may/1_main_workflow.py | 0 .../profiles/workflow-ro-crate/{ => 1.0}/may/2_wrroc_crate.ttl | 0 .../profiles/workflow-ro-crate/{ => 1.0}/must/0_main-workflow.ttl | 0 .../profiles/workflow-ro-crate/{ => 1.0}/must/0_main_workflow.py | 0 .../workflow-ro-crate/{ => 1.0}/must/1_wroc_root_data_entity.ttl | 0 .../profiles/workflow-ro-crate/{ => 1.0}/profile.ttl | 0 .../profiles/workflow-ro-crate/{ => 1.0}/should/1_wroc_crate.ttl | 0 .../workflow-ro-crate/{ => 1.0}/should/2_main-workflow.ttl | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/may/0_main-workflow.ttl (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/may/1_main_workflow.py (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/may/2_wrroc_crate.ttl (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/must/0_main-workflow.ttl (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/must/0_main_workflow.py (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/must/1_wroc_root_data_entity.ttl (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/profile.ttl (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/should/1_wroc_crate.ttl (100%) rename rocrate_validator/profiles/workflow-ro-crate/{ => 1.0}/should/2_main-workflow.ttl (100%) diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/0_main-workflow.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/may/0_main-workflow.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/1_main_workflow.py similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py rename to rocrate_validator/profiles/workflow-ro-crate/1.0/may/1_main_workflow.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/2_wrroc_crate.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/may/2_wrroc_crate.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main-workflow.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main-workflow.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main_workflow.py similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py rename to rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main_workflow.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/1_wroc_root_data_entity.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/must/1_wroc_root_data_entity.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/profile.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/profile.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/profile.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/should/1_wroc_crate.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/should/1_wroc_crate.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/should/2_main-workflow.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/should/2_main-workflow.ttl From 7997715ebc65e4c84f3ce0737ab582cb8063c044 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 4 Nov 2026 15:25:29 +0100 Subject: [PATCH 252/271] wroc 1.1: first draft --- .../1.1/may/0_main-workflow.ttl | 70 +++++++++ .../1.1/may/1_main_workflow.py | 83 ++++++++++ .../1.1/may/2_wrroc_crate.ttl | 49 ++++++ .../1.1/must/0_main-workflow.ttl | 85 +++++++++++ .../1.1/must/0_main_workflow.py | 57 +++++++ .../1.1/must/1_wroc_root_data_entity.ttl | 39 +++++ .../workflow-ro-crate/1.1/profile.ttl | 81 ++++++++++ .../1.1/should/1_wroc_crate.ttl | 64 ++++++++ .../1.1/should/2_main-workflow.ttl | 35 +++++ .../crates/valid/workflow-roc-1.1/README.md | 3 + .../crates/valid/workflow-roc-1.1/blank.png | Bin 0 -> 334 bytes .../workflow-roc-1.1/ro-crate-metadata.json | 143 ++++++++++++++++++ .../workflow-roc-1.1/sort-and-change-case.cwl | 41 +++++ .../workflow-roc-1.1/sort-and-change-case.ga | 118 +++++++++++++++ 14 files changed, 868 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl create mode 100644 tests/data/crates/valid/workflow-roc-1.1/README.md create mode 100644 tests/data/crates/valid/workflow-roc-1.1/blank.png create mode 100644 tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl create mode 100644 tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl new file mode 100644 index 000000000..eacef57a3 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl @@ -0,0 +1,70 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix wroc: . + +workflow-ro-crate:MainWorkflowOptionalProperties a sh:NodeShape ; + sh:name "Main Workflow optional properties" ; + sh:description """Main Workflow properties defined as MAY"""; + sh:targetClass workflow-ro-crate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow image" ; + sh:description "The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'" ; + sh:path schema:image ; + sh:class schema:MediaObject, schema:ImageObject ; + sh:minCount 1 ; + sh:message "The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'" ; + # sh:severity sh:Info ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow subjectOf" ; + sh:description "The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'" ; + sh:path schema:subjectOf ; + sh:node workflow-ro-crate:CWLDescriptionProperties ; + sh:minCount 1 ; + sh:message "The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'" ; + # sh:severity sh:Info ; + ] . + +workflow-ro-crate:CWLDescriptionProperties a sh:NodeShape ; + sh:name "CWL Description properties" ; + sh:description "Main Workflow CWL Description properties" ; + sh:property [ + a sh:PropertyShape ; + sh:name "CWL Description type" ; + sh:description "The CWL Description type must be File, SoftwareSourceCode, HowTo" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject, schema:SoftwareSourceCode, schema:HowTo ; + sh:message "The CWL Description type must be File, SoftwareSourceCode, HowTo" ; + # sh:severity sh:Info ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "CWL Description language" ; + sh:description "The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl" ; + sh:path schema:programmingLanguage ; + sh:hasValue wroc:cwl ; + sh:class schema:ComputerLanguage ; + sh:message "The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl" ; + # sh:severity sh:Info ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py new file mode 100644 index 000000000..6766d188f --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py @@ -0,0 +1,83 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Workflow-related files existence") +class WorkflowFilesExistence(PyFunctionCheck): + """Checks for workflow-related crate files existence.""" + + @check(name="Workflow diagram existence") + def check_workflow_diagram(self, context: ValidationContext) -> CheckResultValue: + """Check if the crate contains the workflow diagram.""" + if context.settings.metadata_only: + logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + image = main_workflow.get_property("image") + diagram_relpath = image.id if image else None + if not diagram_relpath: + context.result.add_issue("main workflow does not have an 'image' property", self) + return False + if not image.is_available(): + context.result.add_issue(f"Workflow diagram '{image.id}' not found in crate", self) + return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow diagram check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow diagram: {e}", self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error checking main workflow image existence") + return False + + @check(name="Workflow description existence") + def check_workflow_description(self, context: ValidationContext) -> CheckResultValue: + """Check if the crate contains the workflow CWL description.""" + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + main_workflow_subject = main_workflow.get_property("subjectOf") + description_relpath = main_workflow_subject.id if main_workflow_subject else None + if not description_relpath: + context.result.add_issue("main workflow does not have a 'subjectOf' property", self) + return False + if not context.settings.metadata_only and not main_workflow_subject.is_available(): + context.result.add_issue( + f"Workflow CWL description {main_workflow_subject.id} not found in crate", self + ) + return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow description check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow description: {e}", self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error checking workflow description existence") + return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl new file mode 100644 index 000000000..3bd2e2476 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . + +workflow-ro-crate:FindTestDir a sh:NodeShape ; + sh:name "test directory" ; + sh:description "Dataset data entity to hold tests" ; + sh:targetNode ro:test\/ ; + sh:property [ + a sh:PropertyShape ; + sh:name "test/ dir type" ; + sh:description "The test/ dir should be a Dataset" ; + sh:path rdf:type ; + sh:hasValue schema:Dataset ; + sh:minCount 1 ; + sh:message "The test/ dir should be a Dataset" ; + ] . + +workflow-ro-crate:FindExamplesDir a sh:NodeShape ; + sh:name "examples directory" ; + sh:description "Dataset data entity to hold examples" ; + sh:targetNode ro:examples\/ ; + sh:property [ + a sh:PropertyShape ; + sh:name "examples/ dir type" ; + sh:description "The examples/ dir should be a Dataset" ; + sh:path rdf:type ; + sh:hasValue schema:Dataset ; + sh:minCount 1 ; + sh:message "The examples/ dir should be a Dataset" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl new file mode 100644 index 000000000..ae418200d --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl @@ -0,0 +1,85 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix bioschemas: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix validator: . + +workflow-ro-crate:MainEntityPropertyMustExist a sh:NodeShape ; + sh:name "Main Workflow entity existence" ; + sh:description "The Main Workflow must be specified through a `mainEntity` property in the root data entity" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:path schema:mainEntity ; + sh:minCount 1 ; + sh:description "Check if the Main Workflow is specified through a `mainEntity` property in the root data entity" ; + sh:message "The Main Workflow must be specified through a `mainEntity` property in the root data entity" ; + ] . + +workflow-ro-crate:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Main Workflow" ; + sh:description "Identify the Main Workflow" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root schema:mainEntity ?this . + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } + """ + ] ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object workflow-ro-crate:MainWorkflow ; + ] . + +workflow-ro-crate:MainWorkflowRequiredProperties a sh:NodeShape ; + sh:name "Main Workflow definition" ; + sh:description """Main Workflow properties defined as MUST"""; + sh:targetClass workflow-ro-crate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow type" ; + sh:description "The Main Workflow types must include File, SoftwareSourceCode and ComputationalWorkflow" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject , + schema:SoftwareSourceCode , + bioschemas:ComputationalWorkflow ; + sh:minCount 1 ; + sh:message "The Main Workflow types must include File, SoftwareSourceCode and ComputationalWorkflow" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow language" ; + sh:description "The Main Workflow must refer to its language via programmingLanguage" ; + sh:path schema:programmingLanguage ; + sh:class schema:ComputerLanguage ; + sh:minCount 1 ; + sh:message "The Main Workflow must refer to its language via programmingLanguage" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py new file mode 100644 index 000000000..86501fbb4 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Main Workflow file existence") +class MainWorkflowFileExistence(PyFunctionCheck): + """Checks for main workflow file existence.""" + + @check(name="Main Workflow file must exist") + def check_workflow(self, context: ValidationContext) -> CheckResultValue: + """Check if the crate contains the main workflow file.""" + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + if not main_workflow: + context.result.add_issue("main workflow does not exist in metadata file", self) + return False + if not context.settings.metadata_only and not main_workflow.is_available(): + context.result.add_issue(f"Main Workflow {main_workflow.id} not found in crate", self) + return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping main workflow check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (TypeError, ValueError) as error: + if str(error) == "no main workflow in metadata file descriptor": + message = ( + "Unable to check the existence of the main workflow file " + "because the metadata file descriptor doesn't contain a `mainEntity`" + ) + else: + message = f"Unable to check the existence of the main workflow file: {error}" + context.result.add_issue(message, self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unable to check main workflow file existence") + return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl new file mode 100644 index 000000000..5615d5c10 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix xsd: . +@prefix validator: . + +workflow-ro-crate:WROCRootDataEntityRequiredProperties a sh:NodeShape ; + sh:name "WROC Root Data Entity Required Properties" ; + sh:description """Root Data Entity properties defined as MUST""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Crate license" ; + sh:description "The Crate must specify a license" ; + sh:path schema:license ; + sh:or ( + [ sh:nodeKind sh:Literal; sh:datatype xsd:string ; ] + [ sh:nodeKind sh:IRI ; ] + ) ; + sh:minCount 1 ; + sh:message "The Crate (Root Data Entity) must specify a license, which should be a URL but can also be a string" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl new file mode 100644 index 000000000..ec79d06e6 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl @@ -0,0 +1,81 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Workflow RO-Crate Metadata Specification 1.1" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Workflow RO-Crate Metadata Specification."""@en ; + + # URI of the publisher of the Workflow RO-Crate Metadata Specification + dct:publisher ; + + # This profile is an extension of the RO-Crate Metadata Specification 1.3 profile + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.3 profile + prof:isTransitiveProfileOf ; + + # this profile has a JSON-LD context resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in JSON-LD format + dct:format ; + + # it conforms to JSON-LD, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Vocabulary" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Vocabulary ; + + # this profile resource's actual file + prof:hasArtifact ; + ] ; + + # this profile has a human-readable documentation resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in HTML format + dct:format ; + + # it conforms to HTML, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Specification" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Specification ; + + # this profile resource's actual file + prof:hasArtifact ; + + # this profile is inherited from the RO-Crate Metadata Specification 1.3 + prof:isInheritedFrom ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "workflow-ro-crate" ; +. diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl new file mode 100644 index 000000000..5b404a6d0 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl @@ -0,0 +1,64 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix dct: . +@prefix bioschemas: . +@prefix schema: . +@prefix sh: . + + +workflow-ro-crate:WROCRootDataEntityMetadata a sh:NodeShape ; + sh:name "Root Data Entity Metadata" ; + sh:description "Properties of the Root Data Entity" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity conformsTo" ; + sh:description "The Root Data Entity conformsTo SHOULD be an array that contains at least https://w3id.org/workflowhub/workflow-ro-crate/1.1" ; + sh:path dct:conformsTo ; + sh:class schema:CreativeWork; + # At least one value of conformsTo must match the URI + sh:qualifiedValueShape [ + sh:hasValue "https://w3id.org/workflowhub/workflow-ro-crate/1.1" ; + ] ; + sh:qualifiedMinCount 1 ; + sh:minCount 1 ; + sh:message "The Root Data Entity conformsTo SHOULD be an array that contains at least https://w3id.org/workflowhub/workflow-ro-crate/1.1" ; + ] . + +workflow-ro-crate:FindReadme a sh:NodeShape ; + sh:name "README.md properties" ; + sh:description "README file for the Workflow RO-Crate" ; + sh:targetNode ro:README.md ; + sh:property [ + a sh:PropertyShape ; + sh:name "README.md about" ; + sh:description "The README.md SHOULD be about the crate" ; + sh:path schema:about ; + sh:class ro-crate:RootDataEntity ; + sh:minCount 1 ; + sh:message "The README.md SHOULD be about the crate" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "README.md encodingFormat" ; + sh:description "The README.md SHOULD have text/markdown as its encodingFormat" ; + sh:path schema:encodingFormat ; + sh:hasValue "text/markdown" ; + sh:minCount 1 ; + sh:message "The README.md SHOULD have text/markdown as its encodingFormat" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl new file mode 100644 index 000000000..b0b74bb1f --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl @@ -0,0 +1,35 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix dct: . +@prefix sh: . +@prefix bioschemas: . + +workflow-ro-crate:MainWorkflowRecommendedProperties a sh:NodeShape ; + sh:name "Main Workflow recommended properties" ; + sh:description """Main Workflow properties defined as SHOULD"""; + sh:targetClass workflow-ro-crate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow Bioschemas compliance" ; + sh:description "The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later" ; + sh:path dct:conformsTo ; + sh:pattern "https://bioschemas.org/profiles/ComputationalWorkflow/[1-9].*" ; + sh:minCount 1 ; + sh:message "The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later" ; + sh:severity sh:Warning ; + ] . diff --git a/tests/data/crates/valid/workflow-roc-1.1/README.md b/tests/data/crates/valid/workflow-roc-1.1/README.md new file mode 100644 index 000000000..33fbaf728 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/README.md @@ -0,0 +1,3 @@ +# Example of a valid Workflow RO-Crate + +The Galaxy workflow has been copied from the LifeMonitor repository: [sort-and-change-case.ga](https://github.com/crs4/life_monitor/blob/b6206ca348701817756e133e72174826a5c3df4a/interaction_experiments/workflow_examples/galaxy/sort-and-change-case/sort-and-change-case.ga). The abstract CWL version of the workflow, `sort-and-change-case.cwl` has been generated by converting the Galaxy one with [ro-crate-py](https://github.com/ResearchObject/ro-crate-py), which, in turn, uses [galaxy2cwl](https://github.com/workflowhub-eu/galaxy2cwl). diff --git a/tests/data/crates/valid/workflow-roc-1.1/blank.png b/tests/data/crates/valid/workflow-roc-1.1/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..053a72f9e8baf61924f44cb84de8cbc4f0fefd3d GIT binary patch literal 334 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKLj6eZ~6-T%effP%+qpu?a!^VE@KZ&eBzCyA` zkS_y6l^O#>Lkk1LFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j&mC3(BMFfiWj5?%u2 zv6p!Iy0X7u5#-VqzOsLg2v8`@)5S5Q;?~>KhP(_6Jj@GzncvO2vHO63m6&Uw2$|?- z9GlIJYbospyVn72P%UwdC`m~yNwrEYN(E93Mg~SEx&|h?hUOuL2397UftDnm{r-UW|cVkx6 literal 0 HcmV?d00001 diff --git a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json new file mode 100644 index 000000000..e3883759e --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json @@ -0,0 +1,143 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": "CreativeWork", + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + } + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl new file mode 100644 index 000000000..662ae8628 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl @@ -0,0 +1,41 @@ +class: Workflow +cwlVersion: v1.2.0-dev2 +doc: 'Abstract CWL Automatically generated from the Galaxy workflow file: sort-and-change-case' +inputs: + 0_Input Dataset: + format: data + type: File +outputs: {} +steps: + 1_Sort: + in: + input: 0_Input Dataset + out: + - out_file1 + run: + class: Operation + id: sort1 + inputs: + input: + format: Any + type: File + outputs: + out_file1: + doc: input + type: File + 2_Change Case: + in: + input: 1_Sort/out_file1 + out: + - out_file1 + run: + class: Operation + id: ChangeCase + inputs: + input: + format: Any + type: File + outputs: + out_file1: + doc: tabular + type: File diff --git a/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga new file mode 100644 index 000000000..5a1999692 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga @@ -0,0 +1,118 @@ +{ + "uuid": "e2a8566c-c025-4181-9e90-7ed29d4e4df1", + "tags": [], + "format-version": "0.1", + "name": "sort-and-change-case", + "version": 0, + "steps": { + "0": { + "tool_id": null, + "tool_version": null, + "outputs": [], + "workflow_outputs": [], + "input_connections": {}, + "tool_state": "{}", + "id": 0, + "uuid": "5a36fad2-66c7-4b9e-8759-0fbcae9b8541", + "errors": null, + "name": "Input dataset", + "label": "bed_input", + "inputs": [], + "position": { + "top": 200, + "left": 200 + }, + "annotation": "", + "content_id": null, + "type": "data_input" + }, + "1": { + "tool_id": "sort1", + "tool_version": "1.1.0", + "outputs": [ + { + "type": "input", + "name": "out_file1" + } + ], + "workflow_outputs": [ + { + "output_name": "out_file1", + "uuid": "8237f71a-bc2a-494e-a63c-09c1e65ef7c8", + "label": "sorted_bed" + } + ], + "input_connections": { + "input": { + "output_name": "output", + "id": 0 + } + }, + "tool_state": "{\"__page__\": null, \"style\": \"\\\"alpha\\\"\", \"column\": \"\\\"1\\\"\", \"__rerun_remap_job_id__\": null, \"column_set\": \"[]\", \"input\": \"{\\\"__class__\\\": \\\"RuntimeValue\\\"}\", \"header_lines\": \"\\\"0\\\"\", \"order\": \"\\\"ASC\\\"\"}", + "id": 1, + "uuid": "0b6b3cda-c75f-452b-85b1-8ae4f3302ba4", + "errors": null, + "name": "Sort", + "post_job_actions": {}, + "label": "sort", + "inputs": [ + { + "name": "input", + "description": "runtime parameter for tool Sort" + } + ], + "position": { + "top": 200, + "left": 420 + }, + "annotation": "", + "content_id": "sort1", + "type": "tool" + }, + "2": { + "tool_id": "ChangeCase", + "tool_version": "1.0.0", + "outputs": [ + { + "type": "tabular", + "name": "out_file1" + } + ], + "workflow_outputs": [ + { + "output_name": "out_file1", + "uuid": "c31cd733-dab6-4d50-9fec-b644d162397b", + "label": "uppercase_bed" + } + ], + "input_connections": { + "input": { + "output_name": "out_file1", + "id": 1 + } + }, + "tool_state": "{\"__page__\": null, \"casing\": \"\\\"up\\\"\", \"__rerun_remap_job_id__\": null, \"cols\": \"\\\"c1\\\"\", \"delimiter\": \"\\\"TAB\\\"\", \"input\": \"{\\\"__class__\\\": \\\"RuntimeValue\\\"}\"}", + "id": 2, + "uuid": "9698bcde-0729-48fe-b88d-ccfb6f6153b4", + "errors": null, + "name": "Change Case", + "post_job_actions": {}, + "label": "change_case", + "inputs": [ + { + "name": "input", + "description": "runtime parameter for tool Change Case" + } + ], + "position": { + "top": 200, + "left": 640 + }, + "annotation": "", + "content_id": "ChangeCase", + "type": "tool" + } + }, + "annotation": "", + "a_galaxy_workflow": "true" +} From 1485d83539d88b81f051def49837739367966247 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 4 Nov 2026 16:27:46 +0100 Subject: [PATCH 253/271] wroc 1.1: fix prof:isTransitiveProfileOf --- rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl index ec79d06e6..5626c8357 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl @@ -35,7 +35,9 @@ prof:isProfileOf ; # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.3 profile - prof:isTransitiveProfileOf ; + prof:isTransitiveProfileOf + , + ; # this profile has a JSON-LD context resource prof:hasResource [ From 6a5dacdf05c17cd971de15d45a83d7bb0a44ffcb Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 5 Nov 2026 09:34:34 +0100 Subject: [PATCH 254/271] workflow-ro-crate tests: set profile to 1.0 --- .../workflow-ro-crate/test_main_workflow.py | 28 +++++++++---------- .../workflow-ro-crate/test_valid_wroc.py | 4 +-- .../workflow-ro-crate/test_wroc_crate.py | 4 +-- .../workflow-ro-crate/test_wroc_descriptor.py | 2 +- .../workflow-ro-crate/test_wroc_readme.py | 4 +-- .../test_wroc_root_metadata.py | 4 +-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py index 857aaf628..91c8bb0f3 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -35,7 +35,7 @@ def test_main_workflow_bad_type(): False, ["Main Workflow definition"], ["The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorkflow"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -50,7 +50,7 @@ def test_main_workflow_no_lang(): False, ["Main Workflow definition"], ["The Main Workflow must refer to its language via programmingLanguage"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -65,7 +65,7 @@ def test_main_workflow_no_image(): False, ["Main Workflow optional properties"], ["The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -80,7 +80,7 @@ def test_main_workflow_no_cwl_desc(): False, ["Main Workflow optional properties"], ["The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -95,7 +95,7 @@ def test_main_workflow_cwl_desc_bad_type(): False, ["Main Workflow optional properties"], ["The CWL Description type must be File, SoftwareSourceCode, HowTo"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -110,7 +110,7 @@ def test_main_workflow_cwl_desc_no_lang(): False, ["Main Workflow optional properties"], ["The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -124,7 +124,7 @@ def test_main_workflow_file_existence(): False, ["Main Workflow file existence"], ["Main Workflow", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -134,7 +134,7 @@ def test_main_workflow_singleton_array(): ValidROC().workflow_roc, Severity.REQUIRED, True, - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, ) @@ -148,7 +148,7 @@ def test_main_workflow_singleton_array_should_be_unpacked(): False, ["Entity properties compact representation"], ['property "mainEntity" SHOULD be represented as a single value'], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, ) @@ -162,7 +162,7 @@ def test_main_workflow_singleton_array_file_existence(): False, ["Main Workflow file existence"], ["Main Workflow", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, ) @@ -185,7 +185,7 @@ def test_main_workflow_invalid_main_entity_is_reported(main_entity): False, ["Main Workflow file existence"], ["mainEntity"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], rocrate_entity_patch={"./": {"mainEntity": main_entity}}, ) @@ -202,7 +202,7 @@ def test_workflow_diagram_file_existence(): False, ["Workflow-related files existence"], ["Workflow diagram", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -217,7 +217,7 @@ def test_workflow_description_file_existence(): False, ["Workflow-related files existence"], ["Workflow CWL description", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -232,5 +232,5 @@ def test_main_workflow_bad_conformsto(): False, ["Main Workflow recommended properties"], ["The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py index 256400230..8f9ddbce1 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -28,13 +28,13 @@ def test_valid_workflow_roc_required(): ValidROC().workflow_roc, Severity.REQUIRED, True, - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], ) do_entity_test( ValidROC().workflow_roc_string_license, Severity.REQUIRED, True, - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py index cfc43460f..994af7e74 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py @@ -31,7 +31,7 @@ def test_wroc_no_tests(): False, ["test directory"], ["The test/ dir should be a Dataset"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -45,5 +45,5 @@ def test_wroc_no_examples(): False, ["examples directory"], ["The examples/ dir should be a Dataset"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py index a3b633f17..74886a63e 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -36,5 +36,5 @@ def test_wroc_descriptor_bad_conforms_to(): "The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 " "and https://w3id.org/workflowhub/workflow-ro-crate/1.0" ], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py index e314643b7..9a690b67e 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py @@ -31,7 +31,7 @@ def test_wroc_readme_not_about_crate(): False, ["README.md properties"], ["The README.md SHOULD be about the crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -45,5 +45,5 @@ def test_wroc_readme_wrong_encoding_format(): False, ["README.md properties"], ["The README.md SHOULD have text/markdown as its encodingFormat"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py index 008db720f..1836f3f8c 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py @@ -31,7 +31,7 @@ def test_wroc_no_license(): False, ["WROC Root Data Entity Required Properties"], ["The Crate (Root Data Entity) must specify a license, which should be a URL but can also be a string"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -45,5 +45,5 @@ def test_wroc_no_mainentity(): False, ["Main Workflow entity existence"], ["The Main Workflow must be specified through a `mainEntity` property in the root data entity"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) From 335323299deaa18730ccdce1fe68df6dd4eb9516 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 5 Nov 2026 12:26:43 +0100 Subject: [PATCH 255/271] add a test for wroc 1.1 --- .../workflow-roc-1.1/ro-crate-metadata.json | 11 +++++-- .../test_valid_wroc_v1_1.py | 33 +++++++++++++++++++ tests/ro_crates.py | 4 +++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py diff --git a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json index e3883759e..8ce38ec98 100644 --- a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json @@ -39,7 +39,10 @@ }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", - "@type": "CreativeWork", + "@type": [ + "CreativeWork", + "Profile" + ], "name": "Workflow RO-Crate", "version": "1.1" }, @@ -90,7 +93,8 @@ "name": "Galaxy", "url": { "@id": "https://galaxyproject.org/" - } + }, + "version": "23.2" }, { "@id": "sort-and-change-case.cwl", @@ -114,7 +118,8 @@ "name": "Common Workflow Language", "url": { "@id": "https://www.commonwl.org/" - } + }, + "version": "v1.2.0-dev2" }, { "@id": "blank.png", diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py new file mode 100644 index 000000000..ef5262414 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER +from tests.ro_crates import ValidROC +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_valid_workflow_roc_required(): + """Test a valid Workflow RO-Crate.""" + do_entity_test( + ValidROC().workflow_roc_v1_1, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.1", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index facb1457d..527ae2d29 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -77,6 +77,10 @@ def bagit_remote_zip(self) -> str: def workflow_roc(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc" + @property + def workflow_roc_v1_1(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc-1.1" + @property def workflow_roc_string_license(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc-string-license" From 717634738f0c43bee4ee8a5ffee5b0771d4e5cc4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 5 Nov 2026 12:39:52 +0100 Subject: [PATCH 256/271] fix(tests): replace skip_checks with metadata_only --- .../profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py index ef5262414..5d2cff584 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py @@ -15,7 +15,6 @@ import logging from rocrate_validator.models import Severity -from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import ValidROC from tests.shared import do_entity_test @@ -23,11 +22,11 @@ def test_valid_workflow_roc_required(): - """Test a valid Workflow RO-Crate.""" + """Test valid Workflow RO-Crate metadata without checking the payload.""" do_entity_test( ValidROC().workflow_roc_v1_1, Severity.REQUIRED, True, profile_identifier="workflow-ro-crate-1.1", - skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + metadata_only=True, ) From 41e4942bd72834c461643c04acf3f30efe925caf Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 5 Nov 2026 14:48:07 +0100 Subject: [PATCH 257/271] move workflow_roc_v1_1 --- .../workflow-ro-crate-1.1/test_valid_wroc_v1_1.py | 4 ++-- tests/ro_crates.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py index 5d2cff584..23361faa8 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py @@ -15,7 +15,7 @@ import logging from rocrate_validator.models import Severity -from tests.ro_crates import ValidROC +from tests.ro_crates import ValidWROC_v1_1 from tests.shared import do_entity_test logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def test_valid_workflow_roc_required(): """Test valid Workflow RO-Crate metadata without checking the payload.""" do_entity_test( - ValidROC().workflow_roc_v1_1, + ValidWROC_v1_1().workflow_roc_v1_1, Severity.REQUIRED, True, profile_identifier="workflow-ro-crate-1.1", diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 527ae2d29..97ef98b7f 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -77,10 +77,6 @@ def bagit_remote_zip(self) -> str: def workflow_roc(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc" - @property - def workflow_roc_v1_1(self) -> Path: - return VALID_CRATES_DATA_PATH / "workflow-roc-1.1" - @property def workflow_roc_string_license(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc-string-license" @@ -126,6 +122,13 @@ def multi_profile_crate(self) -> Path: return VALID_CRATES_DATA_PATH / "multi-profile-crate" +class ValidWROC_v1_1: + + @property + def workflow_roc_v1_1(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc-1.1" + + class InvalidFileDescriptor: base_path = INVALID_CRATES_DATA_PATH / "0_file_descriptor_format" From 4a1bb93827fd6f786389b0216c9bae9bf9d166c1 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 5 Nov 2026 14:51:36 +0100 Subject: [PATCH 258/271] log issues when the expected validation result is True --- tests/shared.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/shared.py b/tests/shared.py index fb9932d7c..f4fb23a8d 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -234,6 +234,11 @@ def do_entity_test( # pylint: disable=too-many-locals requirement_severity, result.context.requirement_severity, ) + + if expected_validation_result and result.has_issues(): + for issue in result.get_issues(): + logger.debug(f"Detected issue of severity {issue.severity.name} with check \"{issue.check.identifier}\": {issue.message}") + assert result.passed() == expected_validation_result, ( f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" ) From 1c47a903380c716fd050dd2d3438e669fb2406e8 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 5 Nov 2026 15:19:09 +0100 Subject: [PATCH 259/271] add test for valid wroc with license as a string --- .../ro-crate-metadata.json | 141 ++++++++++++++++++ .../test_valid_wroc_v1_1.py | 7 + tests/ro_crates.py | 4 + 3 files changed, 152 insertions(+) create mode 100644 tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json diff --git a/tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json new file mode 100644 index 000000000..e914d74ad --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json @@ -0,0 +1,141 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": "Apache-2.0", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py index 23361faa8..acea4169c 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py @@ -30,3 +30,10 @@ def test_valid_workflow_roc_required(): profile_identifier="workflow-ro-crate-1.1", metadata_only=True, ) + do_entity_test( + ValidWROC_v1_1().workflow_roc_string_license_v1_1, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 97ef98b7f..6caae2557 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -128,6 +128,10 @@ class ValidWROC_v1_1: def workflow_roc_v1_1(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc-1.1" + @property + def workflow_roc_string_license_v1_1(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc-string-license-1.1" + class InvalidFileDescriptor: base_path = INVALID_CRATES_DATA_PATH / "0_file_descriptor_format" From fba3365e0e6b3845009c23ec090100d89e99b9ee Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 5 Nov 2026 16:50:17 +0100 Subject: [PATCH 260/271] fix ruff check --- tests/shared.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/shared.py b/tests/shared.py index f4fb23a8d..2fd30bc64 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -152,7 +152,7 @@ def _prepare_temp_rocrate( return temp_rocrate_path -def do_entity_test( # pylint: disable=too-many-locals +def do_entity_test( # noqa: PLR0912 # Shared test driver: the long signature mirrors `ValidationSettings` # plus the assertion-shaping knobs, so locals scale with the surface area # it has to cover. Splitting it would only push the locals to the caller. @@ -237,7 +237,10 @@ def do_entity_test( # pylint: disable=too-many-locals if expected_validation_result and result.has_issues(): for issue in result.get_issues(): - logger.debug(f"Detected issue of severity {issue.severity.name} with check \"{issue.check.identifier}\": {issue.message}") + logger.debug( + f"Detected issue of severity {issue.severity.name} " + f"with check \"{issue.check.identifier}\": {issue.message}" + ) assert result.passed() == expected_validation_result, ( f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" From d859cc9e598e0ea1c73f1571161ffb5cfbd71863 Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 6 Nov 2026 11:01:39 +0100 Subject: [PATCH 261/271] wroc 1.1: add main workflow tests --- .../ro-crate-metadata.json | 148 +++++++++++ .../ro-crate-metadata.json | 148 +++++++++++ .../ro-crate-metadata.json | 147 +++++++++++ .../ro-crate-metadata.json | 132 ++++++++++ .../ro-crate-metadata.json | 130 ++++++++++ .../ro-crate-metadata.json | 148 +++++++++++ .../ro-crate-metadata.json | 135 ++++++++++ .../ro-crate-metadata.json | 133 ++++++++++ .../test_main_workflow_v1_1.py | 235 ++++++++++++++++++ tests/ro_crates.py | 36 +++ 10 files changed, 1392 insertions(+) create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json new file mode 100644 index 000000000..ce423399e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/0.5-DRAFT-2020_07_21" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json new file mode 100644 index 000000000..0c5f6df26 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "Action", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json new file mode 100644 index 000000000..484fad7dd --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json @@ -0,0 +1,147 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json new file mode 100644 index 000000000..e1775cd15 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json @@ -0,0 +1,132 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json new file mode 100644 index 000000000..21a6e7841 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json @@ -0,0 +1,130 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json new file mode 100644 index 000000000..8ce38ec98 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json new file mode 100644 index 000000000..7846f1605 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json @@ -0,0 +1,135 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json new file mode 100644 index 000000000..3f6b31033 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json @@ -0,0 +1,133 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py new file mode 100644 index 000000000..1eb7642ed --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py @@ -0,0 +1,235 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import pytest + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidMainWorkflow_v1_1, ValidWROC_v1_1 +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_main_workflow_bad_type(): + """ + Test a Workflow RO-Crate where the main workflow has an incorrect type. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_bad_type, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["The Main Workflow types must include File, SoftwareSourceCode and ComputationalWorkflow"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_lang(): + """ + Test a Workflow RO-Crate where the main workflow does not have a + programmingLanguage property. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_lang, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["The Main Workflow must refer to its language via programmingLanguage"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_image(): + """ + Test a Workflow RO-Crate where the main workflow does not have an + image property. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_image, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_cwl_desc(): + """ + Test a Workflow RO-Crate where the main workflow does not have a + CWL description. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_cwl_desc, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_cwl_desc_bad_type(): + """ + Test a Workflow RO-Crate where the main workflow has a CWL description + but of the wrong type. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_cwl_desc_bad_type, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The CWL Description type must be File, SoftwareSourceCode, HowTo"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_cwl_desc_no_lang(): + """ + Test a Workflow RO-Crate where the main workflow has a CWL description + but the description has no programmingLanguage. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_cwl_desc_no_lang, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_file_existence(): + """ + Test a Workflow RO-Crate where the main workflow file is not in the crate. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_singleton_array(): + """A singleton-array mainEntity is JSON-LD-equivalent and passes REQUIRED checks.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_should_be_unpacked(): + """RO-Crate 1.3 recommends unpacking a singleton array in compacted JSON-LD.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "mainEntity" SHOULD be represented as a single value'], + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_file_existence(): + """A missing workflow in a singleton array produces the normal validation issue.""" + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +@pytest.mark.parametrize( + "main_entity", + [ + [], + [{"@id": "sort-and-change-case.ga"}, {"@id": "other-workflow.ga"}], + "sort-and-change-case.ga", + {"name": "sort-and-change-case.ga"}, + {"@id": 42}, + ], +) +def test_main_workflow_invalid_main_entity_is_reported(main_entity): + """Malformed mainEntity values fail validation without an unexpected exception.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["mainEntity"], + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + rocrate_entity_patch={"./": {"mainEntity": main_entity}}, + ) + + +def test_workflow_diagram_file_existence(): + """ + Test a Workflow RO-Crate where the workflow diagram file is not in the + crate. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.OPTIONAL, + False, + ["Workflow-related files existence"], + ["Workflow diagram", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_workflow_description_file_existence(): + """ + Test a Workflow RO-Crate where the workflow CWL description file is not in + the crate. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.OPTIONAL, + False, + ["Workflow-related files existence"], + ["Workflow CWL description", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_bad_conformsto(): + """ + Test a Workflow RO-Crate where the main workflow does not conform to the + bioschemas computational workflow 1.0 or later. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_bad_conformsto, + Severity.RECOMMENDED, + False, + ["Main Workflow recommended properties"], + ["The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 6caae2557..b9fd6e932 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -361,6 +361,42 @@ def main_workflow_bad_conformsto(self) -> Path: return self.base_path / "main_workflow_bad_conformsto" +class InvalidMainWorkflow_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "main_workflow" + + @property + def main_workflow_bad_type(self) -> Path: + return self.base_path / "main_workflow_bad_type" + + @property + def main_workflow_no_lang(self) -> Path: + return self.base_path / "main_workflow_no_lang" + + @property + def main_workflow_no_image(self) -> Path: + return self.base_path / "main_workflow_no_image" + + @property + def main_workflow_no_cwl_desc(self) -> Path: + return self.base_path / "main_workflow_no_cwl_desc" + + @property + def main_workflow_cwl_desc_bad_type(self) -> Path: + return self.base_path / "main_workflow_cwl_desc_bad_type" + + @property + def main_workflow_cwl_desc_no_lang(self) -> Path: + return self.base_path / "main_workflow_cwl_desc_no_lang" + + @property + def main_workflow_no_file(self) -> Path: + return self.base_path / "main_workflow_no_file" + + @property + def main_workflow_bad_conformsto(self) -> Path: + return self.base_path / "main_workflow_bad_conformsto" + + class WROCInvalidConformsTo: base_path = INVALID_CRATES_DATA_PATH / "2_wroc_descriptor" From f2b5ff833f266d9b75148114bdfe6e53d970fa36 Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 6 Nov 2026 12:35:41 +0100 Subject: [PATCH 262/271] wroc 1.1: add tests for missing tests and examples --- .../no_license/ro-crate-metadata.json | 126 ++++++++++++++++++ .../test_wroc_crate_v1_1.py | 49 +++++++ tests/ro_crates.py | 8 ++ 3 files changed, 183 insertions(+) create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json new file mode 100644 index 000000000..0420444e0 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json @@ -0,0 +1,126 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py new file mode 100644 index 000000000..4e6c7ad17 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCNoLicense_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_wroc_no_tests(): + """ + Test a Workflow RO-Crate with no test/ Dataset. + """ + do_entity_test( + WROCNoLicense_v1_1().wroc_no_license, + Severity.OPTIONAL, + False, + ["test directory"], + ["The test/ dir should be a Dataset"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_wroc_no_examples(): + """ + Test a Workflow RO-Crate with no examples/ Dataset. + """ + do_entity_test( + WROCNoLicense_v1_1().wroc_no_license, + Severity.OPTIONAL, + False, + ["examples directory"], + ["The examples/ dir should be a Dataset"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index b9fd6e932..04934484b 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -425,6 +425,14 @@ def wroc_no_license(self) -> Path: return self.base_path / "no_license" +class WROCNoLicense_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate/" + + @property + def wroc_no_license(self) -> Path: + return self.base_path / "no_license" + + class WROCMainEntity: base_path = INVALID_CRATES_DATA_PATH / "1_wroc_crate/" From 90f2accd675eb0c1a76147ef2cd1bd4341f2292e Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 6 Nov 2026 15:45:27 +0100 Subject: [PATCH 263/271] wroc 1.1 RDE conformsTo: change hasValue to pattern --- .../1.1/should/1_wroc_crate.ttl | 2 +- .../bad_profile/ro-crate-metadata.json | 148 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl index 5b404a6d0..2cc6ab489 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl @@ -33,7 +33,7 @@ workflow-ro-crate:WROCRootDataEntityMetadata a sh:NodeShape ; sh:class schema:CreativeWork; # At least one value of conformsTo must match the URI sh:qualifiedValueShape [ - sh:hasValue "https://w3id.org/workflowhub/workflow-ro-crate/1.1" ; + sh:pattern "^https://w3id.org/workflowhub/workflow-ro-crate/1\\.1$" ; ] ; sh:qualifiedMinCount 1 ; sh:minCount 1 ; diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json new file mode 100644 index 000000000..b2235ae72 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/foo/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/foo/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} From 769404c1f2c82d16aa0e1a6fe04716180b6da59f Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 9 Nov 2026 11:00:47 +0100 Subject: [PATCH 264/271] wroc 1.1: add test for bad RDE conformsTo --- .../test_wroc_crate_v1_1.py | 17 ++++++++++++++++- tests/ro_crates.py | 8 ++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py index 4e6c7ad17..be6979ca0 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py @@ -15,12 +15,27 @@ import logging from rocrate_validator.models import Severity -from tests.ro_crates import WROCNoLicense_v1_1 +from tests.ro_crates import WROCInvalidConformsTo_v1_1, WROCNoLicense_v1_1 from tests.shared import do_entity_test logger = logging.getLogger(__name__) +def test_wroc_rde_bad_conformsto(): + """ + Test a Workflow RO-Crate where the root data entity does not have a + conformsTo that points to the WROC 1.1 profile. + """ + do_entity_test( + WROCInvalidConformsTo_v1_1().wroc_bad_conforms_to, + Severity.RECOMMENDED, + False, + ["Root Data Entity Metadata"], + ["The Root Data Entity conformsTo SHOULD be an array that contains at least https://w3id.org/workflowhub/workflow-ro-crate/1.1"], + profile_identifier="workflow-ro-crate-1.1", + ) + + def test_wroc_no_tests(): """ Test a Workflow RO-Crate with no test/ Dataset. diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 04934484b..da07dc09b 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -405,6 +405,14 @@ def wroc_descriptor_bad_conforms_to(self) -> Path: return self.base_path / "wroc_descriptor_bad_conforms_to" +class WROCInvalidConformsTo_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate" + + @property + def wroc_bad_conforms_to(self) -> Path: + return self.base_path / "bad_profile" + + class WROCInvalidReadme: base_path = INVALID_CRATES_DATA_PATH / "1_wroc_crate/" From 572efab9bf4125efb3910e207b71e5bd64fc2732 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 9 Nov 2026 11:58:38 +0100 Subject: [PATCH 265/271] wroc 1.1: add test for readme and root metadata --- .../no_mainentity/ro-crate-metadata.json | 145 +++++++++++++++++ .../ro-crate-metadata.json | 145 +++++++++++++++++ .../ro-crate-metadata.json | 148 ++++++++++++++++++ .../test_wroc_readme_v1_1.py | 49 ++++++ .../test_wroc_root_metadata_v1_1.py | 49 ++++++ tests/ro_crates.py | 20 +++ 6 files changed, 556 insertions(+) create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json new file mode 100644 index 000000000..51917090e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json @@ -0,0 +1,145 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json new file mode 100644 index 000000000..810c58a7d --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json @@ -0,0 +1,145 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json new file mode 100644 index 000000000..4f73b3850 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/csv" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py new file mode 100644 index 000000000..cce6fe004 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidReadme_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_wroc_readme_not_about_crate(): + """ + Test a Workflow RO-Crate where the README.md is not about the crate. + """ + do_entity_test( + WROCInvalidReadme_v1_1().wroc_readme_not_about_crate, + Severity.RECOMMENDED, + False, + ["README.md properties"], + ["The README.md SHOULD be about the crate"], + profile_identifier="workflow-ro-crate-1.0", + ) + + +def test_wroc_readme_wrong_encoding_format(): + """ + Test a Workflow RO-Crate where the README.md has the wrong encodingFormat. + """ + do_entity_test( + WROCInvalidReadme_v1_1().wroc_readme_wrong_encoding_format, + Severity.RECOMMENDED, + False, + ["README.md properties"], + ["The README.md SHOULD have text/markdown as its encodingFormat"], + profile_identifier="workflow-ro-crate-1.0", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py new file mode 100644 index 000000000..4eca0fefc --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCMainEntity_v1_1, WROCNoLicense_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_wroc_no_license(): + """ + Test a Workflow RO-Crate where the root data entity has no license. + """ + do_entity_test( + WROCNoLicense_v1_1().wroc_no_license, + Severity.REQUIRED, + False, + ["WROC Root Data Entity Required Properties"], + ["The Crate (Root Data Entity) must specify a license, which should be a URL but can also be a string"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_wroc_no_mainentity(): + """ + Test a Workflow RO-Crate where the root data entity has no mainEntity. + """ + do_entity_test( + WROCMainEntity_v1_1().wroc_no_mainentity, + Severity.REQUIRED, + False, + ["Main Workflow entity existence"], + ["The Main Workflow must be specified through a `mainEntity` property in the root data entity"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index da07dc09b..7f6fd83e6 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -425,6 +425,18 @@ def wroc_readme_wrong_encoding_format(self) -> Path: return self.base_path / "readme_wrong_encoding_format" +class WROCInvalidReadme_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate/" + + @property + def wroc_readme_not_about_crate(self) -> Path: + return self.base_path / "readme_not_about_crate" + + @property + def wroc_readme_wrong_encoding_format(self) -> Path: + return self.base_path / "readme_wrong_encoding_format" + + class WROCNoLicense: base_path = INVALID_CRATES_DATA_PATH / "1_wroc_crate/" @@ -449,6 +461,14 @@ def wroc_no_mainentity(self) -> Path: return self.base_path / "no_mainentity" +class WROCMainEntity_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate/" + + @property + def wroc_no_mainentity(self) -> Path: + return self.base_path / "no_mainentity" + + class InvalidProcRC: base_path = INVALID_CRATES_DATA_PATH / "3_process_run_crate/" From 64e56e35ff46589d33943f3de2308f88a523af56 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 9 Nov 2026 16:32:36 +0100 Subject: [PATCH 266/271] wroc 1.1: add workflow step constraints --- .../1.1/may/0_main-workflow.ttl | 10 ++ .../workflow-ro-crate/1.1/may/3_howtostep.ttl | 30 ++++ .../1.1/must/0_main-workflow.ttl | 32 ++++ .../ro-crate-metadata.json | 166 ++++++++++++++++++ .../ro-crate-metadata.json | 166 ++++++++++++++++++ .../workflow-roc-1.1/ro-crate-metadata.json | 21 ++- .../test_howtostep_v1_1.py | 36 ++++ .../test_main_workflow_v1_1.py | 15 ++ tests/ro_crates.py | 12 ++ 9 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl index eacef57a3..de483f050 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl @@ -44,6 +44,16 @@ workflow-ro-crate:MainWorkflowOptionalProperties a sh:NodeShape ; sh:minCount 1 ; sh:message "The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'" ; # sh:severity sh:Info ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow step" ; + sh:description "The Main Workflow MAY refer to HowToStep descriptions; if present they MUST be referred to via 'step'" ; + sh:path schema:step ; + sh:class schema:HowToStep ; + sh:minCount 1 ; + sh:message "The Main Workflow MAY refer to HowToStep descriptions; if present they MUST be referred to via 'step'" ; + # sh:severity sh:Info ; ] . workflow-ro-crate:CWLDescriptionProperties a sh:NodeShape ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl new file mode 100644 index 000000000..e48a07c35 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl @@ -0,0 +1,30 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix workflow-ro-crate: . +@prefix schema: . +@prefix sh: . + +workflow-ro-crate:WROCHowToStepOptional a sh:NodeShape ; + sh:name "WROC HowToStep MAY" ; + sh:description "Optional properties of a Workflow RO-Crate HowToStep" ; + sh:targetClass schema:HowToStep ; + sh:property [ + a sh:PropertyShape ; + sh:name "HowToStep position" ; + sh:description "A HowToStep may indicate its position in the execution order via position" ; + sh:path schema:position ; + sh:minCount 1 ; + sh:message "A HowToStep may indicate its position in the execution order via position" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl index ae418200d..9742f2885 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl @@ -83,3 +83,35 @@ workflow-ro-crate:MainWorkflowRequiredProperties a sh:NodeShape ; sh:minCount 1 ; sh:message "The Main Workflow must refer to its language via programmingLanguage" ; ] . + + +workflow-ro-crate:MainWorkflowWithSteps a sh:NodeShape ; + sh:name "Main Workflow with steps MUST" ; + sh:description "Required properties of a Main Workflow that links to steps" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root schema:mainEntity ?this . + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) . + ?this schema:step ?step . + ?step a schema:HowToStep + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow with steps type" ; + sh:description "A Main Workflow that links to steps MUST have the HowTo type" ; + sh:path rdf:type ; + sh:hasValue schema:HowTo; + sh:minCount 1 ; + sh:message "A Main Workflow that links to steps MUST have the HowTo type" ; + ] . diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json new file mode 100644 index 000000000..084777d19 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json @@ -0,0 +1,166 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#sort", + "@type": "HowToStep" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json new file mode 100644 index 000000000..1dfd8b744 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json @@ -0,0 +1,166 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json index 8ce38ec98..fa533faca 100644 --- a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json @@ -66,11 +66,20 @@ "@type": [ "File", "SoftwareSourceCode", - "ComputationalWorkflow" + "ComputationalWorkflow", + "HowTo" ], "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], "description": "sort lines and change text to upper case", "image": { "@id": "blank.png" @@ -84,6 +93,16 @@ "@id": "sort-and-change-case.cwl" } }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", "@type": "ComputerLanguage", diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py new file mode 100644 index 000000000..ae224f232 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidHowToStep_v1_1 +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_howtostep_no_position(): + """ + Test a Workflow RO-Crate where a HowToStep has no position. + """ + do_entity_test( + WROCInvalidHowToStep_v1_1().wroc_howtostep_no_position, + Severity.OPTIONAL, + False, + ["WROC HowToStep MAY"], + ["A HowToStep may indicate its position in the execution order via position"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py index 1eb7642ed..90de8a393 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py @@ -233,3 +233,18 @@ def test_main_workflow_bad_conformsto(): ["The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later"], profile_identifier="workflow-ro-crate-1.1", ) + + +def test_main_workflow_with_steps_bad_type(): + """ + Test a Workflow RO-Crate that refers to steps but does not have the + HowTo type. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_with_steps_bad_type, + Severity.REQUIRED, + False, + ["Main Workflow with steps MUST"], + ["A Main Workflow that links to steps MUST have the HowTo type"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 7f6fd83e6..d3a8c6baa 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -396,6 +396,10 @@ def main_workflow_no_file(self) -> Path: def main_workflow_bad_conformsto(self) -> Path: return self.base_path / "main_workflow_bad_conformsto" + @property + def main_workflow_with_steps_bad_type(self) -> Path: + return self.base_path / "main_workflow_with_steps_bad_type" + class WROCInvalidConformsTo: base_path = INVALID_CRATES_DATA_PATH / "2_wroc_descriptor" @@ -469,6 +473,14 @@ def wroc_no_mainentity(self) -> Path: return self.base_path / "no_mainentity" +class WROCInvalidHowToStep_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "howtostep" + + @property + def wroc_howtostep_no_position(self) -> Path: + return self.base_path / "howtostep_no_position" + + class InvalidProcRC: base_path = INVALID_CRATES_DATA_PATH / "3_process_run_crate/" From d357eaaa138b32f0963744bce296b8b61fd959a7 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 10 Nov 2026 11:06:31 +0100 Subject: [PATCH 267/271] wroc 1.1: add workflow input/output constraints --- .../1.1/may/4_computational_workflow.ttl | 39 ++++ .../1.1/must/2_computational_workflow.ttl | 32 +++ .../bad_input/ro-crate-metadata.json | 188 ++++++++++++++++++ .../bad_output/ro-crate-metadata.json | 188 ++++++++++++++++++ .../no_input/ro-crate-metadata.json | 179 +++++++++++++++++ .../no_output/ro-crate-metadata.json | 179 +++++++++++++++++ .../workflow-roc-1.1/ro-crate-metadata.json | 24 +++ .../test_computational_workflow_v1_1.py | 82 ++++++++ tests/ro_crates.py | 19 ++ 9 files changed, 930 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl new file mode 100644 index 000000000..01e930ebc --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix workflow-ro-crate: . + +workflow-ro-crate:WROCComputationalWorkflowOptional a sh:NodeShape ; + sh:name "Workflow RO-Crate ComputationalWorkflow MAY" ; + sh:description "Optional properties of a Workflow RO-Crate ComputationalWorkflow" ; + sh:targetClass bioschemas:ComputationalWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "ComputationalWorkflow input" ; + sh:description "A ComputationalWorkflow MAY have an input" ; + sh:path bioschemas:input ; + sh:minCount 1 ; + sh:message "A ComputationalWorkflow MAY have an input" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "ComputationalWorkflow output" ; + sh:description "A ComputationalWorkflow MAY have an output" ; + sh:path bioschemas:output ; + sh:minCount 1 ; + sh:message "A ComputationalWorkflow MAY have an output" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl new file mode 100644 index 000000000..eed4a6448 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl @@ -0,0 +1,32 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix bioschemas: . +@prefix sh: . +@prefix workflow-ro-crate: . + +workflow-ro-crate:WROCComputationalWorkflowRequired a sh:NodeShape ; + sh:name "Workflow RO-Crate ComputationalWorkflow MUST" ; + sh:description "Required properties of the Workflow RO-Crate ComputationalWorkflow" ; + sh:targetClass bioschemas:ComputationalWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "ComputationalWorkflow input and output" ; + sh:description "ComputationalWorkflow input and output MUST point to FormalParameter entities" ; + sh:path [ + sh:alternativePath (bioschemas:input bioschemas:output) ; + ] ; + sh:class bioschemas:FormalParameter ; + sh:message "ComputationalWorkflow input and output MUST point to FormalParameter entities" ; + ] . diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json new file mode 100644 index 000000000..f48351927 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json @@ -0,0 +1,188 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "CreativeWork" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json new file mode 100644 index 000000000..c6e489e5f --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json @@ -0,0 +1,188 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "CreativeWork" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json new file mode 100644 index 000000000..b08c3c3f6 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json @@ -0,0 +1,179 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json new file mode 100644 index 000000000..0e5aed89e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json @@ -0,0 +1,179 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json index fa533faca..9d8c74d1a 100644 --- a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json @@ -72,6 +72,16 @@ "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], "step": [ { "@id": "#sort" @@ -93,6 +103,20 @@ "@id": "sort-and-change-case.cwl" } }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, { "@id": "#sort", "@type": "HowToStep", diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py new file mode 100644 index 000000000..7aa2f585b --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py @@ -0,0 +1,82 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidComputationalWorkflow_v1_1 +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_computational_workflow_no_input(): + """ + Test a Workflow RO-Crate where a computational workflow does not have + an input property. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().no_input, + Severity.OPTIONAL, + False, + ["Workflow RO-Crate ComputationalWorkflow MAY"], + ["A ComputationalWorkflow MAY have an input"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_computational_workflow_no_output(): + """ + Test a Workflow RO-Crate where a computational workflow does not have + an output property. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().no_output, + Severity.OPTIONAL, + False, + ["Workflow RO-Crate ComputationalWorkflow MAY"], + ["A ComputationalWorkflow MAY have an output"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_computational_workflow_bad_input(): + """ + Test a Workflow RO-Crate where a computational workflow has an input + property that does not point to a FormalParameter. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().bad_input, + Severity.REQUIRED, + False, + ["Workflow RO-Crate ComputationalWorkflow MUST"], + ["ComputationalWorkflow input and output MUST point to FormalParameter entities"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_computational_workflow_bad_output(): + """ + Test a Workflow RO-Crate where a computational workflow has an output + property that does not point to a FormalParameter. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().bad_output, + Severity.REQUIRED, + False, + ["Workflow RO-Crate ComputationalWorkflow MUST"], + ["ComputationalWorkflow input and output MUST point to FormalParameter entities"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index d3a8c6baa..1cc2d9943 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -401,6 +401,25 @@ def main_workflow_with_steps_bad_type(self) -> Path: return self.base_path / "main_workflow_with_steps_bad_type" +class WROCInvalidComputationalWorkflow_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "computational_workflow" + @property + def no_input(self) -> Path: + return self.base_path / "no_input" + + @property + def no_output(self) -> Path: + return self.base_path / "no_output" + + @property + def bad_input(self) -> Path: + return self.base_path / "bad_input" + + @property + def bad_output(self) -> Path: + return self.base_path / "bad_output" + + class WROCInvalidConformsTo: base_path = INVALID_CRATES_DATA_PATH / "2_wroc_descriptor" From 462a1c10d7767fc416688eab4f993c5b9ca76bc8 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 10 Nov 2026 11:33:03 +0100 Subject: [PATCH 268/271] wroc-1.1: add test and examples to valid crate --- .../crates/valid/workflow-roc-1.1/examples/dummy_example.txt | 1 + tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt | 1 + .../profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt create mode 100644 tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt diff --git a/tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt b/tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt new file mode 100644 index 000000000..c88ab3c1b --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt @@ -0,0 +1 @@ +Dummy example diff --git a/tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt b/tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt new file mode 100644 index 000000000..b70f706a0 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt @@ -0,0 +1 @@ +Dummy test diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py index acea4169c..8b558edc7 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py @@ -22,13 +22,12 @@ def test_valid_workflow_roc_required(): - """Test valid Workflow RO-Crate metadata without checking the payload.""" + """Test Workflow RO-Crates that are valid at the REQUIRED level.""" do_entity_test( ValidWROC_v1_1().workflow_roc_v1_1, Severity.REQUIRED, True, - profile_identifier="workflow-ro-crate-1.1", - metadata_only=True, + profile_identifier="workflow-ro-crate-1.1" ) do_entity_test( ValidWROC_v1_1().workflow_roc_string_license_v1_1, From 176c369aba1ede0ab3c5d3e7e1cf11e7ccfca511 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 10 Nov 2026 12:21:57 +0100 Subject: [PATCH 269/271] wroc-1.1: fix step constraints --- .../1.1/may/0_main-workflow.ttl | 5 +- .../1.1/must/0_main-workflow.ttl | 8 + .../ro-crate-metadata.json | 190 ++++++++++++++++++ .../ro-crate-metadata.json | 173 ++++++++++++++++ .../test_main_workflow_v1_1.py | 30 +++ tests/ro_crates.py | 8 + 6 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl index de483f050..92af82be3 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl @@ -48,11 +48,10 @@ workflow-ro-crate:MainWorkflowOptionalProperties a sh:NodeShape ; sh:property [ a sh:PropertyShape ; sh:name "Main Workflow step" ; - sh:description "The Main Workflow MAY refer to HowToStep descriptions; if present they MUST be referred to via 'step'" ; + sh:description "The Main Workflow MAY have a step property" ; sh:path schema:step ; - sh:class schema:HowToStep ; sh:minCount 1 ; - sh:message "The Main Workflow MAY refer to HowToStep descriptions; if present they MUST be referred to via 'step'" ; + sh:message "The Main Workflow MAY have a step property" ; # sh:severity sh:Info ; ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl index 9742f2885..103c4eb17 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl @@ -82,6 +82,14 @@ workflow-ro-crate:MainWorkflowRequiredProperties a sh:NodeShape ; sh:class schema:ComputerLanguage ; sh:minCount 1 ; sh:message "The Main Workflow must refer to its language via programmingLanguage" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow step" ; + sh:description "Main Workflow step MUST point to HowToStep entities" ; + sh:path schema:step ; + sh:class schema:HowToStep ; + sh:message "Main Workflow step MUST point to HowToStep entities" ; ] . diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json new file mode 100644 index 000000000..eba7c481e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json @@ -0,0 +1,190 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "CreativeWork" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json new file mode 100644 index 000000000..1318e62d5 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json @@ -0,0 +1,173 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py index 90de8a393..ff9775b7d 100644 --- a/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py @@ -235,6 +235,36 @@ def test_main_workflow_bad_conformsto(): ) +def test_main_workflow_no_step(): + """ + Test a Workflow RO-Crate where the main workflow does not have a + step property. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_step, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The Main Workflow MAY have a step property"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_bad_step(): + """ + Test a Workflow RO-Crate where the main workflow has a step property + that does not point to HowToStep entities. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_bad_step, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["Main Workflow step MUST point to HowToStep entities"], + profile_identifier="workflow-ro-crate-1.1", + ) + + def test_main_workflow_with_steps_bad_type(): """ Test a Workflow RO-Crate that refers to steps but does not have the diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 1cc2d9943..7b60d25ca 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -396,6 +396,14 @@ def main_workflow_no_file(self) -> Path: def main_workflow_bad_conformsto(self) -> Path: return self.base_path / "main_workflow_bad_conformsto" + @property + def main_workflow_no_step(self) -> Path: + return self.base_path / "main_workflow_no_step" + + @property + def main_workflow_bad_step(self) -> Path: + return self.base_path / "main_workflow_bad_step" + @property def main_workflow_with_steps_bad_type(self) -> Path: return self.base_path / "main_workflow_with_steps_bad_type" From be7ee435d2337ee0f2fa28c4d44de87cda30aa6a Mon Sep 17 00:00:00 2001 From: EttoreM Date: Tue, 10 Nov 2026 12:26:16 +0000 Subject: [PATCH 270/271] Fix Five Safes 0.4 check name uniqueness for roc-validator 0.13.1 --- .../profiles/five-safes-crate/10_outputs.ttl | 2 +- .../11_workflow_execution_phase.ttl | 12 +++---- .../five-safes-crate/12_check_phase.ttl | 30 ++++++++--------- .../five-safes-crate/13_validation_phase.ttl | 32 +++++++++---------- .../14_workflow_retrieval_phase.ttl | 30 ++++++++--------- .../five-safes-crate/1_root_data_entity.ttl | 6 ++-- .../profiles/five-safes-crate/4_sign_off.ttl | 6 ++-- .../five-safes-crate/6_workflow_reference.ttl | 10 +++--- .../7_requested_workflow_run.ttl | 4 +-- .../five-safes-crate/8_disclosure_phase.ttl | 20 ++++++------ .../five-safes-crate/test_5src_10_outputs.py | 2 +- .../test_5src_11_workflow_execution.py | 10 +++--- .../test_5src_12_check_phase.py | 18 +++++------ .../test_5src_13_validation_phase.py | 16 +++++----- .../test_5src_14_workflow_retrieval_phase.py | 22 ++++++------- .../test_5src_1_root_data_entity_metadata.py | 4 +-- .../test_5src_6_workflow_reference.py | 6 ++-- .../test_5src_7_requesting_workflow_run.py | 8 ++--- .../test_5src_8_disclosure_phase.py | 16 +++++----- 19 files changed, 127 insertions(+), 127 deletions(-) diff --git a/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl b/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl index 3e3dbb5e3..7d71eb30d 100644 --- a/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl +++ b/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl @@ -30,7 +30,7 @@ five-safes-crate:CreateActionHasResultIfActionCompleted a sh:NodeShape ; - sh:name "WorkflowRunAction" ; + sh:name "Completed workflow run SHOULD have a result" ; sh:description "The `CreateAction` corresponding to the workflow run, with CompletedActionStatus, SHOULD have the `schema:result` property." ; sh:target [ diff --git a/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl b/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl index c5b813e26..bc750b2ef 100644 --- a/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl @@ -27,12 +27,12 @@ five-safes-crate:WorkflowMustHaveDescriptiveName a sh:NodeShape ; - sh:name "WorkflowExecution" ; + sh:name "Workflow execution MUST have a descriptive name" ; sh:targetClass ro-crate:WorkflowRunAction ; sh:property [ a sh:PropertyShape ; - sh:name "name" ; + sh:name "Workflow execution name" ; sh:minCount 1 ; sh:description "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ; sh:path schema:name ; @@ -45,12 +45,12 @@ five-safes-crate:WorkflowMustHaveDescriptiveName five-safes-crate:WorkflowMustHaveActionStatusWithAllowedValues a sh:NodeShape ; - sh:name "WorkflowExecution" ; + sh:name "Workflow execution MUST have an allowed action status" ; sh:targetClass ro-crate:WorkflowRunAction ; sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "actionStatus" ; + sh:name "Workflow execution action status" ; sh:description "`CreateAction` MUST have an actionStatus with an allowed value (see https://schema.org/ActionStatusType)." ; sh:path schema:actionStatus ; sh:in ( @@ -91,7 +91,7 @@ five-safes-crate:WorkflowexecutionObjectHasEndTimeIfEnded sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "Workflow execution end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -132,7 +132,7 @@ five-safes-crate:WorkflowexecutionObjectHasStartTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "Workflow execution start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl b/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl index fc4d100f5..41f127ef9 100644 --- a/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl @@ -27,7 +27,7 @@ five-safes-crate:CheckValueObjectHasDescriptiveNameAndIsAssessAction a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue MUST have a name and AssessAction type" ; sh:description "" ; sh:target [ @@ -53,7 +53,7 @@ five-safes-crate:CheckValueObjectHasDescriptiveNameAndIsAssessAction sh:property [ sh:a sh:PropertyShape ; - sh:name "name" ; + sh:name "CheckValue name" ; sh:description "CheckValue MUST have a human readable name string." ; sh:path schema:name ; sh:datatype xsd:string ; @@ -63,7 +63,7 @@ five-safes-crate:CheckValueObjectHasDescriptiveNameAndIsAssessAction five-safes-crate:CheckValueActionStatusMustHaveAllowedValues a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue MUST have an allowed action status" ; sh:description "" ; sh:target [ @@ -82,7 +82,7 @@ five-safes-crate:CheckValueActionStatusMustHaveAllowedValues sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "CheckValue action status" ; sh:path schema:actionStatus ; sh:in ( "http://schema.org/PotentialActionStatus" @@ -99,7 +99,7 @@ five-safes-crate:CheckValueActionStatusMustHaveAllowedValues five-safes-crate:RootDataEntityShouldMentionCheckValueObject a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention CheckValue" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "" ; sh:severity sh:Warning ; @@ -125,7 +125,7 @@ five-safes-crate:RootDataEntityShouldMentionCheckValueObject five-safes-crate:CheckValueObjectShouldPointToRootDataEntity a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD reference the root data entity" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -141,7 +141,7 @@ five-safes-crate:CheckValueObjectShouldPointToRootDataEntity sh:property [ a sh:PropertyShape ; - sh:name "object" ; + sh:name "CheckValue object" ; sh:path schema:object ; sh:minCount 1 ; sh:class ro-crate:RootDataEntity ; @@ -152,7 +152,7 @@ five-safes-crate:CheckValueObjectShouldPointToRootDataEntity five-safes-crate:CheckValueInstrumentShouldPointToEntityTypedDefinedTerm a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD reference a DefinedTerm instrument" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -168,7 +168,7 @@ five-safes-crate:CheckValueInstrumentShouldPointToEntityTypedDefinedTerm sh:property [ a sh:PropertyShape ; - sh:name "instrument" ; + sh:name "CheckValue instrument" ; sh:path schema:instrument ; sh:minCount 1 ; sh:class schema:DefinedTerm ; @@ -179,7 +179,7 @@ five-safes-crate:CheckValueInstrumentShouldPointToEntityTypedDefinedTerm five-safes-crate:CheckValueAgentShouldIdentifyTheAgentWhoPerformnedTheCheck a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD identify the agent performing the check" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -206,7 +206,7 @@ five-safes-crate:CheckValueAgentShouldIdentifyTheAgentWhoPerformnedTheCheck five-safes-crate:CheckValueShouldHaveEndTime a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD have an end time" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -227,7 +227,7 @@ five-safes-crate:CheckValueShouldHaveEndTime sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "CheckValue end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -238,7 +238,7 @@ five-safes-crate:CheckValueShouldHaveEndTime five-safes-crate:CheckValueShouldHaveActionStatus a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD have an action status" ; sh:description "" ; sh:target [ @@ -256,7 +256,7 @@ five-safes-crate:CheckValueShouldHaveActionStatus sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "CheckValue action status" ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:severity sh:Warning ; @@ -290,7 +290,7 @@ five-safes-crate:CheckValueMayHaveStartTime sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "CheckValue start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl b/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl index e9d25d46b..e269e3cad 100644 --- a/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl @@ -27,7 +27,7 @@ five-safes-crate:ValidationCheckObjectHasDescriptiveNameAndIsAssessAction a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck MUST have a name and AssessAction type" ; sh:description "" ; sh:target [ @@ -53,7 +53,7 @@ five-safes-crate:ValidationCheckObjectHasDescriptiveNameAndIsAssessAction sh:property [ sh:a sh:PropertyShape ; - sh:name "name" ; + sh:name "ValidationCheck name" ; sh:description "ValidationCheck MUST have a human readable name string." ; sh:path schema:name ; sh:datatype xsd:string ; @@ -64,7 +64,7 @@ five-safes-crate:ValidationCheckObjectHasDescriptiveNameAndIsAssessAction five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck MUST have an allowed action status" ; sh:target [ a sh:SPARQLTarget ; sh:select """ @@ -81,7 +81,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue sh:property [ a sh:PropertyShape ; - sh:name "actionStatus" ; + sh:name "ValidationCheck action status value" ; sh:description "The `actionStatus` of ValidationCheck MUST have an allowed value (see https://schema.org/ActionStatusType)." ; sh:path schema:actionStatus ; sh:in ( @@ -97,7 +97,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck MUST have an allowed action status" ; sh:target [ a sh:SPARQLTarget ; sh:select """ @@ -114,7 +114,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "actionStatus" ; + sh:name "ValidationCheck action status presence and value" ; sh:description "actionStatus MUST be either PotentialActionStatus, ActiveActionStatus, CompletedActionStatus, or FailedActionStatus." ; sh:path schema:actionStatus ; sh:in ( @@ -132,7 +132,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue five-safes-crate:RootDataEntityShouldMentionValidationCheckObject a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention ValidationCheck" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "" ; sh:severity sh:Warning ; @@ -158,7 +158,7 @@ five-safes-crate:RootDataEntityShouldMentionValidationCheckObject five-safes-crate:ValidationCheckObjectShouldPointToRootDataEntity a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD reference the root data entity" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -174,7 +174,7 @@ five-safes-crate:ValidationCheckObjectShouldPointToRootDataEntity sh:property [ a sh:PropertyShape ; - sh:name "object" ; + sh:name "ValidationCheck object" ; sh:path schema:object ; sh:minCount 1 ; sh:class ro-crate:RootDataEntity ; @@ -184,7 +184,7 @@ five-safes-crate:ValidationCheckObjectShouldPointToRootDataEntity five-safes-crate:ValidationCheckInstrumentShouldPointToEntityWithSpecificId a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD reference the Five Safes profile as instrument" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -200,7 +200,7 @@ five-safes-crate:ValidationCheckInstrumentShouldPointToEntityWithSpecificId sh:property [ a sh:PropertyShape ; - sh:name "instrument" ; + sh:name "ValidationCheck instrument" ; sh:description "" ; sh:path schema:instrument ; sh:minCount 1 ; @@ -213,7 +213,7 @@ five-safes-crate:ValidationCheckInstrumentShouldPointToEntityWithSpecificId five-safes-crate:ValidationCheckShouldHaveActionStatus a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD have an action status" ; sh:description "" ; sh:target [ @@ -231,7 +231,7 @@ five-safes-crate:ValidationCheckShouldHaveActionStatus sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "ValidationCheck action status" ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:severity sh:Warning ; @@ -241,7 +241,7 @@ five-safes-crate:ValidationCheckShouldHaveActionStatus five-safes-crate:DownloadActionShouldHaveEndTimeIfBegun a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD have an end time when ended" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -262,7 +262,7 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "ValidationCheck end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -298,7 +298,7 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "ValidationCheck start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl b/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl index df7ad33e8..988e3efbf 100644 --- a/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl @@ -27,13 +27,13 @@ five-safes-crate:DownloadActionObjectMUSTHavesDescriptiveName a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction MUST have a descriptive name" ; sh:targetClass schema:DownloadAction ; sh:description "" ; sh:property [ a sh:PropertyShape ; - sh:name "name" ; + sh:name "DownloadAction name" ; sh:description "DownloadAction MUST have a human readable name string." ; sh:path schema:name ; sh:minCount 1 ; @@ -47,7 +47,7 @@ five-safes-crate:DownloadActionObjectMUSTHavesDescriptiveName five-safes-crate:WorkflowSameAsAndRootDataEntityMainEntityMUSTBeTheSame a sh:NodeShape ; - sh:name "Downloaded Workflow" ; + sh:name "Downloaded workflow sameAs MUST match the root mainEntity" ; sh:description "" ; sh:severity sh:Violation ; @@ -89,7 +89,7 @@ five-safes-crate:WorkflowSameAsAndRootDataEntityMainEntityMUSTBeTheSame five-safes-crate:DownloadedWorkflowDistributionAndDownloadActionObjectMUSTBeTheSame a sh:NodeShape ; - sh:name "Downloaded Workflow" ; + sh:name "Downloaded workflow distribution MUST match the download object" ; sh:description "" ; sh:severity sh:Violation ; @@ -130,7 +130,7 @@ five-safes-crate:DownloadedWorkflowDistributionAndDownloadActionObjectMUSTBeTheS five-safes-crate:DownloadActionActionStatusMUSTHaveAllowedValues a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction MUST have an allowed action status" ; sh:description "" ; sh:target [ @@ -149,7 +149,7 @@ five-safes-crate:DownloadActionActionStatusMUSTHaveAllowedValues sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "DownloadAction action status" ; sh:path schema:actionStatus ; sh:in ( "http://schema.org/PotentialActionStatus" @@ -166,7 +166,7 @@ five-safes-crate:DownloadActionActionStatusMUSTHaveAllowedValues five-safes-crate:DownloadActionEntitySHOULDExist a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "DownloadAction SHOULD exist" ; sh:description "" ; sh:targetClass ro-crate:RootDataEntity ; sh:severity sh:Warning ; @@ -192,7 +192,7 @@ five-safes-crate:DownloadActionEntitySHOULDExist five-safes-crate:RootDataEntitySHOULDMentionDownloadActionIfPresent a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention DownloadAction when present" ; sh:description "" ; sh:targetClass ro-crate:RootDataEntity ; sh:severity sh:Warning ; @@ -218,7 +218,7 @@ five-safes-crate:RootDataEntitySHOULDMentionDownloadActionIfPresent five-safes-crate:DownloadActionShouldHaveEndTimeIfEnded a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction SHOULD have an end time when ended" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -240,7 +240,7 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfEnded sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "DownloadAction end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -251,13 +251,13 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfEnded five-safes-crate:DownloadActionShouldHaveActionStatus a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction SHOULD have an action status" ; sh:targetClass schema:DownloadAction ; sh:description "" ; sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "DownloadAction action status" ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:severity sh:Warning ; @@ -269,7 +269,7 @@ five-safes-crate:DownloadActionShouldHaveActionStatus five-safes-crate:DownloadedWorkflowSHOULDExistAndBeReferencedByDownloadActionResult a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction result MAY reference an existing workflow entity" ; sh:description "Validates that DownloadAction result references an existing entity" ; sh:targetClass schema:DownloadAction ; @@ -304,7 +304,7 @@ five-safes-crate:DownloadedWorkflowSHOULDExistAndBeReferencedByDownloadActionRes five-safes-crate:DownloadActionMayHaveStartTimeIfBegun a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction MAY have a start time when begun" ; sh:description ( "`DownloadAction` MAY have the `startTime` property if `actionStatus` " "is either ActiveActionStatus, CompletedActionStatus or FailedActionStatus." @@ -331,7 +331,7 @@ five-safes-crate:DownloadActionMayHaveStartTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "DownloadAction start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl b/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl index b5481fd12..60c96854f 100644 --- a/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl +++ b/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl @@ -26,12 +26,12 @@ five-safes-crate:RootDataEntityRequiredProperties a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity MUST identify its source organization" ; sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; - sh:name "sourceOrganization" ; + sh:name "Root source organization presence" ; sh:path schema:sourceOrganization; sh:minCount 1 ; sh:severity sh:Violation ; @@ -40,7 +40,7 @@ five-safes-crate:RootDataEntityRequiredProperties sh:property [ a sh:PropertyShape ; - sh:name "sourceOrganization" ; + sh:name "Root source organization Project type" ; sh:path schema:sourceOrganization ; sh:class schema:Project ; sh:severity sh:Violation ; diff --git a/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl b/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl index 0c91901e4..770dc5576 100644 --- a/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl +++ b/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl @@ -78,7 +78,7 @@ five-safes-crate:SignOffObjectHasActionStatus sh:property [ a sh:PropertyShape ; - sh:name "actionStatus" ; + sh:name "SignOff action status" ; sh:description "The value of actionStatus MUST be one of the allowed values." ; sh:path schema:actionStatus ; sh:in ( @@ -248,7 +248,7 @@ five-safes-crate:SignOffPhaseEndTime sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "SignOff end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -284,7 +284,7 @@ five-safes-crate:SignOffPhaseStartTime sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "SignOff start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl b/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl index 222f2bc9b..4dc52c733 100644 --- a/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl +++ b/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl @@ -27,13 +27,13 @@ five-safes-crate:ReferenceToWorkflowCrate a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity MUST reference a workflow crate" ; sh:targetClass ro-crate:RootDataEntity ; # RootDataEntity MUST have an mainEntity property sh:property [ a sh:PropertyShape ; - sh:name "mainEntity" ; + sh:name "Root mainEntity cardinality and IRI" ; sh:path schema:mainEntity ; sh:minCount 1 ; sh:maxCount 1 ; @@ -45,7 +45,7 @@ five-safes-crate:ReferenceToWorkflowCrate # The mainEntity of a RootDataEntity MUST be a Dataset sh:property [ a sh:PropertyShape ; - sh:name "mainEntity" ; + sh:name "Root mainEntity Dataset type" ; sh:path schema:mainEntity ; sh:class schema:Dataset ; sh:severity sh:Violation ; @@ -59,7 +59,7 @@ five-safes-crate:mainEntityHasProperConformsTo sh:targetObjectsOf schema:mainEntity ; sh:property [ a sh:PropertyShape ; - sh:name "conformsTo" ; + sh:name "Workflow conformsTo cardinality" ; sh:path purl:conformsTo ; sh:minCount 1 ; sh:maxCount 1 ; @@ -68,7 +68,7 @@ five-safes-crate:mainEntityHasProperConformsTo ] ; sh:property [ a sh:PropertyShape ; - sh:name "conformsTo" ; + sh:name "Workflow conformsTo profile IRI" ; sh:path purl:conformsTo ; sh:nodeKind sh:IRI ; sh:pattern "^https://w3id\\.org/workflowhub/workflow-ro-crate" ; diff --git a/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl b/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl index 9b66f1769..7cc1040ea 100644 --- a/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl +++ b/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl @@ -26,7 +26,7 @@ five-safes-crate:RootDataEntityMentionsWorkflowRunAction a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity MUST mention a workflow run" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "" ; @@ -94,7 +94,7 @@ five-safes-crate:WorkflowRunObject five-safes-crate:WorkflowRunActionShouldHaveObjectProperty a sh:NodeShape ; sh:targetClass ro-crate:WorkflowRunAction ; - sh:name "WorkflowRunAction" ; + sh:name "Workflow run SHOULD have an object" ; sh:property [ sh:path schema:object ; sh:minCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl b/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl index 61b785a43..436a26886 100644 --- a/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl @@ -27,7 +27,7 @@ five-safes-crate:DisclosureObjectHasDescriptiveNameAndIsAssessAction a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck MUST have a name and AssessAction type" ; sh:description "DisclosureCheck" ; sh:target [ @@ -56,7 +56,7 @@ five-safes-crate:DisclosureObjectHasDescriptiveNameAndIsAssessAction sh:property [ a sh:PropertyShape ; - sh:name "name" ; + sh:name "DisclosureCheck name" ; sh:description "`DisclosureCheck` MUST have a name string of at least 10 characters." ; sh:minCount 1 ; sh:path schema:name ; @@ -69,7 +69,7 @@ five-safes-crate:DisclosureObjectHasDescriptiveNameAndIsAssessAction five-safes-crate:DisclosureObjectHasActionStatusWithAcceptedValue a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck MUST have an allowed action status" ; sh:description "`DisclosureCheck` MUST have an actionStatus with an allowed value (see https://schema.org/ActionStatusType)." ; sh:target [ @@ -88,7 +88,7 @@ five-safes-crate:DisclosureObjectHasActionStatusWithAcceptedValue sh:property [ a sh:PropertyShape ; - sh:name "actionStatus" ; + sh:name "DisclosureCheck action status" ; sh:description "`DisclosureCheck` MUST have an actionStatus with an allowed value (see https://schema.org/ActionStatusType)." ; sh:path schema:actionStatus ; sh:in ( @@ -106,7 +106,7 @@ five-safes-crate:DisclosureObjectHasActionStatusWithAcceptedValue five-safes-crate:RootDataEntityShouldMentionDisclosureObject a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention DisclosureCheck" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "RootDataEntity SHOULD mention a disclosure object." ; sh:severity sh:Warning ; @@ -133,7 +133,7 @@ five-safes-crate:RootDataEntityShouldMentionDisclosureObject five-safes-crate:DisclosureObjectHasActionStatus a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck SHOULD have an action status" ; sh:description "The `DisclosureCheck` SHOULD have `actionStatus` property." ; sh:target [ @@ -151,7 +151,7 @@ five-safes-crate:DisclosureObjectHasActionStatus sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "DisclosureCheck action status" ; sh:description "The `DisclosureCheck` SHOULD have `actionStatus` property." ; sh:path schema:actionStatus ; sh:minCount 1 ; @@ -162,7 +162,7 @@ five-safes-crate:DisclosureObjectHasActionStatus five-safes-crate:DisclosureObjectHasEndTimeIfcompletedOrFailed a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck SHOULD have an end time when ended" ; sh:description "DisclosureCheck" ; sh:target [ @@ -185,7 +185,7 @@ five-safes-crate:DisclosureObjectHasEndTimeIfcompletedOrFailed sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "DisclosureCheck end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -223,7 +223,7 @@ five-safes-crate:DisclosureObjectHasStartTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "DisclosureCheck start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py b/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py index 15dcc2449..a476b465c 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py @@ -50,7 +50,7 @@ def test_completed_createaction_does_not_have_result(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowRunAction"], + expected_triggered_requirements=["Completed workflow run SHOULD have a result"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run,", "with CompletedActionStatus, SHOULD have the `result` property.", diff --git a/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py b/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py index 9f54354d9..3de776bdf 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py @@ -45,7 +45,7 @@ def test_5src_workflow_object_with_no_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have a descriptive name"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ], @@ -76,7 +76,7 @@ def test_5src_workflow_object_with_name_not_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have a descriptive name"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ], @@ -107,7 +107,7 @@ def test_5src_workflow_object_with_not_long_enough_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have a descriptive name"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ], @@ -200,7 +200,7 @@ def test_5src_workflow_object_with_no_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have an allowed action status"], expected_triggered_issues=[ ( "`CreateAction` MUST have an actionStatus " @@ -233,7 +233,7 @@ def test_5src_workflow_object_with_no_properly_valued_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have an allowed action status"], expected_triggered_issues=[ ( "`CreateAction` MUST have an actionStatus " diff --git a/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py index 53171a4c0..93f596ba7 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py @@ -50,7 +50,7 @@ def test_5src_check_value_not_of_type_assess_action(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue MUST have a name and AssessAction type"], expected_triggered_issues=["CheckValue MUST be a `AssessAction`."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -78,7 +78,7 @@ def test_5src_check_value_name_not_a_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue MUST have a name and AssessAction type"], expected_triggered_issues=[ "CheckValue MUST have a human readable name string." ], @@ -177,7 +177,7 @@ def test_5src_check_value_has_action_status_with_not_allowed_value(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue MUST have an allowed action status"], expected_triggered_issues=[ "`CheckValue` --> `actionStatus` MUST have one of the allowed values." ], @@ -206,7 +206,7 @@ def test_5src_root_data_entity_does_not_mention_check_value_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention CheckValue"], expected_triggered_issues=[ "RootDataEntity SHOULD mention a check value object." ], @@ -235,7 +235,7 @@ def test_5src_check_value_object_does_not_point_to_root_data_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD reference the root data entity"], expected_triggered_issues=[ "`CheckValue` --> `object` SHOULD point to the root of the RO-Crate" ], @@ -267,7 +267,7 @@ def test_5src_check_value_instrument_does_not_point_to_entity_with_type_defined_ rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD reference a DefinedTerm instrument"], expected_triggered_issues=[ "`CheckValue` --> `instrument` SHOULD point to an entity typed `DefinedTerm`" ], @@ -295,7 +295,7 @@ def test_5src_check_value_does_not_have_end_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD have an end time"], expected_triggered_issues=["`CheckValue` SHOULD have the `endTime` property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -321,7 +321,7 @@ def test_5src_check_value_does_not_have_action_status_property(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD have an action status"], expected_triggered_issues=["CheckValue SHOULD have actionStatus property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -347,7 +347,7 @@ def test_5src_check_value_does_not_point_to_an_agent(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD identify the agent performing the check"], expected_triggered_issues=[ "`CheckValue` --> `agent` SHOULD reference the agent who initiated the check" ], diff --git a/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py index c19e54663..5a88c9933 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py @@ -47,7 +47,7 @@ def test_5src_validation_check_not_of_type_assess_action(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck MUST have a name and AssessAction type"], expected_triggered_issues=["ValidationCheck MUST be a `AssessAction`."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -74,7 +74,7 @@ def test_5src_validation_check_name_not_a_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck MUST have a name and AssessAction type"], expected_triggered_issues=["ValidationCheck MUST have a human readable name string."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -99,7 +99,7 @@ def test_5src_validation_check_has_action_status_with_not_allowed_value(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck MUST have an allowed action status"], expected_triggered_issues=[ ( "actionStatus MUST be either PotentialActionStatus, ActiveActionStatus, " @@ -197,7 +197,7 @@ def test_5src_root_data_entity_does_not_mention_validation_check_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention ValidationCheck"], expected_triggered_issues=["RootDataEntity SHOULD mention a ValidationCheck object."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -224,7 +224,7 @@ def test_5src_validation_check_object_does_not_point_to_root_data_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD reference the root data entity"], expected_triggered_issues=["`ValidationCheck` --> `object` SHOULD point to the root of the RO-Crate"], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -249,7 +249,7 @@ def test_5src_validation_check_instrument_does_not_point_to_5scrate_0p4(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD reference the Five Safes profile as instrument"], expected_triggered_issues=[ "`ValidationCheck` --> `instrument` SHOULD point to an entity with @id https://w3id.org/5s-crate/0.4" ], @@ -276,7 +276,7 @@ def test_5src_Validation_check_does_not_have_action_status_property(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD have an action status"], expected_triggered_issues=["ValidationCheck SHOULD have actionStatus property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -301,7 +301,7 @@ def test_5src_download_action_does_not_have_end_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD have an end time when ended"], expected_triggered_issues=[ ( "ValidationCheck SHOULD have the `endTime` property if `actionStatus` " diff --git a/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py index c7d92d0cd..e7aed54b6 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py @@ -44,7 +44,7 @@ def test_5src_download_action_does_not_have_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MUST have a descriptive name"], expected_triggered_issues=["DownloadAction MUST have a human readable name string."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -71,7 +71,7 @@ def test_5src_download_action_name_not_a_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MUST have a descriptive name"], expected_triggered_issues=["DownloadAction MUST have a human readable name string."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -167,7 +167,7 @@ def test_5src_downloaded_workflow_same_as_is_not_the_same_as_root_data_entity_ma rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["Downloaded Workflow"], + expected_triggered_requirements=["Downloaded workflow sameAs MUST match the root mainEntity"], expected_triggered_issues=[ ( "The property `sameAs` of the entity representing the downloaded workflow " @@ -200,7 +200,7 @@ def test_5src_downloaded_workflow_distribution_is_not_the_same_as_download_actio rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["Downloaded Workflow"], + expected_triggered_requirements=["Downloaded workflow distribution MUST match the download object"], expected_triggered_issues=[ ("DownloadedWorkflow --> `distribution` MUST reference the same entity as `DownloadAction` --> `object`.") ], @@ -230,7 +230,7 @@ def test_5src_download_action_has_action_status_with_not_allowed_value(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MUST have an allowed action status"], expected_triggered_issues=[ ( "The value of actionStatus MUST be one of the allowed values: " @@ -263,7 +263,7 @@ def test_5src_download_action_is_not_present(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["DownloadAction SHOULD exist"], expected_triggered_issues=["An entity typed DownloadAction SHOULD exist."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -287,7 +287,7 @@ def test_5src_root_data_entity_does_not_mention_download_action_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention DownloadAction when present"], expected_triggered_issues=["RootDataEntity SHOULD mention DownloadAction if this exists."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -312,7 +312,7 @@ def test_5src_download_action_does_not_have_end_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction SHOULD have an end time when ended"], expected_triggered_issues=[ ( "`DownloadAction` SHOULD have the `endTime` property " @@ -342,7 +342,7 @@ def test_5src_download_action_does_not_have_action_status_property(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction SHOULD have an action status"], expected_triggered_issues=["`DownloadAction` SHOULD have `actionStatus` property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -371,7 +371,7 @@ def test_5src_downloaded_workflow_is_not_represented_by_its_own_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.OPTIONAL, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction result MAY reference an existing workflow entity"], expected_triggered_issues=[ ( "The entity representing the downloaded workflow is not defined, " @@ -402,7 +402,7 @@ def test_5src_download_action_does_not_have_start_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.OPTIONAL, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MAY have a start time when begun"], expected_triggered_issues=[ ( "`DownloadAction` MAY have the `startTime` property if `actionStatus` " diff --git a/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py b/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py index 19e3f21fe..6a3b4600f 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py @@ -46,7 +46,7 @@ def test_5src_root_data_entity_without_source_organization(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST identify its source organization"], expected_triggered_issues=["""The Root Data Entity MUST have a `sourceOrganization` property."""], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -79,7 +79,7 @@ def test_5src_root_data_entity_source_organization_not_organization(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST identify its source organization"], expected_triggered_issues=[ """The `sourceOrganization` property of the RootDataEntity MUST point to a Project entity.""" ], diff --git a/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py b/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py index 2aacf26e3..98dd6b233 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py @@ -48,7 +48,7 @@ def test_5src_root_data_entity_two_main_entities(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST reference a workflow crate"], expected_triggered_issues=["The RootDataEntity MUST have exactly one mainEntity property that is an IRI."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -75,7 +75,7 @@ def test_5src_root_data_entity_no_main_entity(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST reference a workflow crate"], expected_triggered_issues=["The RootDataEntity MUST have exactly one mainEntity property that is an IRI."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -108,7 +108,7 @@ def test_5src_root_data_entity_main_entity_not_dataset_iri(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST reference a workflow crate"], expected_triggered_issues=["The mainEntity pointed to by the RootDataEntity MUST be of type Dataset"], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, diff --git a/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py b/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py index c2a82ba3e..4878d3a10 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py @@ -50,7 +50,7 @@ def test_rocrate_does_not_have_createaction(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["RootDataEntity", "Root data entity MUST mention a workflow run"], expected_triggered_issues=[ "`RootDataEntity` MUST reference at least one `CreateAction`", " (corresponding to the workflow run) through `mentions`", @@ -86,7 +86,7 @@ def test_rootdataentity_does_not_have_mentions_property(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST mention a workflow run"], expected_triggered_issues=[ "`RootDataEntity` MUST reference at least one `CreateAction`", " (corresponding to the workflow run) through `mentions`", @@ -120,7 +120,7 @@ def test_rootdataentity_does_not_mention_create_action(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST mention a workflow run"], expected_triggered_issues=[ "`RootDataEntity` MUST reference at least one `CreateAction`", " (corresponding to the workflow run) through `mentions`", @@ -191,7 +191,7 @@ def test_createaction_does_not_have_object_property(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowRunAction"], + expected_triggered_requirements=["Workflow run SHOULD have an object"], expected_triggered_issues=[ "`CreateAction` (corresponding to the workflow run) SHOULD have the property `object` with IRI values." ], diff --git a/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py index 8113a5111..6f70ff13e 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py @@ -45,7 +45,7 @@ def test_5src_disclosure_object_with_no_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST have a name string of at least 10 characters."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -74,7 +74,7 @@ def test_5src_disclosure_object_with_name_not_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST have a name string of at least 10 characters."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -103,7 +103,7 @@ def test_5src_disclosure_object_with_not_long_enough_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST have a name string of at least 10 characters."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -131,7 +131,7 @@ def test_5src_disclosure_object_not_an_assess_action(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST be a `AssessAction`."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -159,7 +159,7 @@ def test_5src_disclosure_object_with_no_proper_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have an allowed action status"], expected_triggered_issues=[ ( "`DisclosureCheck` MUST have an actionStatus with an allowed value " @@ -257,7 +257,7 @@ def test_5src_disclosure_object_not_mentioned_by_root_data_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention DisclosureCheck"], expected_triggered_issues=["`RootDataEntity` SHOULD mention a disclosure object."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -282,7 +282,7 @@ def test_5src_disclosure_object_with_no_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck SHOULD have an action status"], expected_triggered_issues=["The `DisclosureCheck` SHOULD have `actionStatus` property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -312,7 +312,7 @@ def test_5src_disclosure_object_has_no_end_time_if_ended(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck SHOULD have an end time when ended"], expected_triggered_issues=[ ( "`DisclosureCheck` SHOULD have the `endTime` property if `actionStatus` " From 3b2c72b81e6dcd03b861293602b552bcfaffa0ef Mon Sep 17 00:00:00 2001 From: EttoreM Date: Wed, 11 Nov 2026 11:17:11 +0000 Subject: [PATCH 271/271] Preserve RO-Crate 1.1 metadata-only coverage and add 1.3 cases --- .../profiles/test_metadata_only.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/integration/profiles/test_metadata_only.py b/tests/integration/profiles/test_metadata_only.py index 21f46faca..1347a97cf 100644 --- a/tests/integration/profiles/test_metadata_only.py +++ b/tests/integration/profiles/test_metadata_only.py @@ -21,8 +21,8 @@ import pytest from rocrate_validator import models -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from tests.ro_crates import ValidROC +from tests.ro_crates_v1_3 import ContextualEntities, RootDataEntity, ValidROCrate13 from tests.shared import do_entity_test # set up logging @@ -50,7 +50,7 @@ def valid_roc_paths(): @pytest.mark.parametrize("valid_roc_path", valid_roc_paths()) -def test_valid_ro_crates_from_folder(valid_roc_path): +def test_valid_ro_crates_from_folder(valid_roc_path, profile_identifier="ro-crate-1.1"): """Test all valid RO-Crates.""" with tempfile.TemporaryDirectory() as tmpdirname: temp_path = Path(tmpdirname) / valid_roc_path.name @@ -64,14 +64,14 @@ def test_valid_ro_crates_from_folder(valid_roc_path): [], profile_identifier=( # ro-crate-1.1 can't be used for Five Safes crates as they conform to 1.2 - "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else DEFAULT_PROFILE_IDENTIFIER + "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else profile_identifier ), metadata_only=True, ) @pytest.mark.parametrize("valid_roc_path", valid_roc_paths()) -def test_valid_ro_crates_from_metadata_dict(valid_roc_path): +def test_valid_ro_crates_from_metadata_dict(valid_roc_path, profile_identifier="ro-crate-1.1"): """Test all valid RO-Crates using metadata dict.""" metadata_dict = None # Load the metadata dict from the RO-Crate @@ -88,8 +88,33 @@ def test_valid_ro_crates_from_metadata_dict(valid_roc_path): [], profile_identifier=( # ro-crate-1.1 can't be used for Five Safes crates as they conform to 1.2 - "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else DEFAULT_PROFILE_IDENTIFIER + "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else profile_identifier ), metadata_dict=metadata_dict, metadata_only=True, ) + + +def valid_roc_13_paths(): + """Existing RO-Crate 1.3 examples, separate from the original 1.1 fixtures.""" + valid_roc = ValidROCrate13() + contextual_entities = ContextualEntities() + return [ + pytest.param(valid_roc.attached, id="attached"), + pytest.param(valid_roc.attached_absolute_root, id="absolute-root"), + pytest.param(RootDataEntity().valid_additional_conformsTo_reference, id="profile-reference"), + pytest.param(contextual_entities.valid_software_application, id="software-application"), + pytest.param(contextual_entities.valid_computer_language, id="computer-language"), + ] + + +@pytest.mark.parametrize("valid_roc_path", valid_roc_13_paths()) +def test_valid_ro_crates_1_3_from_folder(valid_roc_path): + """Exercise the folder input mode with RO-Crate 1.3 fixtures.""" + test_valid_ro_crates_from_folder(valid_roc_path, profile_identifier="ro-crate-1.3") + + +@pytest.mark.parametrize("valid_roc_path", valid_roc_13_paths()) +def test_valid_ro_crates_1_3_from_metadata_dict(valid_roc_path): + """Exercise the dictionary input mode with RO-Crate 1.3 fixtures.""" + test_valid_ro_crates_from_metadata_dict(valid_roc_path, profile_identifier="ro-crate-1.3")