From ba613474bd29ba7454a0c7184f18841214a1b6da Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:12:46 +0100 Subject: [PATCH 01/24] fix(cli): :bug: do not skip overridden checks when they belong to the target profile --- 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 2bc4a31d..f5b88511 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -817,7 +817,7 @@ def __compute_profile_stats__(validation_settings: dict): # count the checks num_checks = len( [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) - if not _.overridden]) + if not _.overridden or _.requirement.profile == profile]) check_count_by_severity[severity] += num_checks requirement_checks_count += num_checks From a992d3713fecec12075e90406125e6cc4c246b2e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:14:55 +0100 Subject: [PATCH 02/24] feat(core): :zap: extend model to include hidden requirement checks --- rocrate_validator/models.py | 10 +++++++++- rocrate_validator/requirements/shacl/checks.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 74714a92..1e1cfb36 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -839,12 +839,14 @@ def __init__(self, requirement: Requirement, name: str, level: Optional[RequirementLevel] = LevelCollection.REQUIRED, - description: Optional[str] = None): + description: Optional[str] = None, + hidden: Optional[bool] = None): self._requirement: Requirement = requirement self._order_number = 0 self._name = name self._level = level self._description = description + self._hidden = hidden @property def order_number(self) -> int: @@ -912,6 +914,12 @@ def override(self) -> list[RequirementCheck]: def overridden(self) -> bool: return len(self.overridden_by) > 0 + @property + def hidden(self) -> bool: + if self._hidden is not None: + return self._hidden + return self.requirement.hidden + @abstractmethod def execute_check(self, context: ValidationContext) -> bool: raise NotImplementedError() diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index e910a878..afaf4bf1 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -43,7 +43,7 @@ class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, - shape: Shape) -> None: + hidden: Optional[bool] = None, self._shape = shape # init the check super().__init__(requirement, @@ -52,7 +52,7 @@ def __init__(self, else None, shape.description if shape and shape.description else shape.parent.description if shape.parent - else None) + hidden=hidden) # store the instance SHACLCheck.__add_instance__(shape, self) From d9ced660f6d3fac8262f9c01373b35239e03387c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:20:28 +0100 Subject: [PATCH 03/24] feat(shacl): :zap: extend model to mark root requirement checks The root requirement check is the validation associated with the entire SHACL shape, which can include further individual checks --- rocrate_validator/requirements/shacl/checks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index afaf4bf1..971f1406 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -43,8 +43,10 @@ class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, + root: bool = False, hidden: Optional[bool] = None, self._shape = shape + self._root = root # init the check super().__init__(requirement, shape.name if shape and shape.name @@ -72,6 +74,10 @@ def __init__(self, def shape(self) -> Shape: return self._shape + @property + def root(self) -> bool: + return self._root + @property def description(self) -> str: return self._shape.description From e1a4ff6b993138e0c8ec5645af83bc5c55664f52 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:21:57 +0100 Subject: [PATCH 04/24] feat(shacl): :sparkles: extend SHACL check initialisation --- rocrate_validator/requirements/shacl/checks.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 971f1406..2d0b7698 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -43,17 +43,22 @@ class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, + shape: Shape, + name: Optional[str] = None, root: bool = False, hidden: Optional[bool] = None, + level: Optional[bool] = None) -> None: self._shape = shape self._root = root # init the check super().__init__(requirement, - shape.name if shape and shape.name + name or shape.name if shape and shape.name else shape.parent.name if shape.parent else None, - shape.description if shape and shape.description + description=shape.description if shape and shape.description else shape.parent.description if shape.parent + else None, + level=level, hidden=hidden) # store the instance SHACLCheck.__add_instance__(shape, self) @@ -68,7 +73,7 @@ def __init__(self, "shape level %s does not match the level from the containing folder %s. " "Consider moving the shape property or removing the severity property.", self.name, shape.level, requirement_level_from_path) - self._level = None + self._level = level @property def shape(self) -> Shape: From c5d3086ecc34142711e6f0ffab52fe179be3826b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:36:30 +0100 Subject: [PATCH 05/24] feat(shacl): :zap: add root requirement check --- rocrate_validator/requirements/shacl/requirements.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 28dec2a0..4f02c7d5 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -48,6 +48,11 @@ def __init__(self, def __init_checks__(self) -> list[RequirementCheck]: # assign a check to each property of the shape checks = [] + # check if the shape has nested properties + has_properties = hasattr(self.shape, "properties") and len(self.shape.properties) > 0 + # create a check for the shape itself, hidden if the shape has nested properties + checks.append(SHACLCheck(self, self.shape, name=f"Check {self.shape.name}" if has_properties else None, + hidden=has_properties, root=True)) # create a check for each property if the shape has nested properties if hasattr(self.shape, "properties"): for prop in self.shape.properties: From d39d0de7987ff14d6d43a4ca4999acfca535718d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:39:01 +0100 Subject: [PATCH 06/24] refactor(shacl): :recycle: assign an order of 0 to the root check --- rocrate_validator/requirements/shacl/requirements.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 4f02c7d5..5c734b81 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -45,6 +45,12 @@ def __init__(self, # assign check IDs self.__reorder_checks__() + def __reorder_checks__(self) -> None: + i = 0 + for check in self._checks: + check.order_number = i + i += 1 + def __init_checks__(self) -> list[RequirementCheck]: # assign a check to each property of the shape checks = [] From 8ab73fd0ffa2971ced4149dcf4fad14fa3efbedf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:40:22 +0100 Subject: [PATCH 07/24] refactor(shacl): :recycle: restructure the initialisation of shack checks --- rocrate_validator/requirements/shacl/requirements.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 5c734b81..3c5a419a 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -52,6 +52,9 @@ def __reorder_checks__(self) -> None: i += 1 def __init_checks__(self) -> list[RequirementCheck]: + # check if the shape is not None before creating checks + assert self.shape is not None, "The shape cannot be None" + assert self.shape.node is not None, "The shape node cannot be None" # assign a check to each property of the shape checks = [] # check if the shape has nested properties @@ -60,17 +63,13 @@ def __init_checks__(self) -> list[RequirementCheck]: checks.append(SHACLCheck(self, self.shape, name=f"Check {self.shape.name}" if has_properties else None, hidden=has_properties, root=True)) # create a check for each property if the shape has nested properties - if hasattr(self.shape, "properties"): + if has_properties: for prop in self.shape.properties: logger.debug("Creating check for property %s %s", prop.name, prop.description) property_check = SHACLCheck(self, prop) logger.debug("Property check %s: %s", property_check.name, property_check.description) checks.append(property_check) - # if no property checks, add a generic one - assert self.shape is not None, "The shape cannot be None" - if len(checks) == 0 and self.shape is not None and self.shape.node is not None: - checks.append(SHACLCheck(self, self.shape)) return checks @property From 7a79cb039133603ea26c007f909818bee996816d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Nov 2024 16:41:11 +0100 Subject: [PATCH 08/24] test: :white_check_mark: add and update tests --- .../requirements/test_load_requirements.py | 6 +++-- tests/unit/requirements/test_profiles.py | 2 +- tests/unit/test_cli_internals.py | 24 ++++++++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index 770c320d..74ccaaa8 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -17,6 +17,7 @@ from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.models import LevelCollection, Profile, Severity +from rocrate_validator.requirements.shacl.requirements import SHACLRequirement from tests.ro_crates import InvalidFileDescriptorEntity # set up logging @@ -76,12 +77,13 @@ def test_requirements_loading(profiles_requirement_loading: str): assert requirement.severity_from_path == Severity.REQUIRED, \ "The severity of the requirement should be REQUIRED" - assert len(requirement.get_checks()) == number_of_checks_per_requirement, \ + offset = 1 if isinstance(requirement, SHACLRequirement) else 0 + assert len(requirement.get_checks()) == number_of_checks_per_requirement + offset, \ "The number of requirement checks is incorrect" for i in range(number_of_checks_per_requirement): logger.debug("The requirement check: %r", f"{requirement_name}_{i}") - check = requirement.get_checks()[i] + check = requirement.get_checks()[i+offset] assert check.name == f"{requirement_name}_{i}", "The name of the requirement check is incorrect" assert check.level.severity == levels[i].severity, "The level of the requirement check is incorrect" diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 46c9f122..280de4aa 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -401,7 +401,7 @@ def check_profile(profile, check, inherited_profiles, overridden_by, override): logger.debug("The requirement: %r of the profile %r", requirement, profile.token) # The number of checks should be 1 logger.debug("The number of checks: %r", len(requirement.get_checks())) - assert len(requirement.get_checks()) == 1, "The number of checks should be 1" + assert len(requirement.get_checks()) == 2, "The number of checks should be 2" # Get the check check = requirement.get_checks()[0] diff --git a/tests/unit/test_cli_internals.py b/tests/unit/test_cli_internals.py index 7685ea33..19c45517 100644 --- a/tests/unit/test_cli_internals.py +++ b/tests/unit/test_cli_internals.py @@ -17,24 +17,24 @@ from rocrate_validator import log as logging from rocrate_validator.cli.commands.validate import __compute_profile_stats__ -from rocrate_validator.models import DEFAULT_PROFILES_PATH, Profile +from rocrate_validator.models import DEFAULT_PROFILES_PATH, LevelCollection, Profile # set up logging logger = logging.getLogger(__name__) -def test_compute_stats(): +def test_compute_stats(fake_profiles_path): settings = { "fail_fast": False, - "profiles_path": DEFAULT_PROFILES_PATH, - "profile_identifier": "ro-crate", + "profiles_path": fake_profiles_path, + "profile_identifier": "a", "inherit_profiles": True, "allow_requirement_check_override": True, "requirement_severity": "REQUIRED", } - profiles_path = DEFAULT_PROFILES_PATH + profiles_path = settings["profiles_path"] logger.debug("The profiles path: %r", DEFAULT_PROFILES_PATH) assert os.path.exists(profiles_path) profiles = Profile.load_profiles(profiles_path) @@ -45,7 +45,7 @@ def test_compute_stats(): profile = profiles[0] logger.debug("The profile: %r", profile) assert profile is not None - assert profile.identifier == "ro-crate-1.1" + assert profile.identifier == "a" # extract the list of not hidden requirements logger.error("The number of requirements: %r", len(profile.get_requirements())) @@ -64,4 +64,16 @@ def test_compute_stats(): # check the number of requirements in stats and the number of requirements in the profile assert stats["total_requirements"] == len(requirements) + # Check if the number of requirements is greater than 0 + assert stats["total_requirements"] > 0 + + # extract the first and unique requirement + requirement = requirements[0] + + # check the number of checks in the requirement + assert len(requirement.get_checks()) == len(requirement.get_checks_by_level(LevelCollection.get("REQUIRED"))) + + # check the number of requirement checks + assert stats["total_checks"] == len(requirements[0].get_checks()) + logger.error(stats) From 2c762c2aff3c600c4febf84505ed821853a8d764 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 10:31:19 +0100 Subject: [PATCH 09/24] feat(test): :zap: add the ability to dynamically update test data --- tests/shared.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/shared.py b/tests/shared.py index 5e353dc9..c83bdfbc 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -41,7 +41,8 @@ def do_entity_test( expected_triggered_requirements: Optional[list[str]] = None, expected_triggered_issues: Optional[list[str]] = None, abort_on_first: bool = True, - profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, + rocrate_entity_patch: Optional[dict] = None, ): """ Shared function to test a RO-Crate entity @@ -53,6 +54,26 @@ def do_entity_test( if not isinstance(rocrate_path, Path): rocrate_path = Path(rocrate_path) + temp_rocrate_path = None + if rocrate_entity_patch is not None and rocrate_path.is_dir(): + # create a temporary copy of the RO-Crate + temp_rocrate_path = Path(tempfile.TemporaryDirectory().name) + # copy the RO-Crate to the temporary path using shutil + shutil.copytree(rocrate_path, temp_rocrate_path) + # load the RO-Crate metadata as RO-Crate JSON-LD + with open(temp_rocrate_path / "ro-crate-metadata.json", "r") as f: + rocrate = json.load(f) + # update the RO-Crate metadata with the patch + for key, value in rocrate_entity_patch.items(): + for entity in rocrate["@graph"]: + if entity["@id"] == key: + entity.update(value) + break + # save the updated RO-Crate metadata + with open(temp_rocrate_path / "ro-crate-metadata.json", "w") as f: + json.dump(rocrate, f) + rocrate_path = temp_rocrate_path + if expected_triggered_requirements is None: expected_triggered_requirements = [] if expected_triggered_issues is None: @@ -111,3 +132,8 @@ def do_entity_test( logger.debug("Failed requirements: %s", failed_requirements) logger.debug("Detected issues: %s", detected_issues) raise e + finally: + # cleanup + if temp_rocrate_path is not None: + logger.debug("Cleaning up temporary RO-Crate @ path: %s", temp_rocrate_path) + shutil.rmtree(temp_rocrate_path) From 124e2b16bd025da0c6e552886210dc221c2be7ee Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 12:44:34 +0100 Subject: [PATCH 10/24] feat: :sparkles: add a more comprehensive pattern to detect valid ISO 8601 dates --- .../profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/2_root_data_entity_metadata.ttl index 290cc069..c1c64a45 100644 --- a/rocrate_validator/profiles/ro-crate/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -148,13 +148,12 @@ ro-crate:RootDataEntityRequiredProperties 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. - It SHOULD be specified to at least the day level, but MAY include a time component.""" ; + to provide the date when the dataset was published. The datePublished MUST be a valid ISO 8601 date.""" ; sh:minCount 1 ; sh:nodeKind sh:Literal ; sh:path schema_org:datePublished ; - sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; - sh:message "The Root Data Entity MUST have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date and the precision of at least the day level" ; + 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:RootDataEntityHasPartValueRestriction From aa7ac92d28f07a74e4a45a3d6503547ccbf56058 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 12:46:23 +0100 Subject: [PATCH 11/24] fix(test): :bug: missing imports --- tests/shared.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/shared.py b/tests/shared.py index c83bdfbc..e9a8f317 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -16,7 +16,10 @@ Library of shared functions for testing RO-Crate profiles """ +import json import logging +import shutil +import tempfile from collections.abc import Collection from pathlib import Path from typing import Optional, TypeVar, Union From 9b13f5cb44e1775c804a063d703cc9700e157e08 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 12:48:56 +0100 Subject: [PATCH 12/24] test(profile/ro-crate): :white_check_mark: add more tests for valid and invalid datetimes --- tests/conftest.py | 36 +++++++++++++++++++ .../ro-crate/test_root_data_entity.py | 20 ++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index b58f2a73..c4034771 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -110,3 +110,39 @@ def ro_crate_profile_should_path(ro_crate_profile_path): @fixture def ro_crate_profile_may_path(ro_crate_profile_path): return os.path.join(ro_crate_profile_path, "may") + + +@fixture(params=[ + "2024 01 01", + "2024 Jan 01", + "2021-13-01", + "2021-00-10", + "2021-01-32", + "2021-01-01T25:00", + "2021-01-01T23:60", + "2021-01-01T23:59:60", + "T23:59:59", +]) +def invalid_datetime(request): + return request.param + + +@fixture(params=[ + "2024", + "2024-01", + "202401", + "2024-01-01", + "20240101", + "2024-001", + "2024-W01", + "2024-W01-1", + "2024-01-01T00:00", + "2024-01-01T00:00:00", + "2024-01-01T00:00:00Z", + "2024-01-01T00:00:00+00:00", + "2024-01-01T00:00:00.000", + "2024-01-01T00:00:00.000Z", + "2024-01-01T00:00:00.000+00:00", +]) +def valid_datetime(request): + return request.param diff --git a/tests/integration/profiles/ro-crate/test_root_data_entity.py b/tests/integration/profiles/ro-crate/test_root_data_entity.py index 21640bd3..2d6c425c 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -92,10 +92,28 @@ def test_recommended_root_data_entity_value(): ) -def test_invalid_root_date(): +def test_invalid_required_root_date(invalid_datetime): """Test a RO-Crate with an invalid root data entity date.""" do_entity_test( paths.invalid_root_date, + models.Severity.REQUIRED, + False, + ["RO-Crate Root Data Entity REQUIRED properties"], + ["The Root Data Entity MUST have a `datePublished` property (as specified by schema.org) " + "with a valid ISO 8601 date"], + rocrate_entity_patch={"./": {"datePublished": invalid_datetime}} + ) + + +def test_valid_required_root_date(valid_datetime): + """Test a RO-Crate with a valid root data entity date.""" + do_entity_test( + ValidROC().wrroc_paper, + models.Severity.REQUIRED, + True, + rocrate_entity_patch={"./": {"datePublished": valid_datetime}} + ) + models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], From 43331409b1cd8410c4666d1d06a32ebc64164d68 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 12:51:36 +0100 Subject: [PATCH 13/24] feat(profile/ro-crate): :zap: add check for recommended values of the `RootDataEntity` `datePublished` property --- .../ro-crate/should/2_root_data_entity_metadata.ttl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_metadata.ttl index 23fcb04b..06517cdc 100644 --- a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_metadata.ttl @@ -59,4 +59,16 @@ ro-crate:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:message "The `publisher` property of a `Root Data Entity` SHOULD be an `Organization`"; sh:nodeKind sh:IRI ; sh:class schema_org:Organization ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: RECOMMENDED `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. + It SHOULD be specified to at least the day level, but MAY include a time component.""" ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + 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 and the precision of at least the day level" ; ] . From b479be4031980809b76a0f10a37347c207805d42 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 12:52:30 +0100 Subject: [PATCH 14/24] test(profile/ro-crate): :white_check_mark: test recommended value of the `RootDataEntity` `datePublished` property --- .../ro-crate-metadata.json | 152 ++++++++++++++++++ .../ro-crate/test_root_data_entity.py | 9 +- tests/ro_crates.py | 4 + 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 tests/data/crates/invalid/2_root_data_entity_metadata/invalid_recommended_root_date/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_recommended_root_date/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_recommended_root_date/ro-crate-metadata.json new file mode 100644 index 00000000..bcfea981 --- /dev/null +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_recommended_root_date/ro-crate-metadata.json @@ -0,0 +1,152 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + { + "GithubService": "https://w3id.org/ro/terms/test#GithubService", + "JenkinsService": "https://w3id.org/ro/terms/test#JenkinsService", + "PlanemoEngine": "https://w3id.org/ro/terms/test#PlanemoEngine", + "TestDefinition": "https://w3id.org/ro/terms/test#TestDefinition", + "TestInstance": "https://w3id.org/ro/terms/test#TestInstance", + "TestService": "https://w3id.org/ro/terms/test#TestService", + "TestSuite": "https://w3id.org/ro/terms/test#TestSuite", + "TravisService": "https://w3id.org/ro/terms/test#TravisService", + "definition": "https://w3id.org/ro/terms/test#definition", + "engineVersion": "https://w3id.org/ro/terms/test#engineVersion", + "instance": "https://w3id.org/ro/terms/test#instance", + "resource": "https://w3id.org/ro/terms/test#resource", + "runsOn": "https://w3id.org/ro/terms/test#runsOn" + } + ], + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "202401", + "description": "This RO Crate contains the workflow MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": "MIT", + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + } + ] + }, + { + "@id": "my-workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "MyWorkflow", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "url": "https://github.com/kikkomep/myworkflow", + "version": "main" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02", + "@type": "TestSuite", + "definition": { + "@id": "my-workflow-test.yml" + }, + "instance": [ + { + "@id": "#350f2567-6ed2-4080-b354-a0921f49a4a9" + } + ], + "mainEntity": { + "@id": "my-workflow.ga" + }, + "name": "Test suite for MyWorkflow" + }, + { + "@id": "#350f2567-6ed2-4080-b354-a0921f49a4a9", + "@type": "TestInstance", + "name": "GitHub Actions workflow for testing MyWorkflow", + "resource": "repos/kikkomep/myworkflow/actions/workflows/main.yml", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#GithubService" + }, + "url": "https://api.github.com" + }, + { + "@id": "https://w3id.org/ro/terms/test#GithubService", + "@type": "TestService", + "name": "Github Actions", + "url": { + "@id": "https://github.com" + } + }, + { + "@id": "my-workflow-test.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + } + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + }, + { + "@id": "test-data/", + "@type": "Dataset", + "description": "Data files for testing the workflow" + }, + { + "@id": "README.md", + "@type": "File", + "description": "Workflow documentation" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/test_root_data_entity.py b/tests/integration/profiles/ro-crate/test_root_data_entity.py index 2d6c425c..8b7a02b4 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -15,7 +15,7 @@ import logging from rocrate_validator import models -from tests.ro_crates import InvalidRootDataEntity +from tests.ro_crates import InvalidRootDataEntity, ValidROC from tests.shared import do_entity_test # set up logging @@ -114,11 +114,16 @@ def test_valid_required_root_date(valid_datetime): rocrate_entity_patch={"./": {"datePublished": valid_datetime}} ) + +def test_invalid_recommended_root_date(): + """Test a RO-Crate with an invalid root data entity date.""" + do_entity_test( + paths.invalid_recommended_root_date, models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], ["The Root Data Entity MUST have a `datePublished` property (as specified by schema.org) " - "with a valid ISO 8601 date and the precision of at least the day level"] + "with a valid ISO 8601 date and the precision of at least the day level"] ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 538343fc..47cb268b 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -120,6 +120,10 @@ def recommended_root_value(self) -> Path: def invalid_root_date(self) -> Path: return self.base_path / "invalid_root_date" + @property + def invalid_recommended_root_date(self) -> Path: + return self.base_path / "invalid_recommended_root_date" + @property def missing_root_name(self) -> Path: return self.base_path / "missing_root_name" From 6828061624b4849510b542417805fbcf1a4282a7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 13:38:33 +0100 Subject: [PATCH 15/24] feat(profile/ro-crate): :zap: more comprehensive pattern to detect valid ISO 8601 sdDatePublished properties --- .../profiles/ro-crate/should/5_web_data_entity_metadata.ttl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.ttl index 759c1a9f..6509d717 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.ttl @@ -58,8 +58,6 @@ ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "Web-based Data Entity: `sdDatePublished` property" ; sh:description """Check if the Web-based Data Entity has a `sdDatePublished` property""" ; sh:path schema_org:sdDatePublished ; - # sh:datatype xsd:dateTime ; - sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; - sh:severity sh:Warning ; - sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property""" ; + 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""" ; ] . From 55bf68b4d8d04a0dd002f015bff899f83040282e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 13:39:49 +0100 Subject: [PATCH 16/24] test(profile/ro-crate): :white_check_mark: test `sdDatePublished` property of web data entities --- .../ro-crate-metadata.json | 140 ++++++++++++++++++ .../no_sdDatePublished/ro-crate-metadata.json | 139 +++++++++++++++++ .../ro-crate/test_web_based_data_entity.py | 51 +++++++ tests/ro_crates.py | 8 + 4 files changed, 338 insertions(+) create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/invalid_sdDatePublished/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/no_sdDatePublished/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate/test_web_based_data_entity.py diff --git a/tests/data/crates/invalid/4_data_entity_metadata/invalid_sdDatePublished/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/invalid_sdDatePublished/ro-crate-metadata.json new file mode 100644 index 00000000..30acfb00 --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/invalid_sdDatePublished/ro-crate-metadata.json @@ -0,0 +1,140 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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": "https://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://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.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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": "https://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": "https://sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + }, + "sdDatePublished": "2024 Jan 01" + }, + { + "@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/invalid/4_data_entity_metadata/no_sdDatePublished/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/no_sdDatePublished/ro-crate-metadata.json new file mode 100644 index 00000000..3e625e6b --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/no_sdDatePublished/ro-crate-metadata.json @@ -0,0 +1,139 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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": "https://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://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.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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": "https://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": "https://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/integration/profiles/ro-crate/test_web_based_data_entity.py b/tests/integration/profiles/ro-crate/test_web_based_data_entity.py new file mode 100644 index 00000000..663ac5ee --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_web_based_data_entity.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024 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 import InvalidDataEntity +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +#  Global set up the paths +paths = InvalidDataEntity() + + +def test_no_recommended_sdDatePublished(): + """Test a web based data entity without a recommended sdDatePublished property.""" + do_entity_test( + paths.no_sdDatePublished, + models.Severity.RECOMMENDED, + False, + ["Web-based Data Entity: RECOMMENDED properties"], + ["Web-based Data Entities SHOULD have " + "a `sdDatePublished` property to indicate when the absolute URL was accessed"] + ) + + +def test_invalid_recommended_sdDatePublished(invalid_datetime): + """Test a web based data entity with an invalid sdDatePublished property.""" + do_entity_test( + paths.invalid_sdDatePublished, + models.Severity.RECOMMENDED, + False, + ["Web-based Data Entity: RECOMMENDED properties"], + ["Web-based Data Entities SHOULD have " + "a `sdDatePublished` property to indicate when the absolute URL was accessed"], + rocrate_entity_patch={"https://sort-and-change-case.cwl": {"datePublished": invalid_datetime}} + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 47cb268b..13cd7fee 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -226,6 +226,14 @@ def valid_encoding_format_ctx_entity(self) -> Path: def valid_encoding_format_pronom(self) -> Path: return self.base_path / "valid_encoding_format_pronom" + @property + def no_sdDatePublished(self) -> Path: + return self.base_path / "no_sdDatePublished" + + @property + def invalid_sdDatePublished(self) -> Path: + return self.base_path / "invalid_sdDatePublished" + class InvalidMainWorkflow: From cf374a5fadfcf27fbb59d1ade0d88df902554038 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 17:32:21 +0100 Subject: [PATCH 17/24] build(python): :pushpin: update the minimum python version to 3.9.20 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 32a9c27e..1193d2a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ include = [ ] [tool.poetry.dependencies] -python = "^3.8.1" +python = "^3.9.20" rdflib = "^7.0.0" pyshacl = "^0.26.0" click = "^8.1.7" From bcf1f6f3d5fa636db11a847e970b794a39eb25bb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 17:38:27 +0100 Subject: [PATCH 18/24] build: :construction_worker: update poetry.lock --- poetry.lock | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5ac8c403..fe38dce6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "appnope" @@ -1120,8 +1120,8 @@ astroid = ">=3.2.4,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" @@ -1496,7 +1496,6 @@ files = [ [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.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -1704,5 +1703,5 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" -python-versions = "^3.8.1" -content-hash = "3973b73c6ba7eed27a4ebe8ce3f43654a2752abd6681a96802c06321ff977057" +python-versions = "^3.9.20" +content-hash = "3d2178523411060222b49dcd3e54d922115ce5fe3075d4db85703e3c63e687d2" From d5ad7ff08116f336be906477caeb03fcd3bd5b95 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 18:13:07 +0100 Subject: [PATCH 19/24] feat(utils): :sparkles: add utility functions to read and check the minimum required Python version --- rocrate_validator/utils.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 552b3a51..353ca6ec 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -142,6 +142,29 @@ def get_version() -> str: return f"{version}-dirty" if dirty else version +def get_min_python_version() -> tuple[int, int, Optional[int]]: + """ + Get the minimum Python version required by the package + + :return: The minimum Python version + """ + min_version_str = config["tool"]["poetry"]["dependencies"]["python"] + assert min_version_str, "The minimum Python version is required" + # remove any non-digit characters + min_version_str = re.sub(r'[^\d.]+', '', min_version_str) + # convert the version string to a tuple + min_version = tuple(map(int, min_version_str.split("."))) + logger.debug(f"Minimum Python version: {min_version}") + return min_version + + +def check_python_version() -> bool: + """ + Check if the current Python version meets the minimum requirements + """ + return sys.version_info >= get_min_python_version() + + def get_config(property: Optional[str] = None) -> dict: """ Get the configuration for the package or a specific property From 02497bb7a06fb0b1e7d2ed6fbe1d3c71d79888c6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 5 Nov 2024 18:15:07 +0100 Subject: [PATCH 20/24] feat(cli): :sparkles: check the minimum required Python version --- rocrate_validator/cli/main.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index c3876e19..01e8655c 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -18,7 +18,8 @@ import rocrate_validator.log as logging from rocrate_validator.cli.utils import Console, SystemPager -from rocrate_validator.utils import get_version +from rocrate_validator.utils import (check_python_version, + get_min_python_version, get_version) # set up logging logger = logging.getLogger(__name__) @@ -68,6 +69,13 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ ctx.obj['interactive'] = interactive try: + # Check the python version + if not check_python_version(): + console.print( + "\n[bold][red]ERROR:[/red] A Python version " + f"{'.'.join([str(_) for _ in get_min_python_version()])} or newer is required ! [/bold]") + sys.exit(1) + # If the version flag is set, print the version and exit if version: console.print( From b09746cf10b331e5ef6cb6694b858434ffc405a8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 6 Nov 2024 10:43:42 +0100 Subject: [PATCH 21/24] refactor(cli): :stethoscope: disable version check --- rocrate_validator/cli/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 01e8655c..1cf78915 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -18,8 +18,7 @@ import rocrate_validator.log as logging from rocrate_validator.cli.utils import Console, SystemPager -from rocrate_validator.utils import (check_python_version, - get_min_python_version, get_version) +from rocrate_validator.utils import get_version # set up logging logger = logging.getLogger(__name__) @@ -70,11 +69,12 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ try: # Check the python version - if not check_python_version(): - console.print( - "\n[bold][red]ERROR:[/red] A Python version " - f"{'.'.join([str(_) for _ in get_min_python_version()])} or newer is required ! [/bold]") - sys.exit(1) + # from rocrate_validator.utils import check_python_version, get_min_python_version + # if not check_python_version(): + # console.print( + # "\n[bold][red]ERROR:[/red] A Python version " + # f"{'.'.join([str(_) for _ in get_min_python_version()])} or newer is required ! [/bold]") + # sys.exit(1) # If the version flag is set, print the version and exit if version: From 5b91de66bdca6ebef1120081c2568f8095de2d89 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 6 Nov 2024 10:56:27 +0100 Subject: [PATCH 22/24] =?UTF-8?q?=F0=9F=94=96=20bump=20version=20number=20?= =?UTF-8?q?to=200.4.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 1193d2a2..6db142b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "roc-validator" -version = "0.4.2" +version = "0.4.3" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 219ba9027e86ee5f451d286b8c7a596c9e7cdf63 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 6 Nov 2024 11:21:11 +0100 Subject: [PATCH 23/24] fix(cli): skip counting overridden checks --- 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 f5b88511..2bc4a31d 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -817,7 +817,7 @@ def __compute_profile_stats__(validation_settings: dict): # count the checks num_checks = len( [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) - if not _.overridden or _.requirement.profile == profile]) + if not _.overridden]) check_count_by_severity[severity] += num_checks requirement_checks_count += num_checks From 5d508f9390d253358965ac441157197d07013c51 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 6 Nov 2024 13:01:40 +0100 Subject: [PATCH 24/24] test(cli): :white_check_mark: fix test --- tests/unit/test_cli_internals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_cli_internals.py b/tests/unit/test_cli_internals.py index 19c45517..f65f5ba9 100644 --- a/tests/unit/test_cli_internals.py +++ b/tests/unit/test_cli_internals.py @@ -74,6 +74,6 @@ def test_compute_stats(fake_profiles_path): assert len(requirement.get_checks()) == len(requirement.get_checks_by_level(LevelCollection.get("REQUIRED"))) # check the number of requirement checks - assert stats["total_checks"] == len(requirements[0].get_checks()) + assert stats["total_checks"] == len([_ for _ in requirements[0].get_checks() if not _.overridden]) logger.error(stats)