Skip to content

Commit

Permalink
release: 🔖 version 0.4.4
Browse files Browse the repository at this point in the history
  • Loading branch information
kikkomep committed Nov 7, 2024
2 parents 9310f2c + dec7f84 commit 48b72be
Show file tree
Hide file tree
Showing 20 changed files with 475 additions and 28 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "roc-validator"
version = "0.4.3"
version = "0.4.4"
description = "A Python package to validate RO-Crates"
authors = [
"Marco Enrico Piras <[email protected]>",
Expand Down
3 changes: 2 additions & 1 deletion rocrate_validator/cli/commands/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from rich.console import Console

import rocrate_validator.log as logging
from rocrate_validator.errors import InvalidProfilePath, ProfileNotFound, ProfilesDirectoryNotFound
from rocrate_validator.errors import (InvalidProfilePath, ProfileNotFound,
ProfilesDirectoryNotFound)

# Create a logger for this module
logger = logging.getLogger(__name__)
Expand Down
20 changes: 8 additions & 12 deletions rocrate_validator/cli/commands/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@
from rocrate_validator.cli.main import cli, click
from rocrate_validator.cli.utils import Console, get_app_header_rule
from rocrate_validator.colors import get_severity_color
from rocrate_validator.errors import ROCrateInvalidURIError
from rocrate_validator.events import Event, EventType, Subscriber
from rocrate_validator.models import (LevelCollection, Profile, Severity,
ValidationResult)
from rocrate_validator.utils import URI, get_profiles_path
from rocrate_validator.utils import (URI, get_profiles_path,
validate_rocrate_uri)

# from rich.markdown import Markdown
# from rich.table import Table
Expand All @@ -58,17 +60,11 @@ def validate_uri(ctx, param, value):
"""
if value:
try:
# parse the value to extract the scheme
uri = URI(value)
if not uri.is_remote_resource() and not uri.is_local_directory() and not uri.is_local_file():
raise click.BadParameter(f"Invalid RO-Crate URI \"{value}\": "
"it MUST be a local directory or a ZIP file (local or remote).", param=param)
if not uri.is_available():
raise click.BadParameter("RO-crate URI not available", param=param)
except ValueError as e:
logger.debug(e)
raise click.BadParameter("Invalid RO-crate path or URI", param=param)

validate_rocrate_uri(value)
except ROCrateInvalidURIError as e:
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise click.BadParameter(e.message, param=param)
return value


Expand Down
14 changes: 8 additions & 6 deletions rocrate_validator/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ def __repr__(self):
class ROCrateInvalidURIError(ROCValidatorError):
"""Raised when an invalid URI is provided."""

def __init__(self, uri: Optional[str] = None, message: Optional[str] = None):
def __init__(self, uri: str, message: Optional[str] = None):
self._uri = uri
self._message = message
self._message = message or self.default_error_message(uri)

@property
def uri(self) -> Optional[str]:
Expand All @@ -258,14 +258,16 @@ def message(self) -> Optional[str]:
return self._message

def __str__(self) -> str:
if self._message:
return f"Invalid URI \"{self._uri!r}\": {self._message!r}"
else:
return f"Invalid URI \"{self._uri!r}\""
return self._message

def __repr__(self):
return f"ROCrateInvalidURIError({self._uri!r})"

@classmethod
def default_error_message(cls, uri: str) -> str:
return f"\"{uri}\" is not a valid RO-Crate URI. "\
"It MUST be either a local path to the RO-Crate root directory or a local/remote RO-Crate ZIP file."


class ROCrateMetadataNotFoundError(ROCValidatorError):
"""Raised when the RO-Crate metadata is not found."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any
import rocrate_validator.log as logging
from rocrate_validator.models import ValidationContext
from rocrate_validator.requirements.python import (PyFunctionCheck, check,
Expand Down Expand Up @@ -93,6 +94,43 @@ def check_context(self, context: ValidationContext) -> bool:
logger.exception(e)
return False

@check(name="File Descriptor JSON-LD must be flattened")
def check_flattened(self, context: ValidationContext) -> bool:
""" Check if the file descriptor is flattened """

def is_entity_flat_recursive(entity: Any, is_first: bool = True) -> bool:
""" Recursively check if the given data corresponds to a flattened JSON-LD object
and returns False if it does not and is not a root element
"""
if isinstance(entity, dict):
if is_first:
for _, elem in entity.items():
if not is_entity_flat_recursive(elem, False):
return False
# if this is not the root element, it must not contain more properties than @id
else:
if "@id" in entity and len(entity) > 1:
return False
if isinstance(entity, list):
for element in entity:
if not is_entity_flat_recursive(element, False):
return False
return True

try:
json_dict = context.ro_crate.metadata.as_dict()
for entity in json_dict["@graph"]:
if not is_entity_flat_recursive(entity):
context.result.add_error(
f'RO-Crate file descriptor "{context.rel_fd_path}" '
f'is not fully flattened at entity "{entity.get("@id", entity)}"', self)
return False
return True
except Exception as e:
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
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 """
Expand Down
3 changes: 2 additions & 1 deletion rocrate_validator/requirements/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def hidden(self) -> bool:
return getattr(self.requirement_check_class, "hidden", False)


def requirement(name: str, description: Optional[str] = None):
def requirement(name: str, description: Optional[str] = None, hidden: bool = False):
"""
A decorator to mark functions as "requirements" (by setting an attribute
`requirement=True`) and annotating them with a human-legible name.
Expand All @@ -117,6 +117,7 @@ def decorator(cls):
cls.__rq_name__ = name
if description:
cls.__rq_description__ = description
cls.hidden = hidden
return cls

return decorator
Expand Down
8 changes: 4 additions & 4 deletions rocrate_validator/rocrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

from rocrate_validator import log as logging
from rocrate_validator.errors import ROCrateInvalidURIError
from rocrate_validator.utils import URI
from rocrate_validator.utils import URI, validate_rocrate_uri

# set up logging
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -362,8 +362,8 @@ def get_external_file_size(uri: str) -> int:
@staticmethod
def new_instance(uri: Union[str, Path, URI]) -> 'ROCrate':
# check if the URI is valid
if not uri:
raise ValueError("Invalid URI")
validate_rocrate_uri(uri, silent=False)
# create a new instance based on the URI
if not isinstance(uri, URI):
uri = URI(uri)
# check if the URI is a local directory
Expand All @@ -376,7 +376,7 @@ def new_instance(uri: Union[str, Path, URI]) -> 'ROCrate':
if uri.is_remote_resource():
return ROCrateRemoteZip(uri)
# if the URI is not supported, raise an error
raise ROCrateInvalidURIError(uri=uri, message="Unsupported URI")
raise ROCrateInvalidURIError(uri=uri, message="Unsupported RO-Crate URI")


class ROCrateLocalFolder(ROCrate):
Expand Down
37 changes: 36 additions & 1 deletion rocrate_validator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def __init__(self, uri: Union[str, Path]):
except Exception as e:
if logger.isEnabledFor(logging.DEBUG):
logger.debug(e)
raise ValueError("Invalid URI: %s", uri)
raise ValueError("Invalid URI: %s" % uri)

@property
def uri(self) -> str:
Expand Down Expand Up @@ -458,6 +458,41 @@ def __hash__(self):
return hash(self._uri)


def validate_rocrate_uri(uri: Union[str, URI], silent: bool = False) -> bool:
"""
Validate the RO-Crate URI
:param uri: The RO-Crate URI
:param silent: If True, do not raise an exception
:return: True if the URI is valid, False otherwise
"""
try:
assert uri, "The RO-Crate URI is required"
assert isinstance(uri, (str, URI)), "The RO-Crate URI must be a string or URI object"
try:
# parse the value to extract the scheme
uri = URI(uri) if isinstance(uri, str) else uri
# check if the URI is a remote resource or local directory or local file
if not uri.is_remote_resource() and not uri.is_local_directory() and not uri.is_local_file():
raise errors.ROCrateInvalidURIError(uri)
# check if the local file is a ZIP file
if uri.is_local_file() and uri.as_path().suffix != ".zip":
raise errors.ROCrateInvalidURIError(uri)
# check if the resource is available
if not uri.is_available():
raise errors.ROCrateInvalidURIError(uri, message=f"The RO-crate at the URI \"{uri}\" is not available")
return True
except ValueError as e:
logger.error(e)
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise errors.ROCrateInvalidURIError(uri)
except Exception as e:
if not silent:
raise e
return False


class MapIndex:

def __init__(self, name: str, unique: bool = False):
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ def profiles_requirement_loading():
return f"{TEST_DATA_PATH}/profiles/requirement_loading"


@fixture
def profiles_loading_hidden_requirements():
return f"{TEST_DATA_PATH}/profiles/hidden_requirements"


@fixture
def profiles_with_free_folder_structure_path():
return f"{TEST_DATA_PATH}/profiles/free_folder_structure"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"@context": "https://w3id.org/ro/crate/1.1/context",
"@graph": [
{
"@type": "CreativeWork",
"@id": "ro-crate-metadata.json",
"conformsTo": {
"@id": "https://w3id.org/ro/crate/1.1"
},
"about": {
"@type": "Dataset",
"@id": "./",
"hasPart": [
{
"@type": "File",
"@id": "test.csv",
"encodingFormat": "text/csv",
"name": "This is a test file",
"description": "This is a test 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"
}
}
]
}
46 changes: 46 additions & 0 deletions tests/data/profiles/hidden_requirements/xh/a.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@prefix dct: <http://purl.org/dc/terms/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ro: <./> .
@prefix schema_org: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix validator: <https://github.com/crs4/rocrate-validator/> .
@prefix xml1: <http://www.w3.org/2001/XMLSchema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ro:A
a sh:NodeShape, validator:HiddenShape ;
sh:name "A" ;
sh:description "This is the requirement A" ;
sh:targetNode ro:ro-crate-metadata.json ;
sh:property [
a sh:PropertyShape ;
sh:name "A_0" ;
sh:description "Check A_0: no sh:severity declared" ;
sh:path rdf:type ;
sh:minCount 1 ;
] ;
sh:property [
a sh:PropertyShape ;
sh:name "A_1" ;
sh:description "Check A_1: sh:severity set to sh:Violation" ;
sh:path rdf:type ;
sh:minCount 1 ;
sh:severity sh:Violation ;
] ;
sh:property [
a sh:PropertyShape ;
sh:name "A_2" ;
sh:description "Check A_2: sh:severity set to sh:Warning" ;
sh:path rdf:type ;
sh:minCount 1 ;
sh:severity sh:Warning ;
] ;
sh:property [
a sh:PropertyShape ;
sh:name "A_3" ;
sh:description "Check A_3: sh:severity set to sh:Info" ;
sh:path rdf:type ;
sh:minCount 1 ;
sh:severity sh:Info ;
] .

48 changes: 48 additions & 0 deletions tests/data/profiles/hidden_requirements/xh/bh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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 rocrate_validator.log as logging
from rocrate_validator.models import Severity, ValidationContext
from rocrate_validator.requirements.python import (PyFunctionCheck, check,
requirement)

# set up logging
logger = logging.getLogger(__name__)


@requirement(name="B", hidden=True)
class BH(PyFunctionCheck):
"""
Test requirement outside requirement level folder
"""

@check(name="B_0")
def check_b0(self, context: ValidationContext) -> bool:
"""Check B_0: no requirement level"""
return True

@check(name="B_1", severity=Severity.REQUIRED)
def check_b1(self, context: ValidationContext) -> bool:
"""Check B_1: REQUIRED requirement level"""
return True

@check(name="B_2", severity=Severity.RECOMMENDED)
def check_b2(self, context: ValidationContext) -> bool:
"""Check B_2: RECOMMENDED requirement level"""
return True

@check(name="B_3", severity=Severity.OPTIONAL)
def check_b3(self, context: ValidationContext) -> bool:
"""Check B_3: OPTIONAL requirement level"""
return True
Loading

0 comments on commit 48b72be

Please sign in to comment.